@plasius/gpu-renderer 0.2.5 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -24,9 +24,11 @@ __export(index_exports, {
24
24
  createGpuRenderer: () => createGpuRenderer,
25
25
  createRayTracingRenderPlan: () => createRayTracingRenderPlan,
26
26
  createRendererDebugHooks: () => createRendererDebugHooks,
27
+ createWavefrontAdaptiveSamplingLevels: () => createWavefrontAdaptiveSamplingLevels,
27
28
  createWavefrontBvhBuildLevels: () => createWavefrontBvhBuildLevels,
28
29
  createWavefrontBvhSortStages: () => createWavefrontBvhSortStages,
29
30
  createWavefrontEmissiveTriangleIndexSource: () => createWavefrontEmissiveTriangleIndexSource,
31
+ createWavefrontGpuMaterialSource: () => createWavefrontGpuMaterialSource,
30
32
  createWavefrontGpuMeshSource: () => createWavefrontGpuMeshSource,
31
33
  createWavefrontMeshAcceleration: () => createWavefrontMeshAcceleration,
32
34
  createWavefrontPathTracingComputeConfig: () => createWavefrontPathTracingComputeConfig,
@@ -70,12 +72,147 @@ __export(index_exports, {
70
72
  });
71
73
  module.exports = __toCommonJS(index_exports);
72
74
 
75
+ // src/wavefront-frame-runtime.js
76
+ function createGpuParallelismCounters() {
77
+ return {
78
+ directDispatches: 0,
79
+ directWorkgroups: 0,
80
+ directShaderInvocations: 0,
81
+ multiWorkgroupDispatches: 0,
82
+ largestDirectWorkgroupsPerDispatch: 0,
83
+ indirectDispatches: 0,
84
+ estimatedIndirectWorkgroupsUpperBound: 0,
85
+ estimatedIndirectShaderInvocationsUpperBound: 0,
86
+ indirectDispatchesWithMultiWorkgroupCapacity: 0,
87
+ largestEstimatedIndirectWorkgroupsPerDispatch: 0
88
+ };
89
+ }
90
+ function countDispatchWorkgroups(groups) {
91
+ return groups.reduce((product, value) => {
92
+ const numeric = Number(value ?? 1);
93
+ const count = Number.isFinite(numeric) ? Math.max(1, Math.trunc(numeric)) : 1;
94
+ return product * count;
95
+ }, 1);
96
+ }
97
+ function recordDirectDispatch(parallelism, groups, invocationsPerWorkgroup = 1) {
98
+ const workgroups = countDispatchWorkgroups(groups);
99
+ parallelism.directDispatches += 1;
100
+ parallelism.directWorkgroups += workgroups;
101
+ parallelism.directShaderInvocations += workgroups * invocationsPerWorkgroup;
102
+ parallelism.largestDirectWorkgroupsPerDispatch = Math.max(
103
+ parallelism.largestDirectWorkgroupsPerDispatch,
104
+ workgroups
105
+ );
106
+ if (workgroups > 1) {
107
+ parallelism.multiWorkgroupDispatches += 1;
108
+ }
109
+ }
110
+ function recordIndirectDispatch(parallelism, estimatedWorkgroupsUpperBound, invocationsPerWorkgroup = 1) {
111
+ const workgroups = Math.max(1, Math.trunc(Number(estimatedWorkgroupsUpperBound) || 1));
112
+ parallelism.indirectDispatches += 1;
113
+ parallelism.estimatedIndirectWorkgroupsUpperBound += workgroups;
114
+ parallelism.estimatedIndirectShaderInvocationsUpperBound += workgroups * invocationsPerWorkgroup;
115
+ parallelism.largestEstimatedIndirectWorkgroupsPerDispatch = Math.max(
116
+ parallelism.largestEstimatedIndirectWorkgroupsPerDispatch,
117
+ workgroups
118
+ );
119
+ if (workgroups > 1) {
120
+ parallelism.indirectDispatchesWithMultiWorkgroupCapacity += 1;
121
+ }
122
+ }
123
+ function createGpuParallelismDiagnostics(adapterDiagnostics, counters) {
124
+ const totalEstimatedWorkgroupsUpperBound = counters.directWorkgroups + counters.estimatedIndirectWorkgroupsUpperBound;
125
+ const totalEstimatedShaderInvocationsUpperBound = counters.directShaderInvocations + counters.estimatedIndirectShaderInvocationsUpperBound;
126
+ const exposesMultiWorkgroupParallelism = counters.multiWorkgroupDispatches > 0 || counters.indirectDispatchesWithMultiWorkgroupCapacity > 0;
127
+ return Object.freeze({
128
+ ...adapterDiagnostics,
129
+ directDispatches: counters.directDispatches,
130
+ directWorkgroups: counters.directWorkgroups,
131
+ directShaderInvocations: counters.directShaderInvocations,
132
+ multiWorkgroupDispatches: counters.multiWorkgroupDispatches,
133
+ largestDirectWorkgroupsPerDispatch: counters.largestDirectWorkgroupsPerDispatch,
134
+ indirectDispatches: counters.indirectDispatches,
135
+ estimatedIndirectWorkgroupsUpperBound: counters.estimatedIndirectWorkgroupsUpperBound,
136
+ estimatedIndirectShaderInvocationsUpperBound: counters.estimatedIndirectShaderInvocationsUpperBound,
137
+ indirectDispatchesWithMultiWorkgroupCapacity: counters.indirectDispatchesWithMultiWorkgroupCapacity,
138
+ largestEstimatedIndirectWorkgroupsPerDispatch: counters.largestEstimatedIndirectWorkgroupsPerDispatch,
139
+ totalEstimatedWorkgroupsUpperBound,
140
+ totalEstimatedShaderInvocationsUpperBound,
141
+ exposesMultiWorkgroupParallelism,
142
+ likelyUsesMoreThanOnePhysicalGpuCore: null,
143
+ coreUtilizationStatus: "not-exposed-by-webgpu"
144
+ });
145
+ }
146
+ function createGpuWorkerJobDiagnostics(parallelism, commandSubmissions, frameTimeMs, awaitedGpuCompletion) {
147
+ const directDispatchesCompleted = Math.max(0, Number(parallelism?.directDispatches ?? 0));
148
+ const indirectDispatchesCompleted = Math.max(
149
+ 0,
150
+ Number(parallelism?.indirectDispatches ?? 0)
151
+ );
152
+ const completedPerFrame = directDispatchesCompleted + indirectDispatchesCompleted;
153
+ const completedPerSubmission = commandSubmissions > 0 ? completedPerFrame / commandSubmissions : completedPerFrame;
154
+ const completedPerSecond = awaitedGpuCompletion && frameTimeMs > 0 ? completedPerFrame * 1e3 / frameTimeMs : null;
155
+ return Object.freeze({
156
+ completedPerFrame,
157
+ completedPerSecond,
158
+ completedPerSubmission,
159
+ directDispatchesCompleted,
160
+ indirectDispatchesCompleted,
161
+ frameTimeMs,
162
+ awaitedGpuCompletion
163
+ });
164
+ }
165
+ function createGpuSubmissionBatcher({
166
+ device,
167
+ frameIndex,
168
+ maxFramePassesPerSubmission,
169
+ startingSubmissionCount = 0,
170
+ labelPrefix = "plasius.wavefront.frame"
171
+ }) {
172
+ let encodedFramePasses = 0;
173
+ let submissionCount = 0;
174
+ let encoder = createCommandEncoder();
175
+ function createCommandEncoder() {
176
+ return device.createCommandEncoder({
177
+ label: `${labelPrefix}.${frameIndex}.batched.${startingSubmissionCount + submissionCount + 1}`
178
+ });
179
+ }
180
+ function submitCurrentEncoder() {
181
+ if (encodedFramePasses <= 0) {
182
+ return false;
183
+ }
184
+ device.queue.submit([encoder.finish()]);
185
+ submissionCount += 1;
186
+ encodedFramePasses = 0;
187
+ encoder = createCommandEncoder();
188
+ return true;
189
+ }
190
+ return Object.freeze({
191
+ reserve(passCount = 1) {
192
+ if (encodedFramePasses > 0 && encodedFramePasses + passCount > maxFramePassesPerSubmission) {
193
+ submitCurrentEncoder();
194
+ }
195
+ encodedFramePasses += passCount;
196
+ return encoder;
197
+ },
198
+ flush() {
199
+ submitCurrentEncoder();
200
+ return submissionCount;
201
+ },
202
+ getSubmissionCount() {
203
+ return submissionCount;
204
+ }
205
+ });
206
+ }
207
+
73
208
  // src/wavefront-compute.js
74
209
  var DEFAULT_WIDTH = 1280;
75
210
  var DEFAULT_HEIGHT = 720;
76
211
  var DEFAULT_MAX_DEPTH = 6;
77
212
  var DEFAULT_TILE_SIZE = 128;
78
213
  var DEFAULT_SAMPLES_PER_PIXEL = 1;
214
+ var MAX_SAMPLES_PER_PIXEL = 256;
215
+ var DEFAULT_BRDF_LUT_SIZE = 256;
79
216
  var DEFAULT_MAX_FRAME_PASSES_PER_SUBMISSION = 256;
80
217
  var DEFAULT_SCENE_OBJECT_CAPACITY = 128;
81
218
  var DEFAULT_ENVIRONMENT_PORTAL_CAPACITY = 32;
@@ -84,22 +221,27 @@ var rendererWavefrontComputeMode = "webgpu-compute";
84
221
  var rendererWavefrontComputeWorkgroupSize = WORKGROUP_SIZE;
85
222
  var rendererWavefrontComputeStatsStride = 8;
86
223
  var RAY_RECORD_BYTES = 80;
87
- var HIT_RECORD_BYTES = 208;
88
- var SCENE_OBJECT_RECORD_BYTES = 96;
224
+ var HIT_RECORD_BYTES = 256;
225
+ var SCENE_OBJECT_RECORD_BYTES = 144;
89
226
  var MESH_VERTEX_RECORD_BYTES = 48;
90
- var MESH_RANGE_RECORD_BYTES = 96;
91
- var TRIANGLE_RECORD_BYTES = 208;
227
+ var MESH_RANGE_RECORD_BYTES = 240;
228
+ var TRIANGLE_RECORD_BYTES = 352;
229
+ var GPU_MATERIAL_RECORD_BYTES = 192;
92
230
  var BVH_NODE_RECORD_BYTES = 48;
93
231
  var BVH_LEAF_REF_RECORD_BYTES = 16;
94
232
  var EMISSIVE_TRIANGLE_INDEX_BYTES = 4;
95
233
  var ENVIRONMENT_PORTAL_RECORD_BYTES = 96;
96
234
  var ACCUMULATION_RECORD_BYTES = 16;
97
235
  var PATH_VERTEX_RECORD_BYTES = 16;
98
- var CONFIG_BUFFER_BYTES = 304;
236
+ var GPU_SUBMITTED_WORK_TIMEOUT_MS = 5e3;
237
+ var GPU_READBACK_COMPLETION_TIMEOUT_MS = 6e4;
238
+ var GPU_MAX_SUBMITTED_WORK_TIMEOUT_MS = 6e4;
239
+ var CONFIG_BUFFER_BYTES = 320;
99
240
  var COUNTER_DISPATCH_ARGS_OFFSET = 16;
100
241
  var INDIRECT_DISPATCH_ARGS_BYTES = 12;
101
242
  var COUNTER_BUFFER_BYTES = 32;
102
243
  var TRACE_STORAGE_BUFFER_BINDINGS = 10;
244
+ var BRDF_LUT_UPLOAD_CACHE = /* @__PURE__ */ new Map();
103
245
  var MATERIAL_DIFFUSE = 0;
104
246
  var MATERIAL_METAL = 1;
105
247
  var MATERIAL_DIELECTRIC = 2;
@@ -134,6 +276,7 @@ var wavefrontPathTracingComputeLimits = Object.freeze({
134
276
  meshVertexRecordBytes: MESH_VERTEX_RECORD_BYTES,
135
277
  meshRangeRecordBytes: MESH_RANGE_RECORD_BYTES,
136
278
  triangleRecordBytes: TRIANGLE_RECORD_BYTES,
279
+ materialRecordBytes: GPU_MATERIAL_RECORD_BYTES,
137
280
  bvhNodeRecordBytes: BVH_NODE_RECORD_BYTES,
138
281
  bvhLeafReferenceRecordBytes: BVH_LEAF_REF_RECORD_BYTES,
139
282
  emissiveTriangleIndexBytes: EMISSIVE_TRIANGLE_INDEX_BYTES,
@@ -217,6 +360,32 @@ function asColor(value, fallback = [1, 1, 1, 1]) {
217
360
  clamp(readFiniteNumber("color[3]", value[3], fallback[3] ?? 1), 0, 1)
218
361
  ];
219
362
  }
363
+ function deriveLegacySheenColor(baseColor, sheen, sheenTint) {
364
+ const sheenStrength = clamp(Number(sheen) || 0, 0, 1);
365
+ if (sheenStrength <= 0) {
366
+ return [0, 0, 0, 1];
367
+ }
368
+ const tint = clamp(Number(sheenTint) || 0, 0, 1);
369
+ const base = asColor(baseColor, [1, 1, 1, 1]);
370
+ return [
371
+ clamp((1 - tint) * sheenStrength + base[0] * tint * sheenStrength, 0, 1),
372
+ clamp((1 - tint) * sheenStrength + base[1] * tint * sheenStrength, 0, 1),
373
+ clamp((1 - tint) * sheenStrength + base[2] * tint * sheenStrength, 0, 1),
374
+ 1
375
+ ];
376
+ }
377
+ function resolveSheenColor(input, fallbackBaseColor) {
378
+ if (input?.sheenColor || input?.material?.sheenColor) {
379
+ return asColor(input.sheenColor ?? input.material?.sheenColor, [0, 0, 0, 1]).map(
380
+ (value, index) => index < 3 ? clamp(value, 0, 1) : 1
381
+ );
382
+ }
383
+ return deriveLegacySheenColor(
384
+ fallbackBaseColor,
385
+ input?.sheen ?? input?.material?.sheen,
386
+ input?.sheenTint ?? input?.material?.sheenTint
387
+ );
388
+ }
220
389
  function resolveEnvironmentMap(input = null) {
221
390
  const source = input && typeof input === "object" ? input : null;
222
391
  const hasTexture = Boolean(source?.view || source?.texture || source?.data);
@@ -226,6 +395,11 @@ function resolveEnvironmentMap(input = null) {
226
395
  enabled: hasTexture && source?.enabled !== false,
227
396
  width,
228
397
  height,
398
+ mipLevelCount: readPositiveInteger(
399
+ "environmentMap.mipLevelCount",
400
+ source?.mipLevelCount,
401
+ 1
402
+ ),
229
403
  format: typeof source?.format === "string" ? source.format : "rgba16float",
230
404
  projection: typeof source?.projection === "string" ? source.projection : "equirectangular",
231
405
  texture: source?.texture ?? null,
@@ -237,7 +411,8 @@ function resolveEnvironmentMap(input = null) {
237
411
  ambientStrength: Math.max(
238
412
  0,
239
413
  readFiniteNumber("environmentMap.ambientStrength", source?.ambientStrength, 0.32)
240
- )
414
+ ),
415
+ hasImportanceData: source?.hasImportanceData === true
241
416
  });
242
417
  }
243
418
  function resolveDeferredPathResolve(options = {}) {
@@ -411,7 +586,8 @@ function normalizeWavefrontSceneObject(input = {}, index = 0) {
411
586
  input.halfExtent ?? input.halfExtents ?? input.extents ?? bounds?.halfExtent,
412
587
  [0.5, 0.5, 0.5]
413
588
  ).map((value) => Math.max(value, 1e-3));
414
- const materialKind = readMaterialKind(input.materialKind ?? input.material?.kind);
589
+ const materialKindInput = input.materialKind ?? input.material?.kind;
590
+ const materialKind = readMaterialKind(materialKindInput);
415
591
  const color = asColor(
416
592
  input.color ?? input.baseColor ?? input.albedo ?? input.material?.color ?? input.material?.baseColor,
417
593
  [0.72, 0.72, 0.68, 1]
@@ -420,10 +596,22 @@ function normalizeWavefrontSceneObject(input = {}, index = 0) {
420
596
  input.emission ?? input.emissive ?? input.material?.emission ?? input.material?.emissive,
421
597
  [0, 0, 0, 1]
422
598
  );
599
+ const opacity = clamp(readFiniteNumber("opacity", input.opacity ?? input.material?.opacity, color[3] ?? 1), 0, 1);
600
+ const transmission = clamp(
601
+ readFiniteNumber("transmission", input.transmission ?? input.material?.transmission, 0),
602
+ 0,
603
+ 1
604
+ );
605
+ const sheenColor = resolveSheenColor(input, color);
606
+ const specularColor = asColor(
607
+ input.specularColor ?? input.material?.specularColor,
608
+ [1, 1, 1, 1]
609
+ ).map((value, componentIndex) => componentIndex < 3 ? clamp(value, 0, 1) : 1);
610
+ 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;
423
611
  return Object.freeze({
424
612
  id: readNonNegativeInteger("id", input.id, index + 1),
425
613
  kind,
426
- materialKind: emission[0] > 0 || emission[1] > 0 || emission[2] > 0 ? MATERIAL_EMISSIVE : materialKind,
614
+ materialKind: resolvedMaterialKind,
427
615
  flags: readNonNegativeInteger("flags", input.flags, 0),
428
616
  center: Object.freeze(center),
429
617
  halfExtent: Object.freeze(halfExtent),
@@ -431,8 +619,24 @@ function normalizeWavefrontSceneObject(input = {}, index = 0) {
431
619
  emission: Object.freeze(emission),
432
620
  roughness: clamp(readFiniteNumber("roughness", input.roughness ?? input.material?.roughness, 0.72), 0, 1),
433
621
  metallic: clamp(readFiniteNumber("metallic", input.metallic ?? input.material?.metallic, 0), 0, 1),
434
- opacity: clamp(readFiniteNumber("opacity", input.opacity ?? input.material?.opacity, color[3] ?? 1), 0, 1),
435
- ior: clamp(readFiniteNumber("ior", input.ior ?? input.material?.ior, 1.45), 1, 3)
622
+ opacity,
623
+ ior: clamp(readFiniteNumber("ior", input.ior ?? input.material?.ior, 1.45), 1, 3),
624
+ sheen: clamp(readFiniteNumber("sheen", input.sheen ?? input.material?.sheen, 0), 0, 1),
625
+ sheenTint: clamp(readFiniteNumber("sheenTint", input.sheenTint ?? input.material?.sheenTint, 0), 0, 1),
626
+ sheenColor: Object.freeze(sheenColor),
627
+ clearcoat: clamp(readFiniteNumber("clearcoat", input.clearcoat ?? input.material?.clearcoat, 0), 0, 1),
628
+ clearcoatRoughness: clamp(
629
+ readFiniteNumber(
630
+ "clearcoatRoughness",
631
+ input.clearcoatRoughness ?? input.material?.clearcoatRoughness,
632
+ 0.08
633
+ ),
634
+ 0,
635
+ 1
636
+ ),
637
+ specular: clamp(readFiniteNumber("specular", input.specular ?? input.material?.specular, 1), 0, 1),
638
+ specularColor: Object.freeze(specularColor),
639
+ transmission
436
640
  });
437
641
  }
438
642
  function createDefaultWavefrontSceneObjects() {
@@ -504,7 +708,8 @@ function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
504
708
  input.uvs ?? input.texcoords ?? input.uv,
505
709
  (value) => readFiniteNumber("mesh uv", value, 0)
506
710
  ) : null;
507
- const materialKind = readMaterialKind(input.materialKind ?? input.material?.kind);
711
+ const materialKindInput = input.materialKind ?? input.material?.kind;
712
+ const materialKind = readMaterialKind(materialKindInput);
508
713
  const color = asColor(
509
714
  input.color ?? input.baseColor ?? input.albedo ?? input.material?.color ?? input.material?.baseColor,
510
715
  [0.72, 0.72, 0.68, 1]
@@ -513,13 +718,25 @@ function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
513
718
  input.emission ?? input.emissive ?? input.material?.emission ?? input.material?.emissive,
514
719
  [0, 0, 0, 1]
515
720
  );
721
+ const opacity = clamp(readFiniteNumber("opacity", input.opacity ?? input.material?.opacity, color[3] ?? 1), 0, 1);
722
+ const transmission = clamp(
723
+ readFiniteNumber("transmission", input.transmission ?? input.material?.transmission, 0),
724
+ 0,
725
+ 1
726
+ );
727
+ const sheenColor = resolveSheenColor(input, color);
728
+ const specularColor = asColor(
729
+ input.specularColor ?? input.material?.specularColor,
730
+ [1, 1, 1, 1]
731
+ ).map((value, componentIndex) => componentIndex < 3 ? clamp(value, 0, 1) : 1);
732
+ 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;
516
733
  return Object.freeze({
517
734
  id: readNonNegativeInteger("mesh id", input.id, meshIndex + 1),
518
735
  positions: Object.freeze(Array.from(positions, (value) => readFiniteNumber("mesh position", value, 0))),
519
736
  indices: Object.freeze(indices),
520
737
  normals: normals ? Object.freeze(normals) : null,
521
738
  uvs: uvs ? Object.freeze(uvs) : null,
522
- materialKind: emission[0] > 0 || emission[1] > 0 || emission[2] > 0 ? MATERIAL_EMISSIVE : materialKind,
739
+ materialKind: resolvedMaterialKind,
523
740
  flags: readNonNegativeInteger("mesh flags", input.flags, 0),
524
741
  materialRefId: readNonNegativeInteger(
525
742
  "mesh materialRefId",
@@ -535,10 +752,167 @@ function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
535
752
  emission: Object.freeze(emission),
536
753
  roughness: clamp(readFiniteNumber("roughness", input.roughness ?? input.material?.roughness, 0.72), 0, 1),
537
754
  metallic: clamp(readFiniteNumber("metallic", input.metallic ?? input.material?.metallic, 0), 0, 1),
538
- opacity: clamp(readFiniteNumber("opacity", input.opacity ?? input.material?.opacity, color[3] ?? 1), 0, 1),
539
- ior: clamp(readFiniteNumber("ior", input.ior ?? input.material?.ior, 1.45), 1, 3)
755
+ opacity,
756
+ ior: clamp(readFiniteNumber("ior", input.ior ?? input.material?.ior, 1.45), 1, 3),
757
+ sheen: clamp(readFiniteNumber("sheen", input.sheen ?? input.material?.sheen, 0), 0, 1),
758
+ sheenTint: clamp(readFiniteNumber("sheenTint", input.sheenTint ?? input.material?.sheenTint, 0), 0, 1),
759
+ sheenColor: Object.freeze(sheenColor),
760
+ clearcoat: clamp(readFiniteNumber("clearcoat", input.clearcoat ?? input.material?.clearcoat, 0), 0, 1),
761
+ clearcoatRoughness: clamp(
762
+ readFiniteNumber(
763
+ "clearcoatRoughness",
764
+ input.clearcoatRoughness ?? input.material?.clearcoatRoughness,
765
+ 0.08
766
+ ),
767
+ 0,
768
+ 1
769
+ ),
770
+ specular: clamp(readFiniteNumber("specular", input.specular ?? input.material?.specular, 1), 0, 1),
771
+ specularColor: Object.freeze(specularColor),
772
+ transmission,
773
+ baseColorTexture: input.baseColorTexture ?? input.material?.baseColorTexture ?? null,
774
+ metallicRoughnessTexture: input.metallicRoughnessTexture ?? input.material?.metallicRoughnessTexture ?? null,
775
+ normalTexture: input.normalTexture ?? input.material?.normalTexture ?? null,
776
+ occlusionTexture: input.occlusionTexture ?? input.material?.occlusionTexture ?? null,
777
+ emissiveTexture: input.emissiveTexture ?? input.material?.emissiveTexture ?? null
540
778
  });
541
779
  }
780
+ function clampUnit(value) {
781
+ return clamp(Number(value) || 0, 0, 1);
782
+ }
783
+ function srgbToLinear(value) {
784
+ const channel = clampUnit(value);
785
+ if (channel <= 0.04045) {
786
+ return channel / 12.92;
787
+ }
788
+ return ((channel + 0.055) / 1.055) ** 2.4;
789
+ }
790
+ function sampleTextureRgba(texture, uv = [0, 0], colorSpace = "linear") {
791
+ if (!texture || !Number.isFinite(texture.width) || !Number.isFinite(texture.height) || !texture.data || texture.width <= 0 || texture.height <= 0) {
792
+ return [1, 1, 1, 1];
793
+ }
794
+ const u = (uv[0] % 1 + 1) % 1;
795
+ const v = (uv[1] % 1 + 1) % 1;
796
+ const x = Math.min(texture.width - 1, Math.max(0, Math.round(u * (texture.width - 1))));
797
+ const y = Math.min(texture.height - 1, Math.max(0, Math.round((1 - v) * (texture.height - 1))));
798
+ const offset = (y * texture.width + x) * 4;
799
+ const data = texture.data;
800
+ const scale2 = resolveTextureSampleScale(data);
801
+ const defaultChannel = scale2 === 1 ? 1 : Math.round(1 / scale2);
802
+ const color = [
803
+ (data[offset] ?? defaultChannel) * scale2,
804
+ (data[offset + 1] ?? defaultChannel) * scale2,
805
+ (data[offset + 2] ?? defaultChannel) * scale2,
806
+ (data[offset + 3] ?? defaultChannel) * scale2
807
+ ];
808
+ if (colorSpace === "srgb") {
809
+ return [srgbToLinear(color[0]), srgbToLinear(color[1]), srgbToLinear(color[2]), color[3]];
810
+ }
811
+ return color;
812
+ }
813
+ function resolveTextureSampleScale(data) {
814
+ if (data instanceof Uint8Array || data instanceof Uint8ClampedArray) {
815
+ return 1 / 255;
816
+ }
817
+ if (data instanceof Uint16Array) {
818
+ return 1 / 65535;
819
+ }
820
+ if (Array.isArray(data) && data.some((value) => Number(value) > 1)) {
821
+ return 1 / 255;
822
+ }
823
+ return 1;
824
+ }
825
+ function normalizeVectorOrFallback(vector, fallback) {
826
+ return normalize(Array.isArray(vector) ? vector : fallback, fallback);
827
+ }
828
+ function buildTriangleTangentBasis(v0, v1, v2, uv0, uv1, uv2, fallbackNormal) {
829
+ const edge1 = subtract(v1, v0);
830
+ const edge2 = subtract(v2, v0);
831
+ const deltaUv1 = [uv1[0] - uv0[0], uv1[1] - uv0[1]];
832
+ const deltaUv2 = [uv2[0] - uv0[0], uv2[1] - uv0[1]];
833
+ const determinant = deltaUv1[0] * deltaUv2[1] - deltaUv1[1] * deltaUv2[0];
834
+ if (Math.abs(determinant) < 1e-6) {
835
+ const tangentFallback = Math.abs(fallbackNormal[1]) < 0.999 ? [0, 1, 0] : [1, 0, 0];
836
+ const tangent2 = normalize(cross(tangentFallback, fallbackNormal), [1, 0, 0]);
837
+ const bitangent2 = normalize(cross(fallbackNormal, tangent2), [0, 0, 1]);
838
+ return { tangent: tangent2, bitangent: bitangent2 };
839
+ }
840
+ const inverse = 1 / determinant;
841
+ const tangent = normalize(
842
+ [
843
+ inverse * (edge1[0] * deltaUv2[1] - edge2[0] * deltaUv1[1]),
844
+ inverse * (edge1[1] * deltaUv2[1] - edge2[1] * deltaUv1[1]),
845
+ inverse * (edge1[2] * deltaUv2[1] - edge2[2] * deltaUv1[1])
846
+ ],
847
+ [1, 0, 0]
848
+ );
849
+ const bitangent = normalize(
850
+ [
851
+ inverse * (-edge1[0] * deltaUv2[0] + edge2[0] * deltaUv1[0]),
852
+ inverse * (-edge1[1] * deltaUv2[0] + edge2[1] * deltaUv1[0]),
853
+ inverse * (-edge1[2] * deltaUv2[0] + edge2[2] * deltaUv1[0])
854
+ ],
855
+ [0, 0, 1]
856
+ );
857
+ return { tangent, bitangent };
858
+ }
859
+ function applyNormalMap(normal, tangent, bitangent, normalTexture, uv) {
860
+ if (!normalTexture) {
861
+ return normalizeVectorOrFallback(normal, [0, 1, 0]);
862
+ }
863
+ const sample = sampleTextureRgba(normalTexture, uv, "linear");
864
+ const strength = clampUnit(normalTexture.scale ?? 1);
865
+ const tangentNormal = normalize(
866
+ [
867
+ (sample[0] * 2 - 1) * strength,
868
+ (sample[1] * 2 - 1) * strength,
869
+ 1 + (sample[2] * 2 - 1 - 1) * strength
870
+ ],
871
+ [0, 0, 1]
872
+ );
873
+ return normalize(
874
+ [
875
+ tangent[0] * tangentNormal[0] + bitangent[0] * tangentNormal[1] + normal[0] * tangentNormal[2],
876
+ tangent[1] * tangentNormal[0] + bitangent[1] * tangentNormal[1] + normal[1] * tangentNormal[2],
877
+ tangent[2] * tangentNormal[0] + bitangent[2] * tangentNormal[1] + normal[2] * tangentNormal[2]
878
+ ],
879
+ normal
880
+ );
881
+ }
882
+ function sampleBaseColor(mesh, uv) {
883
+ const sample = mesh.baseColorTexture ? sampleTextureRgba(mesh.baseColorTexture, uv, "srgb") : [1, 1, 1, 1];
884
+ return [
885
+ clampUnit(mesh.color[0] * sample[0]),
886
+ clampUnit(mesh.color[1] * sample[1]),
887
+ clampUnit(mesh.color[2] * sample[2]),
888
+ clampUnit((mesh.color[3] ?? 1) * sample[3])
889
+ ];
890
+ }
891
+ function sampleSurfaceMaterial(mesh, uv) {
892
+ const textureSample = mesh.metallicRoughnessTexture ? sampleTextureRgba(mesh.metallicRoughnessTexture, uv, "linear") : [1, 1, 1, 1];
893
+ return {
894
+ roughness: clamp(mesh.roughness * textureSample[1], 0, 1),
895
+ metallic: clamp(mesh.metallic * textureSample[2], 0, 1)
896
+ };
897
+ }
898
+ function averageColors(colors) {
899
+ const count = Math.max(colors.length, 1);
900
+ return colors.reduce(
901
+ (accumulator, color) => [
902
+ accumulator[0] + color[0] / count,
903
+ accumulator[1] + color[1] / count,
904
+ accumulator[2] + color[2] / count,
905
+ accumulator[3] + color[3] / count
906
+ ],
907
+ [0, 0, 0, 0]
908
+ );
909
+ }
910
+ function averageNumbers(values, fallback = 0) {
911
+ if (!Array.isArray(values) || values.length === 0) {
912
+ return fallback;
913
+ }
914
+ return values.reduce((sum, value) => sum + value, 0) / values.length;
915
+ }
542
916
  function createMeshTriangleRecords(meshes) {
543
917
  const source = Array.isArray(meshes) ? meshes : [];
544
918
  let nextTriangleId = 0;
@@ -559,6 +933,16 @@ function createMeshTriangleRecords(meshes) {
559
933
  const uv0 = mesh.uvs ? readVector2(mesh.uvs, a) : [0, 0];
560
934
  const uv1 = mesh.uvs ? readVector2(mesh.uvs, b) : [0, 0];
561
935
  const uv2 = mesh.uvs ? readVector2(mesh.uvs, c) : [0, 0];
936
+ const tangentBasis = buildTriangleTangentBasis(v0, v1, v2, uv0, uv1, uv2, faceNormal);
937
+ const shadedN0 = applyNormalMap(n0, tangentBasis.tangent, tangentBasis.bitangent, mesh.normalTexture, uv0);
938
+ const shadedN1 = applyNormalMap(n1, tangentBasis.tangent, tangentBasis.bitangent, mesh.normalTexture, uv1);
939
+ const shadedN2 = applyNormalMap(n2, tangentBasis.tangent, tangentBasis.bitangent, mesh.normalTexture, uv2);
940
+ const sampledColors = [sampleBaseColor(mesh, uv0), sampleBaseColor(mesh, uv1), sampleBaseColor(mesh, uv2)];
941
+ const sampledMaterials = [
942
+ sampleSurfaceMaterial(mesh, uv0),
943
+ sampleSurfaceMaterial(mesh, uv1),
944
+ sampleSurfaceMaterial(mesh, uv2)
945
+ ];
562
946
  const bounds = triangleBounds(v0, v1, v2);
563
947
  triangles.push(
564
948
  Object.freeze({
@@ -568,18 +952,42 @@ function createMeshTriangleRecords(meshes) {
568
952
  flags: mesh.flags,
569
953
  materialRefId: mesh.materialRefId,
570
954
  mediumRefId: mesh.mediumRefId,
955
+ materialSlot: meshIndex,
571
956
  v0: Object.freeze(v0),
572
957
  v1: Object.freeze(v1),
573
958
  v2: Object.freeze(v2),
574
- n0: Object.freeze(n0),
575
- n1: Object.freeze(n1),
576
- n2: Object.freeze(n2),
959
+ n0: Object.freeze(shadedN0),
960
+ n1: Object.freeze(shadedN1),
961
+ n2: Object.freeze(shadedN2),
577
962
  uv0: Object.freeze(uv0),
578
963
  uv1: Object.freeze(uv1),
579
964
  uv2: Object.freeze(uv2),
580
- color: mesh.color,
965
+ color: Object.freeze(averageColors(sampledColors)),
581
966
  emission: mesh.emission,
582
- material: Object.freeze([mesh.roughness, mesh.metallic, mesh.opacity, mesh.ior]),
967
+ material: Object.freeze([
968
+ averageNumbers(sampledMaterials.map((sample) => sample.roughness), mesh.roughness),
969
+ averageNumbers(sampledMaterials.map((sample) => sample.metallic), mesh.metallic),
970
+ mesh.opacity,
971
+ mesh.ior
972
+ ]),
973
+ materialResponse: Object.freeze([
974
+ mesh.sheenColor[0] ?? 0,
975
+ mesh.sheenColor[1] ?? 0,
976
+ mesh.sheenColor[2] ?? 0,
977
+ mesh.clearcoat
978
+ ]),
979
+ materialExtension: Object.freeze([
980
+ mesh.clearcoatRoughness,
981
+ mesh.specular,
982
+ mesh.transmission,
983
+ 0
984
+ ]),
985
+ specularColor: Object.freeze([
986
+ mesh.specularColor[0] ?? 1,
987
+ mesh.specularColor[1] ?? 1,
988
+ mesh.specularColor[2] ?? 1,
989
+ 1
990
+ ]),
583
991
  bounds: Object.freeze({
584
992
  min: Object.freeze(bounds.min),
585
993
  max: Object.freeze(bounds.max)
@@ -688,6 +1096,220 @@ function nextPowerOfTwo(value) {
688
1096
  }
689
1097
  return 2 ** Math.ceil(Math.log2(value));
690
1098
  }
1099
+ function textureComponentToByte(value, fallback) {
1100
+ const numeric = Number(value);
1101
+ if (!Number.isFinite(numeric)) {
1102
+ return fallback;
1103
+ }
1104
+ if (numeric >= 0 && numeric <= 1) {
1105
+ return Math.max(0, Math.min(255, Math.round(numeric * 255)));
1106
+ }
1107
+ return Math.max(0, Math.min(255, Math.round(numeric)));
1108
+ }
1109
+ function createSolidTextureSample(width, height, rgba) {
1110
+ const data = new Uint8Array(width * height * 4);
1111
+ for (let offset = 0; offset < data.length; offset += 4) {
1112
+ data[offset] = rgba[0];
1113
+ data[offset + 1] = rgba[1];
1114
+ data[offset + 2] = rgba[2];
1115
+ data[offset + 3] = rgba[3];
1116
+ }
1117
+ return Object.freeze({
1118
+ width,
1119
+ height,
1120
+ data
1121
+ });
1122
+ }
1123
+ function normalizeTextureSampleInput(texture, fallbackColor) {
1124
+ if (!texture || !Number.isFinite(texture.width) || !Number.isFinite(texture.height) || texture.width <= 0 || texture.height <= 0) {
1125
+ return createSolidTextureSample(1, 1, fallbackColor);
1126
+ }
1127
+ const pixelCount = Math.trunc(texture.width) * Math.trunc(texture.height) * 4;
1128
+ const source = ArrayBuffer.isView(texture.data) || Array.isArray(texture.data) ? texture.data : null;
1129
+ if (!source || source.length < pixelCount) {
1130
+ return createSolidTextureSample(1, 1, fallbackColor);
1131
+ }
1132
+ const data = new Uint8Array(pixelCount);
1133
+ for (let index = 0; index < pixelCount; index += 1) {
1134
+ data[index] = textureComponentToByte(source[index], fallbackColor[index % 4]);
1135
+ }
1136
+ return Object.freeze({
1137
+ width: Math.trunc(texture.width),
1138
+ height: Math.trunc(texture.height),
1139
+ data
1140
+ });
1141
+ }
1142
+ function buildTextureAtlas(textures, fallbackColor) {
1143
+ const padding = 1;
1144
+ const defaultTexture = createSolidTextureSample(1, 1, fallbackColor);
1145
+ const uniqueEntries = [{ source: null, texture: defaultTexture }];
1146
+ const bySource = /* @__PURE__ */ new Map();
1147
+ for (const texture of Array.isArray(textures) ? textures : []) {
1148
+ if (!texture || bySource.has(texture)) {
1149
+ continue;
1150
+ }
1151
+ const normalized = normalizeTextureSampleInput(texture, fallbackColor);
1152
+ bySource.set(texture, uniqueEntries.length);
1153
+ uniqueEntries.push({ source: texture, texture: normalized });
1154
+ }
1155
+ const totalArea = uniqueEntries.reduce((sum, entry) => {
1156
+ return sum + (entry.texture.width + padding * 2) * (entry.texture.height + padding * 2);
1157
+ }, 0);
1158
+ const maxTileWidth = uniqueEntries.reduce((maxWidth, entry) => {
1159
+ return Math.max(maxWidth, entry.texture.width + padding * 2);
1160
+ }, 1);
1161
+ const targetWidth = Math.max(
1162
+ maxTileWidth,
1163
+ nextPowerOfTwo(Math.max(maxTileWidth, Math.ceil(Math.sqrt(totalArea))))
1164
+ );
1165
+ let cursorX = 0;
1166
+ let cursorY = 0;
1167
+ let rowHeight = 0;
1168
+ let atlasWidth = 0;
1169
+ const placements = uniqueEntries.map((entry) => {
1170
+ const tileWidth = entry.texture.width + padding * 2;
1171
+ const tileHeight = entry.texture.height + padding * 2;
1172
+ if (cursorX > 0 && cursorX + tileWidth > targetWidth) {
1173
+ cursorX = 0;
1174
+ cursorY += rowHeight;
1175
+ rowHeight = 0;
1176
+ }
1177
+ const placement = Object.freeze({
1178
+ x: cursorX,
1179
+ y: cursorY,
1180
+ tileWidth,
1181
+ tileHeight,
1182
+ width: entry.texture.width,
1183
+ height: entry.texture.height
1184
+ });
1185
+ cursorX += tileWidth;
1186
+ atlasWidth = Math.max(atlasWidth, cursorX);
1187
+ rowHeight = Math.max(rowHeight, tileHeight);
1188
+ return placement;
1189
+ });
1190
+ const atlasHeight = Math.max(1, cursorY + rowHeight);
1191
+ const atlasData = new Uint8Array(Math.max(1, atlasWidth * atlasHeight * 4));
1192
+ const writePixel = (x, y, rgba) => {
1193
+ const offset = (y * atlasWidth + x) * 4;
1194
+ atlasData[offset] = rgba[0];
1195
+ atlasData[offset + 1] = rgba[1];
1196
+ atlasData[offset + 2] = rgba[2];
1197
+ atlasData[offset + 3] = rgba[3];
1198
+ };
1199
+ const rects = placements.map((placement, entryIndex) => {
1200
+ const { texture } = uniqueEntries[entryIndex];
1201
+ for (let y = 0; y < placement.tileHeight; y += 1) {
1202
+ for (let x = 0; x < placement.tileWidth; x += 1) {
1203
+ const sampleX = Math.max(0, Math.min(texture.width - 1, x - padding));
1204
+ const sampleY = Math.max(0, Math.min(texture.height - 1, y - padding));
1205
+ const sourceOffset = (sampleY * texture.width + sampleX) * 4;
1206
+ writePixel(placement.x + x, placement.y + y, texture.data.slice(sourceOffset, sourceOffset + 4));
1207
+ }
1208
+ }
1209
+ return Object.freeze([
1210
+ (placement.x + padding) / Math.max(1, atlasWidth),
1211
+ (placement.y + padding) / Math.max(1, atlasHeight),
1212
+ placement.width / Math.max(1, atlasWidth),
1213
+ placement.height / Math.max(1, atlasHeight)
1214
+ ]);
1215
+ });
1216
+ const rectBySource = /* @__PURE__ */ new Map();
1217
+ uniqueEntries.forEach((entry, index) => {
1218
+ if (entry.source) {
1219
+ rectBySource.set(entry.source, rects[index]);
1220
+ }
1221
+ });
1222
+ return Object.freeze({
1223
+ width: Math.max(1, atlasWidth),
1224
+ height: Math.max(1, atlasHeight),
1225
+ data: atlasData,
1226
+ defaultRect: rects[0],
1227
+ resolveRect(texture) {
1228
+ return rectBySource.get(texture) ?? rects[0];
1229
+ }
1230
+ });
1231
+ }
1232
+ function createWavefrontGpuMaterialSource(meshes = []) {
1233
+ const source = Array.isArray(meshes) ? meshes : [meshes];
1234
+ const normalized = source.map((meshInput, meshIndex) => normalizeWavefrontMesh(meshInput, meshIndex));
1235
+ const baseColorAtlas = buildTextureAtlas(
1236
+ normalized.map((mesh) => mesh.baseColorTexture),
1237
+ [255, 255, 255, 255]
1238
+ );
1239
+ const metallicRoughnessAtlas = buildTextureAtlas(
1240
+ normalized.map((mesh) => mesh.metallicRoughnessTexture),
1241
+ [255, 255, 255, 255]
1242
+ );
1243
+ const normalAtlas = buildTextureAtlas(
1244
+ normalized.map((mesh) => mesh.normalTexture),
1245
+ [128, 128, 255, 255]
1246
+ );
1247
+ const occlusionAtlas = buildTextureAtlas(
1248
+ normalized.map((mesh) => mesh.occlusionTexture),
1249
+ [255, 255, 255, 255]
1250
+ );
1251
+ const emissiveAtlas = buildTextureAtlas(
1252
+ normalized.map((mesh) => mesh.emissiveTexture),
1253
+ [255, 255, 255, 255]
1254
+ );
1255
+ const bytes = new ArrayBuffer(Math.max(1, normalized.length) * GPU_MATERIAL_RECORD_BYTES);
1256
+ const floatView = new Float32Array(bytes);
1257
+ normalized.forEach((mesh, meshIndex) => {
1258
+ const byteOffset = meshIndex * GPU_MATERIAL_RECORD_BYTES;
1259
+ writeVec4(floatView, byteOffset, mesh.color);
1260
+ writeVec4(floatView, byteOffset + 16, mesh.emission);
1261
+ writeVec4(floatView, byteOffset + 32, [
1262
+ mesh.roughness,
1263
+ mesh.metallic,
1264
+ mesh.opacity,
1265
+ mesh.ior
1266
+ ]);
1267
+ writeVec4(floatView, byteOffset + 48, [
1268
+ mesh.sheenColor[0] ?? 0,
1269
+ mesh.sheenColor[1] ?? 0,
1270
+ mesh.sheenColor[2] ?? 0,
1271
+ mesh.clearcoat
1272
+ ]);
1273
+ writeVec4(floatView, byteOffset + 64, [
1274
+ mesh.clearcoatRoughness,
1275
+ mesh.specular,
1276
+ mesh.transmission,
1277
+ 0
1278
+ ]);
1279
+ writeVec4(floatView, byteOffset + 80, [
1280
+ mesh.specularColor[0] ?? 1,
1281
+ mesh.specularColor[1] ?? 1,
1282
+ mesh.specularColor[2] ?? 1,
1283
+ 1
1284
+ ]);
1285
+ writeVec4(floatView, byteOffset + 96, baseColorAtlas.resolveRect(mesh.baseColorTexture));
1286
+ writeVec4(
1287
+ floatView,
1288
+ byteOffset + 112,
1289
+ metallicRoughnessAtlas.resolveRect(mesh.metallicRoughnessTexture)
1290
+ );
1291
+ writeVec4(floatView, byteOffset + 128, normalAtlas.resolveRect(mesh.normalTexture));
1292
+ writeVec4(floatView, byteOffset + 144, occlusionAtlas.resolveRect(mesh.occlusionTexture));
1293
+ writeVec4(floatView, byteOffset + 160, emissiveAtlas.resolveRect(mesh.emissiveTexture));
1294
+ writeVec4(floatView, byteOffset + 176, [
1295
+ clampUnit(mesh.normalTexture?.scale ?? mesh.normalTexture?.strength ?? 1),
1296
+ clampUnit(mesh.occlusionTexture?.strength ?? 1),
1297
+ clampUnit(mesh.emissiveTexture?.strength ?? 1),
1298
+ 0
1299
+ ]);
1300
+ });
1301
+ return Object.freeze({
1302
+ buffer: bytes,
1303
+ count: normalized.length,
1304
+ recordBytes: GPU_MATERIAL_RECORD_BYTES,
1305
+ records: Object.freeze(normalized),
1306
+ baseColorAtlas,
1307
+ metallicRoughnessAtlas,
1308
+ normalAtlas,
1309
+ occlusionAtlas,
1310
+ emissiveAtlas
1311
+ });
1312
+ }
691
1313
  function estimateBvhLeafSortCapacity(triangleCount) {
692
1314
  return triangleCount <= 0 ? 0 : nextPowerOfTwo(triangleCount);
693
1315
  }
@@ -745,9 +1367,10 @@ function resolveAccelerationBuildMode(options = {}) {
745
1367
  }
746
1368
  return mode;
747
1369
  }
748
- function createWavefrontGpuMeshSource(meshes = []) {
1370
+ function createWavefrontGpuMeshSource(meshes = [], gpuMaterialSourceInput = null) {
749
1371
  const source = Array.isArray(meshes) ? meshes : [meshes];
750
1372
  const normalized = source.map((meshInput, meshIndex) => normalizeWavefrontMesh(meshInput, meshIndex));
1373
+ const gpuMaterialSource = gpuMaterialSourceInput ?? createWavefrontGpuMaterialSource(normalized);
751
1374
  const vertexCount = normalized.reduce((count, mesh) => count + mesh.positions.length / 3, 0);
752
1375
  const indexCount = normalized.reduce((count, mesh) => count + mesh.indices.length, 0);
753
1376
  const triangleCount = Math.floor(indexCount / 3);
@@ -799,7 +1422,7 @@ function createWavefrontGpuMeshSource(meshes = []) {
799
1422
  meshUints[meshOffset + 8] = mesh.indices.length / 3;
800
1423
  meshUints[meshOffset + 9] = meshVertexBase;
801
1424
  meshUints[meshOffset + 10] = meshVertexCount;
802
- meshUints[meshOffset + 11] = 0;
1425
+ meshUints[meshOffset + 11] = meshIndex;
803
1426
  const floatOffset = meshOffset;
804
1427
  writeVec4(meshFloats, floatOffset * 4 + 48, mesh.color);
805
1428
  writeVec4(meshFloats, floatOffset * 4 + 64, mesh.emission);
@@ -809,6 +1432,55 @@ function createWavefrontGpuMeshSource(meshes = []) {
809
1432
  mesh.opacity,
810
1433
  mesh.ior
811
1434
  ]);
1435
+ writeVec4(meshFloats, floatOffset * 4 + 96, [
1436
+ mesh.sheenColor[0] ?? 0,
1437
+ mesh.sheenColor[1] ?? 0,
1438
+ mesh.sheenColor[2] ?? 0,
1439
+ mesh.clearcoat
1440
+ ]);
1441
+ writeVec4(meshFloats, floatOffset * 4 + 112, [
1442
+ mesh.clearcoatRoughness,
1443
+ mesh.specular,
1444
+ mesh.transmission,
1445
+ 0
1446
+ ]);
1447
+ writeVec4(meshFloats, floatOffset * 4 + 128, [
1448
+ mesh.specularColor[0] ?? 1,
1449
+ mesh.specularColor[1] ?? 1,
1450
+ mesh.specularColor[2] ?? 1,
1451
+ 1
1452
+ ]);
1453
+ writeVec4(
1454
+ meshFloats,
1455
+ floatOffset * 4 + 144,
1456
+ gpuMaterialSource.baseColorAtlas.resolveRect(mesh.baseColorTexture)
1457
+ );
1458
+ writeVec4(
1459
+ meshFloats,
1460
+ floatOffset * 4 + 160,
1461
+ gpuMaterialSource.metallicRoughnessAtlas.resolveRect(mesh.metallicRoughnessTexture)
1462
+ );
1463
+ writeVec4(
1464
+ meshFloats,
1465
+ floatOffset * 4 + 176,
1466
+ gpuMaterialSource.normalAtlas.resolveRect(mesh.normalTexture)
1467
+ );
1468
+ writeVec4(
1469
+ meshFloats,
1470
+ floatOffset * 4 + 192,
1471
+ gpuMaterialSource.occlusionAtlas.resolveRect(mesh.occlusionTexture)
1472
+ );
1473
+ writeVec4(
1474
+ meshFloats,
1475
+ floatOffset * 4 + 208,
1476
+ gpuMaterialSource.emissiveAtlas.resolveRect(mesh.emissiveTexture)
1477
+ );
1478
+ writeVec4(meshFloats, floatOffset * 4 + 224, [
1479
+ clampUnit(mesh.normalTexture?.scale ?? mesh.normalTexture?.strength ?? 1),
1480
+ clampUnit(mesh.occlusionTexture?.strength ?? 1),
1481
+ clampUnit(mesh.emissiveTexture?.strength ?? 1),
1482
+ 0
1483
+ ]);
812
1484
  vertexCursor += meshVertexCount;
813
1485
  indexCursor += mesh.indices.length;
814
1486
  triangleCursor += mesh.indices.length / 3;
@@ -1111,12 +1783,14 @@ function estimateWavefrontPathTracingMemory(options = {}) {
1111
1783
  options.environmentPortalCapacity,
1112
1784
  0
1113
1785
  );
1786
+ const materialCapacity = readNonNegativeInteger("materialCapacity", options.materialCapacity, 0);
1114
1787
  const queueBytes = tilePixelCapacity * RAY_RECORD_BYTES;
1115
1788
  const hitBytes = tilePixelCapacity * HIT_RECORD_BYTES;
1116
1789
  const accumulationBytes = tilePixelCapacity * ACCUMULATION_RECORD_BYTES;
1117
1790
  const pathVertexBytes = tilePixelCapacity * (maxDepth + 1) * PATH_VERTEX_RECORD_BYTES;
1118
1791
  const sceneObjectBytes = sceneObjectCapacity * SCENE_OBJECT_RECORD_BYTES;
1119
1792
  const triangleBytes = triangleCapacity * TRIANGLE_RECORD_BYTES;
1793
+ const materialTableBytes = materialCapacity * GPU_MATERIAL_RECORD_BYTES;
1120
1794
  const bvhNodeBytes = bvhNodeCapacity * BVH_NODE_RECORD_BYTES;
1121
1795
  const bvhLeafReferenceBytes = bvhLeafSortCapacity * BVH_LEAF_REF_RECORD_BYTES;
1122
1796
  const emissiveTriangleMetadataBytes = emissiveTriangleCapacity * BVH_NODE_RECORD_BYTES;
@@ -1129,6 +1803,7 @@ function estimateWavefrontPathTracingMemory(options = {}) {
1129
1803
  pathVertexBytes,
1130
1804
  sceneObjectBytes,
1131
1805
  triangleBytes,
1806
+ materialTableBytes,
1132
1807
  bvhNodeBytes,
1133
1808
  bvhLeafReferenceBytes,
1134
1809
  emissiveTriangleMetadataBytes,
@@ -1136,7 +1811,7 @@ function estimateWavefrontPathTracingMemory(options = {}) {
1136
1811
  configBytes: CONFIG_BUFFER_BYTES,
1137
1812
  counterBytes: COUNTER_BUFFER_BYTES,
1138
1813
  indirectDispatchBytes: INDIRECT_DISPATCH_ARGS_BYTES,
1139
- totalHotBufferBytes: queueBytes * 2 + hitBytes + accumulationBytes + pathVertexBytes + sceneObjectBytes + triangleBytes + bvhNodeBytes + bvhLeafReferenceBytes + emissiveTriangleMetadataBytes + environmentPortalBytes + CONFIG_BUFFER_BYTES + COUNTER_BUFFER_BYTES + INDIRECT_DISPATCH_ARGS_BYTES
1814
+ totalHotBufferBytes: queueBytes * 2 + hitBytes + accumulationBytes + pathVertexBytes + sceneObjectBytes + triangleBytes + materialTableBytes + bvhNodeBytes + bvhLeafReferenceBytes + emissiveTriangleMetadataBytes + environmentPortalBytes + CONFIG_BUFFER_BYTES + COUNTER_BUFFER_BYTES + INDIRECT_DISPATCH_ARGS_BYTES
1140
1815
  });
1141
1816
  }
1142
1817
  function createWavefrontPathTracingComputeConfig(options = {}) {
@@ -1150,7 +1825,7 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1150
1825
  const samplesPerPixel = clamp(
1151
1826
  readPositiveInteger("samplesPerPixel", options.samplesPerPixel, DEFAULT_SAMPLES_PER_PIXEL),
1152
1827
  1,
1153
- 64
1828
+ MAX_SAMPLES_PER_PIXEL
1154
1829
  );
1155
1830
  const maxFramePassesPerSubmission = clamp(
1156
1831
  readPositiveInteger(
@@ -1168,7 +1843,8 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1168
1843
  );
1169
1844
  const meshes = normalizeMeshes(options);
1170
1845
  const meshSourceShape = estimateMeshSourceShape(meshes);
1171
- const gpuMeshSource = meshes.length > 0 ? createWavefrontGpuMeshSource(meshes) : createWavefrontGpuMeshSource([]);
1846
+ const gpuMaterialSource = meshes.length > 0 ? createWavefrontGpuMaterialSource(meshes) : createWavefrontGpuMaterialSource([]);
1847
+ const gpuMeshSource = meshes.length > 0 ? createWavefrontGpuMeshSource(meshes, gpuMaterialSource) : createWavefrontGpuMeshSource([]);
1172
1848
  const meshAcceleration = accelerationBuildMode === "cpu-debug" ? createWavefrontMeshAcceleration(meshes) : Object.freeze({ nodes: Object.freeze([]), triangles: Object.freeze([]) });
1173
1849
  const emissiveTriangleIndices = createWavefrontEmissiveTriangleIndexSource(
1174
1850
  meshes,
@@ -1240,6 +1916,7 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1240
1916
  accelerationBuildMode,
1241
1917
  gpuAccelerationBuildRequired: accelerationBuildMode === "gpu" && triangleCount > 0,
1242
1918
  gpuMeshSource,
1919
+ gpuMaterialSource,
1243
1920
  meshAcceleration,
1244
1921
  emissiveTriangleIndices,
1245
1922
  emissiveTriangleCount: emissiveTriangleIndices.count,
@@ -1270,6 +1947,7 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1270
1947
  maxDepth,
1271
1948
  sceneObjectCapacity,
1272
1949
  triangleCapacity,
1950
+ materialCapacity: gpuMaterialSource.count,
1273
1951
  bvhNodeCapacity,
1274
1952
  bvhLeafSortCapacity,
1275
1953
  emissiveTriangleCapacity: emissiveTriangleIndices.capacity,
@@ -1346,6 +2024,24 @@ function packWavefrontSceneObjects(sceneObjects, capacity = sceneObjects.length)
1346
2024
  object.opacity,
1347
2025
  object.ior
1348
2026
  ]);
2027
+ writeVec4(floatView, byteOffset + 96, [
2028
+ object.sheenColor[0] ?? 0,
2029
+ object.sheenColor[1] ?? 0,
2030
+ object.sheenColor[2] ?? 0,
2031
+ object.clearcoat
2032
+ ]);
2033
+ writeVec4(floatView, byteOffset + 112, [
2034
+ object.clearcoatRoughness,
2035
+ object.specular,
2036
+ object.transmission,
2037
+ 0
2038
+ ]);
2039
+ writeVec4(floatView, byteOffset + 128, [
2040
+ object.specularColor[0] ?? 1,
2041
+ object.specularColor[1] ?? 1,
2042
+ object.specularColor[2] ?? 1,
2043
+ 1
2044
+ ]);
1349
2045
  });
1350
2046
  return Object.freeze({
1351
2047
  buffer: bytes,
@@ -1370,7 +2066,7 @@ function packWavefrontTriangles(triangles, capacity = triangles.length) {
1370
2066
  uintView[u32 + 3] = triangle.flags;
1371
2067
  uintView[u32 + 4] = triangle.materialRefId;
1372
2068
  uintView[u32 + 5] = triangle.mediumRefId;
1373
- uintView[u32 + 6] = 0;
2069
+ uintView[u32 + 6] = triangle.materialSlot ?? 0;
1374
2070
  uintView[u32 + 7] = 0;
1375
2071
  writeVec4(floatView, byteOffset + 32, [...triangle.v0, 0]);
1376
2072
  writeVec4(floatView, byteOffset + 48, [...triangle.v1, 0]);
@@ -1383,6 +2079,15 @@ function packWavefrontTriangles(triangles, capacity = triangles.length) {
1383
2079
  writeVec4(floatView, byteOffset + 160, triangle.color);
1384
2080
  writeVec4(floatView, byteOffset + 176, triangle.emission);
1385
2081
  writeVec4(floatView, byteOffset + 192, triangle.material);
2082
+ writeVec4(floatView, byteOffset + 208, triangle.materialResponse);
2083
+ writeVec4(floatView, byteOffset + 224, triangle.materialExtension ?? [0.08, 1, 0, 0]);
2084
+ writeVec4(floatView, byteOffset + 240, triangle.specularColor ?? [1, 1, 1, 1]);
2085
+ writeVec4(floatView, byteOffset + 256, triangle.baseColorAtlas ?? [0, 0, 1, 1]);
2086
+ writeVec4(floatView, byteOffset + 272, triangle.metallicRoughnessAtlas ?? [0, 0, 1, 1]);
2087
+ writeVec4(floatView, byteOffset + 288, triangle.normalAtlas ?? [0, 0, 1, 1]);
2088
+ writeVec4(floatView, byteOffset + 304, triangle.occlusionAtlas ?? [0, 0, 1, 1]);
2089
+ writeVec4(floatView, byteOffset + 320, triangle.emissiveAtlas ?? [0, 0, 1, 1]);
2090
+ writeVec4(floatView, byteOffset + 336, triangle.textureSettings ?? [1, 1, 1, 0]);
1386
2091
  });
1387
2092
  return Object.freeze({
1388
2093
  buffer: bytes,
@@ -1476,6 +2181,12 @@ function createConfigPayload(config, tile, frameIndex, buildRange = {}) {
1476
2181
  0,
1477
2182
  0
1478
2183
  ]);
2184
+ writeVec4(floatView, 304, [
2185
+ config.environmentMap.width ?? 1,
2186
+ config.environmentMap.height ?? 1,
2187
+ config.environmentMap.mipLevelCount ?? 1,
2188
+ config.environmentMap.hasImportanceData ? 1 : 0
2189
+ ]);
1479
2190
  return bytes;
1480
2191
  }
1481
2192
  function createTiles(width, height, tileSize) {
@@ -1649,7 +2360,8 @@ function intersectWavefrontReferenceTriangle(ray, triangle, options = {}) {
1649
2360
  position: Object.freeze(position),
1650
2361
  color: triangle.color,
1651
2362
  emission: triangle.emission,
1652
- material: triangle.material
2363
+ material: triangle.material,
2364
+ materialResponse: triangle.materialResponse
1653
2365
  });
1654
2366
  }
1655
2367
  function createWavefrontReferenceEnvironmentHit(config, ray) {
@@ -1675,7 +2387,8 @@ function createWavefrontReferenceEnvironmentHit(config, ray) {
1675
2387
  position: Object.freeze(add(ray.origin, scale(ray.direction, 1e3))),
1676
2388
  color: Object.freeze([0, 0, 0, 0]),
1677
2389
  emission: radiance,
1678
- material: Object.freeze([1, 0, 1, 1])
2390
+ material: Object.freeze([1, 0, 1, 1]),
2391
+ materialResponse: Object.freeze([0, 0, 0, 0])
1679
2392
  });
1680
2393
  }
1681
2394
  function traceWavefrontReferenceTriangles(config, ray, triangles, options = {}) {
@@ -1754,40 +2467,24 @@ function environmentMapIntegerScale(data) {
1754
2467
  }
1755
2468
  return 1;
1756
2469
  }
1757
- function readEnvironmentMapComponent(data, index, fallback, integerScale = 1) {
1758
- if (!data || index >= data.length) {
1759
- return fallback;
2470
+ function environmentMapHasSamplingData(environmentMap) {
2471
+ if (!environmentMap || !environmentMap.data) {
2472
+ return false;
1760
2473
  }
1761
- const value = Number(data[index]);
1762
- return Number.isFinite(value) ? Math.max(0, value) * integerScale : fallback;
2474
+ const width = Math.max(1, environmentMap.width ?? 1);
2475
+ const height = Math.max(1, environmentMap.height ?? 1);
2476
+ return environmentMap.data.length >= width * height * 4;
1763
2477
  }
1764
- function createEnvironmentMapUploadBytes(environmentMap, fallbackColor) {
1765
- const width = Math.max(1, environmentMap.width);
1766
- const height = Math.max(1, environmentMap.height);
1767
- const rowBytes = width * 8;
1768
- const bytesPerRow = alignTo(rowBytes, 256);
2478
+ function createRgba8TextureUpload(source) {
2479
+ const width = Math.max(1, Math.trunc(source.width));
2480
+ const height = Math.max(1, Math.trunc(source.height));
2481
+ const bytesPerRow = alignTo(width * 4, 256);
1769
2482
  const bytes = new Uint8Array(bytesPerRow * height);
1770
- const data = environmentMap.data;
1771
- const integerScale = environmentMapIntegerScale(data);
1772
- const view = new DataView(bytes.buffer);
1773
- const writeComponent = (targetOffset, sourceOffset, fallback) => {
1774
- view.setUint16(
1775
- targetOffset,
1776
- float32ToFloat16Bits(
1777
- readEnvironmentMapComponent(data, sourceOffset, fallback, integerScale)
1778
- ),
1779
- true
1780
- );
1781
- };
2483
+ const data = source.data instanceof Uint8Array ? source.data : new Uint8Array(source.data);
1782
2484
  for (let y = 0; y < height; y += 1) {
1783
- for (let x = 0; x < width; x += 1) {
1784
- const sourceOffset = (y * width + x) * 4;
1785
- const targetOffset = y * bytesPerRow + x * 8;
1786
- writeComponent(targetOffset, sourceOffset, fallbackColor[0]);
1787
- writeComponent(targetOffset + 2, sourceOffset + 1, fallbackColor[1]);
1788
- writeComponent(targetOffset + 4, sourceOffset + 2, fallbackColor[2]);
1789
- writeComponent(targetOffset + 6, sourceOffset + 3, fallbackColor[3] ?? 1);
1790
- }
2485
+ const sourceOffset = y * width * 4;
2486
+ const targetOffset = y * bytesPerRow;
2487
+ bytes.set(data.subarray(sourceOffset, sourceOffset + width * 4), targetOffset);
1791
2488
  }
1792
2489
  return Object.freeze({
1793
2490
  bytes,
@@ -1796,6 +2493,320 @@ function createEnvironmentMapUploadBytes(environmentMap, fallbackColor) {
1796
2493
  height
1797
2494
  });
1798
2495
  }
2496
+ function readEnvironmentMapComponent(data, index, fallback, integerScale = 1) {
2497
+ if (!data || index >= data.length) {
2498
+ return fallback;
2499
+ }
2500
+ const value = Number(data[index]);
2501
+ return Number.isFinite(value) ? Math.max(0, value) * integerScale : fallback;
2502
+ }
2503
+ function buildOrthonormalBasis(normal) {
2504
+ const tangentFallback = Math.abs(normal[1]) < 0.999 ? [0, 1, 0] : [1, 0, 0];
2505
+ const tangent = normalize(cross(tangentFallback, normal), [1, 0, 0]);
2506
+ const bitangent = normalize(cross(normal, tangent), [0, 0, 1]);
2507
+ return { tangent, bitangent };
2508
+ }
2509
+ function localToWorld(local, normal) {
2510
+ const basis = buildOrthonormalBasis(normal);
2511
+ return normalize(
2512
+ add(
2513
+ add(scale(basis.tangent, local[0]), scale(basis.bitangent, local[1])),
2514
+ scale(normal, local[2])
2515
+ ),
2516
+ normal
2517
+ );
2518
+ }
2519
+ function radicalInverseVdc(bits) {
2520
+ let value = bits >>> 0;
2521
+ value = (value << 16 | value >>> 16) >>> 0;
2522
+ value = ((value & 1431655765) << 1 | (value & 2863311530) >>> 1) >>> 0;
2523
+ value = ((value & 858993459) << 2 | (value & 3435973836) >>> 2) >>> 0;
2524
+ value = ((value & 252645135) << 4 | (value & 4042322160) >>> 4) >>> 0;
2525
+ value = ((value & 16711935) << 8 | (value & 4278255360) >>> 8) >>> 0;
2526
+ return value * 23283064365386963e-26;
2527
+ }
2528
+ function hammersley(index, count) {
2529
+ return [index / Math.max(count, 1), radicalInverseVdc(index)];
2530
+ }
2531
+ function importanceSampleGgx(sample, roughness, normal) {
2532
+ const alpha = Math.max(roughness * roughness, 1e-4);
2533
+ const phi = 2 * Math.PI * sample[0];
2534
+ const cosTheta = Math.sqrt((1 - sample[1]) / (1 + (alpha * alpha - 1) * sample[1]));
2535
+ const sinTheta = Math.sqrt(Math.max(0, 1 - cosTheta * cosTheta));
2536
+ const halfVector = localToWorld(
2537
+ [Math.cos(phi) * sinTheta, Math.sin(phi) * sinTheta, cosTheta],
2538
+ normal
2539
+ );
2540
+ return normalize(halfVector, normal);
2541
+ }
2542
+ function geometrySchlickGgx(nDotV, roughness) {
2543
+ const k = (roughness + 1) * (roughness + 1) / 8;
2544
+ return nDotV / Math.max(nDotV * (1 - k) + k, 1e-6);
2545
+ }
2546
+ function geometrySmith(nDotV, nDotL, roughness) {
2547
+ return geometrySchlickGgx(nDotV, roughness) * geometrySchlickGgx(nDotL, roughness);
2548
+ }
2549
+ function integrateBrdfSample(nDotV, roughness, sampleCount) {
2550
+ const viewDirection = [Math.sqrt(Math.max(0, 1 - nDotV * nDotV)), 0, nDotV];
2551
+ const normal = [0, 0, 1];
2552
+ let scaleTerm = 0;
2553
+ let biasTerm = 0;
2554
+ for (let index = 0; index < sampleCount; index += 1) {
2555
+ const xi = hammersley(index, sampleCount);
2556
+ const halfVector = importanceSampleGgx(xi, roughness, normal);
2557
+ const vDotH = Math.max(dot(viewDirection, halfVector), 0);
2558
+ const lightDirection = normalize(
2559
+ subtract(scale(halfVector, 2 * vDotH), viewDirection),
2560
+ normal
2561
+ );
2562
+ const nDotL = Math.max(lightDirection[2], 0);
2563
+ const nDotH = Math.max(halfVector[2], 0);
2564
+ if (nDotL <= 0 || nDotH <= 0 || vDotH <= 0) {
2565
+ continue;
2566
+ }
2567
+ const geometry = geometrySmith(nDotV, nDotL, roughness);
2568
+ const visibility = geometry * vDotH / Math.max(nDotH * nDotV, 1e-6);
2569
+ const fresnel = (1 - vDotH) ** 5;
2570
+ scaleTerm += (1 - fresnel) * visibility;
2571
+ biasTerm += fresnel * visibility;
2572
+ }
2573
+ return [scaleTerm / sampleCount, biasTerm / sampleCount];
2574
+ }
2575
+ function createBrdfLutUploadBytes(size = DEFAULT_BRDF_LUT_SIZE, sampleCount = 1024) {
2576
+ const cacheKey = `${Math.max(1, Math.trunc(size))}:${Math.max(1, Math.trunc(sampleCount))}`;
2577
+ const cached = BRDF_LUT_UPLOAD_CACHE.get(cacheKey);
2578
+ if (cached) {
2579
+ return cached;
2580
+ }
2581
+ const width = Math.max(1, Math.trunc(size));
2582
+ const height = Math.max(1, Math.trunc(size));
2583
+ const rowBytes = width * 8;
2584
+ const bytesPerRow = alignTo(rowBytes, 256);
2585
+ const bytes = new Uint8Array(bytesPerRow * height);
2586
+ const view = new DataView(bytes.buffer);
2587
+ for (let y = 0; y < height; y += 1) {
2588
+ const roughness = (y + 0.5) / height;
2589
+ for (let x = 0; x < width; x += 1) {
2590
+ const nDotV = Math.max((x + 0.5) / width, 1e-4);
2591
+ const [scaleTerm, biasTerm] = integrateBrdfSample(nDotV, roughness, sampleCount);
2592
+ const offset = y * bytesPerRow + x * 8;
2593
+ view.setUint16(offset, float32ToFloat16Bits(scaleTerm), true);
2594
+ view.setUint16(offset + 2, float32ToFloat16Bits(biasTerm), true);
2595
+ view.setUint16(offset + 4, float32ToFloat16Bits(0), true);
2596
+ view.setUint16(offset + 6, float32ToFloat16Bits(1), true);
2597
+ }
2598
+ }
2599
+ const upload = Object.freeze({ bytes, bytesPerRow, width, height });
2600
+ BRDF_LUT_UPLOAD_CACHE.set(cacheKey, upload);
2601
+ return upload;
2602
+ }
2603
+ function createLinearEnvironmentPixels(environmentMap, fallbackColor) {
2604
+ const width = Math.max(1, environmentMap.width);
2605
+ const height = Math.max(1, environmentMap.height);
2606
+ const pixels = new Float32Array(width * height * 4);
2607
+ const data = environmentMap.data;
2608
+ const integerScale = environmentMapIntegerScale(data);
2609
+ for (let index = 0; index < width * height; index += 1) {
2610
+ const sourceOffset = index * 4;
2611
+ const targetOffset = index * 4;
2612
+ pixels[targetOffset] = readEnvironmentMapComponent(data, sourceOffset, fallbackColor[0], integerScale);
2613
+ pixels[targetOffset + 1] = readEnvironmentMapComponent(data, sourceOffset + 1, fallbackColor[1], integerScale);
2614
+ pixels[targetOffset + 2] = readEnvironmentMapComponent(data, sourceOffset + 2, fallbackColor[2], integerScale);
2615
+ pixels[targetOffset + 3] = readEnvironmentMapComponent(data, sourceOffset + 3, fallbackColor[3] ?? 1, integerScale);
2616
+ }
2617
+ return pixels;
2618
+ }
2619
+ function environmentUvToDirection(u, v, rotationRadians = 0) {
2620
+ const angle = (u - rotationRadians / (2 * Math.PI) - 0.5) * 2 * Math.PI;
2621
+ const theta = v * Math.PI;
2622
+ const sinTheta = Math.sin(theta);
2623
+ return [
2624
+ Math.cos(angle) * sinTheta,
2625
+ Math.cos(theta),
2626
+ Math.sin(angle) * sinTheta
2627
+ ];
2628
+ }
2629
+ function sampleEnvironmentPixelsBilinear(pixels, width, height, u, v) {
2630
+ const wrappedU = (u % 1 + 1) % 1;
2631
+ const clampedV = clamp(v, 0, 1);
2632
+ const x = wrappedU * width - 0.5;
2633
+ const y = clampedV * height - 0.5;
2634
+ const x0 = (Math.floor(x) % width + width) % width;
2635
+ const y0 = clamp(Math.floor(y), 0, height - 1);
2636
+ const x1 = (x0 + 1) % width;
2637
+ const y1 = clamp(y0 + 1, 0, height - 1);
2638
+ const tx = x - Math.floor(x);
2639
+ const ty = y - Math.floor(y);
2640
+ const read = (px, py) => {
2641
+ const offset = (py * width + px) * 4;
2642
+ return [pixels[offset], pixels[offset + 1], pixels[offset + 2], pixels[offset + 3]];
2643
+ };
2644
+ const a = read(x0, y0);
2645
+ const b = read(x1, y0);
2646
+ const c = read(x0, y1);
2647
+ const d = read(x1, y1);
2648
+ const mixPair = (first, second, factor) => first * (1 - factor) + second * factor;
2649
+ return [
2650
+ mixPair(mixPair(a[0], b[0], tx), mixPair(c[0], d[0], tx), ty),
2651
+ mixPair(mixPair(a[1], b[1], tx), mixPair(c[1], d[1], tx), ty),
2652
+ mixPair(mixPair(a[2], b[2], tx), mixPair(c[2], d[2], tx), ty),
2653
+ mixPair(mixPair(a[3], b[3], tx), mixPair(c[3], d[3], tx), ty)
2654
+ ];
2655
+ }
2656
+ function directionToEnvironmentUv(direction, rotationRadians = 0) {
2657
+ const unitDirection = normalize(direction, [0, 1, 0]);
2658
+ const rotationTurns = rotationRadians / (2 * Math.PI);
2659
+ const u = ((Math.atan2(unitDirection[2], unitDirection[0]) / (2 * Math.PI) + 0.5 + rotationTurns) % 1 + 1) % 1;
2660
+ const v = Math.acos(clamp(unitDirection[1], -1, 1)) / Math.PI;
2661
+ return [u, clamp(v, 0, 1)];
2662
+ }
2663
+ function sampleEnvironmentRadiance(pixels, width, height, direction, rotationRadians = 0) {
2664
+ const [u, v] = directionToEnvironmentUv(direction, rotationRadians);
2665
+ return sampleEnvironmentPixelsBilinear(pixels, width, height, u, v);
2666
+ }
2667
+ function createFloat16RgbaUploadFromLevels(levels) {
2668
+ return levels.map((level) => {
2669
+ const rowBytes = level.width * 8;
2670
+ const bytesPerRow = alignTo(rowBytes, 256);
2671
+ const bytes = new Uint8Array(bytesPerRow * level.height);
2672
+ const view = new DataView(bytes.buffer);
2673
+ for (let y = 0; y < level.height; y += 1) {
2674
+ for (let x = 0; x < level.width; x += 1) {
2675
+ const sourceOffset = (y * level.width + x) * 4;
2676
+ const targetOffset = y * bytesPerRow + x * 8;
2677
+ view.setUint16(targetOffset, float32ToFloat16Bits(level.data[sourceOffset]), true);
2678
+ view.setUint16(targetOffset + 2, float32ToFloat16Bits(level.data[sourceOffset + 1]), true);
2679
+ view.setUint16(targetOffset + 4, float32ToFloat16Bits(level.data[sourceOffset + 2]), true);
2680
+ view.setUint16(targetOffset + 6, float32ToFloat16Bits(level.data[sourceOffset + 3]), true);
2681
+ }
2682
+ }
2683
+ return Object.freeze({ bytes, bytesPerRow, width: level.width, height: level.height });
2684
+ });
2685
+ }
2686
+ function createPrefilteredEnvironmentLevels(environmentMap, fallbackColor) {
2687
+ const sourcePixels = createLinearEnvironmentPixels(environmentMap, fallbackColor);
2688
+ const sourceWidth = Math.max(1, environmentMap.width);
2689
+ const sourceHeight = Math.max(1, environmentMap.height);
2690
+ const mipLevelCount = Math.max(1, Math.floor(Math.log2(Math.max(sourceWidth, sourceHeight))) + 1);
2691
+ const levels = [
2692
+ Object.freeze({
2693
+ width: sourceWidth,
2694
+ height: sourceHeight,
2695
+ data: sourcePixels
2696
+ })
2697
+ ];
2698
+ for (let mipLevel = 1; mipLevel < mipLevelCount; mipLevel += 1) {
2699
+ const width = Math.max(1, sourceWidth >> mipLevel);
2700
+ const height = Math.max(1, sourceHeight >> mipLevel);
2701
+ const roughness = mipLevelCount <= 1 ? 0 : mipLevel / (mipLevelCount - 1);
2702
+ const data = new Float32Array(width * height * 4);
2703
+ const sampleCount = roughness < 0.25 ? 64 : roughness < 0.6 ? 96 : 128;
2704
+ for (let y = 0; y < height; y += 1) {
2705
+ for (let x = 0; x < width; x += 1) {
2706
+ const direction = environmentUvToDirection((x + 0.5) / width, (y + 0.5) / height, environmentMap.rotationRadians);
2707
+ const normal = normalize(direction, [0, 1, 0]);
2708
+ const viewDirection = normal;
2709
+ let totalWeight = 0;
2710
+ const accum = [0, 0, 0];
2711
+ for (let sampleIndex = 0; sampleIndex < sampleCount; sampleIndex += 1) {
2712
+ const xi = hammersley(sampleIndex, sampleCount);
2713
+ const halfVector = importanceSampleGgx(xi, roughness, normal);
2714
+ const viewDotHalf = Math.max(dot(viewDirection, halfVector), 0);
2715
+ const lightDirection = normalize(
2716
+ subtract(scale(halfVector, 2 * viewDotHalf), viewDirection),
2717
+ normal
2718
+ );
2719
+ const nDotL = Math.max(dot(normal, lightDirection), 0);
2720
+ if (nDotL <= 1e-6) {
2721
+ continue;
2722
+ }
2723
+ const radiance = sampleEnvironmentRadiance(
2724
+ sourcePixels,
2725
+ sourceWidth,
2726
+ sourceHeight,
2727
+ lightDirection,
2728
+ environmentMap.rotationRadians
2729
+ );
2730
+ accum[0] += radiance[0] * nDotL;
2731
+ accum[1] += radiance[1] * nDotL;
2732
+ accum[2] += radiance[2] * nDotL;
2733
+ totalWeight += nDotL;
2734
+ }
2735
+ const offset = (y * width + x) * 4;
2736
+ data[offset] = accum[0] / Math.max(totalWeight, 1e-6);
2737
+ data[offset + 1] = accum[1] / Math.max(totalWeight, 1e-6);
2738
+ data[offset + 2] = accum[2] / Math.max(totalWeight, 1e-6);
2739
+ data[offset + 3] = 1;
2740
+ }
2741
+ }
2742
+ levels.push(Object.freeze({ width, height, data }));
2743
+ }
2744
+ return Object.freeze({
2745
+ levels,
2746
+ mipLevelCount,
2747
+ width: sourceWidth,
2748
+ height: sourceHeight
2749
+ });
2750
+ }
2751
+ function createEnvironmentSamplingTables(environmentMap, fallbackColor) {
2752
+ if (!environmentMapHasSamplingData(environmentMap)) {
2753
+ return Object.freeze({
2754
+ width: 1,
2755
+ height: 1,
2756
+ pdf: new Float32Array([1]),
2757
+ marginalCdf: new Float32Array([1]),
2758
+ conditionalCdf: new Float32Array([1]),
2759
+ hasImportanceData: false
2760
+ });
2761
+ }
2762
+ const pixels = createLinearEnvironmentPixels(environmentMap, fallbackColor);
2763
+ const width = Math.max(1, environmentMap.width);
2764
+ const height = Math.max(1, environmentMap.height);
2765
+ const pdf = new Float32Array(width * height);
2766
+ const marginalCdf = new Float32Array(height);
2767
+ const conditionalCdf = new Float32Array(width * height);
2768
+ const rowSums = new Float32Array(height);
2769
+ let totalWeight = 0;
2770
+ for (let y = 0; y < height; y += 1) {
2771
+ const theta = (y + 0.5) / height * Math.PI;
2772
+ const sinTheta = Math.max(Math.sin(theta), 1e-4);
2773
+ let rowWeight = 0;
2774
+ for (let x = 0; x < width; x += 1) {
2775
+ const offset = (y * width + x) * 4;
2776
+ const luminance = pixels[offset] * 0.2126 + pixels[offset + 1] * 0.7152 + pixels[offset + 2] * 0.0722;
2777
+ const weight = Math.max(luminance * sinTheta, 1e-6);
2778
+ pdf[y * width + x] = weight;
2779
+ rowWeight += weight;
2780
+ conditionalCdf[y * width + x] = rowWeight;
2781
+ }
2782
+ rowSums[y] = rowWeight;
2783
+ totalWeight += rowWeight;
2784
+ if (rowWeight > 0) {
2785
+ for (let x = 0; x < width; x += 1) {
2786
+ conditionalCdf[y * width + x] /= rowWeight;
2787
+ }
2788
+ } else {
2789
+ for (let x = 0; x < width; x += 1) {
2790
+ conditionalCdf[y * width + x] = (x + 1) / width;
2791
+ }
2792
+ }
2793
+ marginalCdf[y] = totalWeight;
2794
+ }
2795
+ for (let y = 0; y < height; y += 1) {
2796
+ marginalCdf[y] /= Math.max(totalWeight, 1e-6);
2797
+ }
2798
+ for (let index = 0; index < pdf.length; index += 1) {
2799
+ pdf[index] /= Math.max(totalWeight, 1e-6);
2800
+ }
2801
+ return Object.freeze({
2802
+ width,
2803
+ height,
2804
+ pdf,
2805
+ marginalCdf,
2806
+ conditionalCdf,
2807
+ hasImportanceData: true
2808
+ });
2809
+ }
1799
2810
  function createEnvironmentMapResource(device, constants, environmentMap, fallbackColor) {
1800
2811
  if (environmentMap.view) {
1801
2812
  return Object.freeze({
@@ -1805,10 +2816,14 @@ function createEnvironmentMapResource(device, constants, environmentMap, fallbac
1805
2816
  addressModeU: "repeat",
1806
2817
  addressModeV: "clamp-to-edge",
1807
2818
  magFilter: "linear",
1808
- minFilter: "linear"
2819
+ minFilter: "linear",
2820
+ mipmapFilter: "linear"
1809
2821
  }),
1810
2822
  texture: null,
1811
- ownsTexture: false
2823
+ ownsTexture: false,
2824
+ width: Math.max(1, environmentMap.width),
2825
+ height: Math.max(1, environmentMap.height),
2826
+ mipLevelCount: Math.max(1, environmentMap.mipLevelCount ?? 1)
1812
2827
  });
1813
2828
  }
1814
2829
  if (environmentMap.texture && typeof environmentMap.texture.createView === "function") {
@@ -1819,15 +2834,91 @@ function createEnvironmentMapResource(device, constants, environmentMap, fallbac
1819
2834
  addressModeU: "repeat",
1820
2835
  addressModeV: "clamp-to-edge",
1821
2836
  magFilter: "linear",
1822
- minFilter: "linear"
2837
+ minFilter: "linear",
2838
+ mipmapFilter: "linear"
1823
2839
  }),
1824
2840
  texture: environmentMap.texture,
1825
- ownsTexture: false
2841
+ ownsTexture: false,
2842
+ width: Math.max(1, environmentMap.width),
2843
+ height: Math.max(1, environmentMap.height),
2844
+ mipLevelCount: Math.max(1, environmentMap.mipLevelCount ?? 1)
1826
2845
  });
1827
2846
  }
1828
- const upload = createEnvironmentMapUploadBytes(environmentMap, fallbackColor);
2847
+ const prefiltered = createPrefilteredEnvironmentLevels(environmentMap, fallbackColor);
2848
+ const uploads = createFloat16RgbaUploadFromLevels(prefiltered.levels);
1829
2849
  const texture = device.createTexture({
1830
2850
  label: environmentMap.enabled ? "plasius.wavefront.environmentMap" : "plasius.wavefront.environmentMapFallback",
2851
+ size: { width: prefiltered.width, height: prefiltered.height },
2852
+ format: "rgba16float",
2853
+ mipLevelCount: prefiltered.mipLevelCount,
2854
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
2855
+ });
2856
+ uploads.forEach((upload, mipLevel) => {
2857
+ device.queue.writeTexture(
2858
+ { texture, mipLevel },
2859
+ upload.bytes,
2860
+ { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
2861
+ { width: upload.width, height: upload.height, depthOrArrayLayers: 1 }
2862
+ );
2863
+ });
2864
+ return Object.freeze({
2865
+ view: texture.createView(),
2866
+ sampler: environmentMap.sampler ?? device.createSampler({
2867
+ label: "plasius.wavefront.environmentMapSampler",
2868
+ addressModeU: "repeat",
2869
+ addressModeV: "clamp-to-edge",
2870
+ magFilter: "linear",
2871
+ minFilter: "linear",
2872
+ mipmapFilter: "linear"
2873
+ }),
2874
+ texture,
2875
+ ownsTexture: true,
2876
+ width: prefiltered.width,
2877
+ height: prefiltered.height,
2878
+ mipLevelCount: prefiltered.mipLevelCount
2879
+ });
2880
+ }
2881
+ function createEnvironmentSamplingTextureResource(device, constants, environmentMap, fallbackColor) {
2882
+ const tables = createEnvironmentSamplingTables(environmentMap, fallbackColor);
2883
+ const rowBytes = tables.width * 8;
2884
+ const bytesPerRow = alignTo(rowBytes, 256);
2885
+ const bytes = new Uint8Array(bytesPerRow * tables.height);
2886
+ const view = new DataView(bytes.buffer);
2887
+ for (let y = 0; y < tables.height; y += 1) {
2888
+ for (let x = 0; x < tables.width; x += 1) {
2889
+ const probability = tables.pdf[y * tables.width + x];
2890
+ const conditional = tables.conditionalCdf[y * tables.width + x];
2891
+ const marginal = tables.marginalCdf[y];
2892
+ const offset = y * bytesPerRow + x * 8;
2893
+ view.setUint16(offset, float32ToFloat16Bits(probability), true);
2894
+ view.setUint16(offset + 2, float32ToFloat16Bits(conditional), true);
2895
+ view.setUint16(offset + 4, float32ToFloat16Bits(marginal), true);
2896
+ view.setUint16(offset + 6, float32ToFloat16Bits(1), true);
2897
+ }
2898
+ }
2899
+ const texture = device.createTexture({
2900
+ label: "plasius.wavefront.environmentSampling",
2901
+ size: { width: tables.width, height: tables.height },
2902
+ format: "rgba16float",
2903
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
2904
+ });
2905
+ device.queue.writeTexture(
2906
+ { texture },
2907
+ bytes,
2908
+ { bytesPerRow, rowsPerImage: tables.height },
2909
+ { width: tables.width, height: tables.height, depthOrArrayLayers: 1 }
2910
+ );
2911
+ return Object.freeze({
2912
+ view: texture.createView(),
2913
+ texture,
2914
+ ownsTexture: true,
2915
+ hasImportanceData: tables.hasImportanceData
2916
+ });
2917
+ }
2918
+ function createBrdfLutResource(device, constants, size = DEFAULT_BRDF_LUT_SIZE) {
2919
+ const upload = createBrdfLutUploadBytes(size);
2920
+ const texture = device.createTexture({
2921
+ label: "plasius.wavefront.brdfLut",
1831
2922
  size: { width: upload.width, height: upload.height },
1832
2923
  format: "rgba16float",
1833
2924
  usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
@@ -1840,14 +2931,36 @@ function createEnvironmentMapResource(device, constants, environmentMap, fallbac
1840
2931
  );
1841
2932
  return Object.freeze({
1842
2933
  view: texture.createView(),
1843
- sampler: environmentMap.sampler ?? device.createSampler({
1844
- label: "plasius.wavefront.environmentMapSampler",
1845
- addressModeU: "repeat",
2934
+ sampler: device.createSampler({
2935
+ label: "plasius.wavefront.brdfLutSampler",
2936
+ addressModeU: "clamp-to-edge",
1846
2937
  addressModeV: "clamp-to-edge",
1847
2938
  magFilter: "linear",
1848
2939
  minFilter: "linear"
1849
2940
  }),
1850
2941
  texture,
2942
+ ownsTexture: true,
2943
+ width: upload.width,
2944
+ height: upload.height
2945
+ });
2946
+ }
2947
+ function createAtlasTextureResource(device, constants, atlas, label) {
2948
+ const upload = createRgba8TextureUpload(atlas);
2949
+ const texture = device.createTexture({
2950
+ label,
2951
+ size: { width: upload.width, height: upload.height },
2952
+ format: "rgba8unorm",
2953
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
2954
+ });
2955
+ device.queue.writeTexture(
2956
+ { texture },
2957
+ upload.bytes,
2958
+ { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
2959
+ { width: upload.width, height: upload.height, depthOrArrayLayers: 1 }
2960
+ );
2961
+ return Object.freeze({
2962
+ texture,
2963
+ view: texture.createView(),
1851
2964
  ownsTexture: true
1852
2965
  });
1853
2966
  }
@@ -1893,6 +3006,24 @@ ${diagnostics}` : "";
1893
3006
  });
1894
3007
  }
1895
3008
  }
3009
+ async function assertShaderModuleCompiles(shaderModule, label) {
3010
+ if (typeof shaderModule?.compilationInfo !== "function") {
3011
+ return;
3012
+ }
3013
+ const info = await shaderModule.compilationInfo();
3014
+ const messages = Array.isArray(info?.messages) ? info.messages : [];
3015
+ const errors = messages.filter((message) => message?.type === "error");
3016
+ if (errors.length <= 0) {
3017
+ return;
3018
+ }
3019
+ const diagnostics = errors.map((message) => {
3020
+ const line = Number.isFinite(message.lineNum) ? message.lineNum : "?";
3021
+ const column = Number.isFinite(message.linePos) ? message.linePos : "?";
3022
+ return `line ${line}:${column} ${message.message}`;
3023
+ }).join("\n");
3024
+ throw new Error(`WGSL compilation preflight failed for ${label}:
3025
+ ${diagnostics}`);
3026
+ }
1896
3027
  async function createRenderPipeline(device, descriptor) {
1897
3028
  if (typeof device.createRenderPipelineAsync === "function") {
1898
3029
  return device.createRenderPipelineAsync(descriptor);
@@ -1901,6 +3032,7 @@ async function createRenderPipeline(device, descriptor) {
1901
3032
  }
1902
3033
  var WAVEFRONT_COMPUTE_WGSL = `
1903
3034
  const RAY_FLAG_GUIDED_EMISSIVE: u32 = 1u;
3035
+ const RAY_FLAG_DELTA_SAMPLE: u32 = 2u;
1904
3036
 
1905
3037
  struct RayRecord {
1906
3038
  rayId: u32,
@@ -1926,11 +3058,12 @@ struct HitRecord {
1926
3058
  primitiveId: u32,
1927
3059
  materialRefId: u32,
1928
3060
  mediumRefId: u32,
3061
+ materialSlot: u32,
1929
3062
  pad0: u32,
1930
3063
  pad1: u32,
1931
- pad2: u32,
1932
3064
  distance: f32,
1933
- pad3: vec3<f32>,
3065
+ occlusion: f32,
3066
+ pad2: vec2<f32>,
1934
3067
  position: vec4<f32>,
1935
3068
  geometricNormal: vec4<f32>,
1936
3069
  shadingNormal: vec4<f32>,
@@ -1939,6 +3072,9 @@ struct HitRecord {
1939
3072
  color: vec4<f32>,
1940
3073
  emission: vec4<f32>,
1941
3074
  material: vec4<f32>,
3075
+ materialResponse: vec4<f32>,
3076
+ materialExtension: vec4<f32>,
3077
+ specularColor: vec4<f32>,
1942
3078
  };
1943
3079
 
1944
3080
  struct SceneObject {
@@ -1951,6 +3087,9 @@ struct SceneObject {
1951
3087
  color: vec4<f32>,
1952
3088
  emission: vec4<f32>,
1953
3089
  material: vec4<f32>,
3090
+ materialResponse: vec4<f32>,
3091
+ materialExtension: vec4<f32>,
3092
+ specularColor: vec4<f32>,
1954
3093
  };
1955
3094
 
1956
3095
  struct TriangleRecord {
@@ -1960,7 +3099,7 @@ struct TriangleRecord {
1960
3099
  flags: u32,
1961
3100
  materialRefId: u32,
1962
3101
  mediumRefId: u32,
1963
- pad0: u32,
3102
+ materialSlot: u32,
1964
3103
  pad1: u32,
1965
3104
  v0: vec4<f32>,
1966
3105
  v1: vec4<f32>,
@@ -1973,6 +3112,15 @@ struct TriangleRecord {
1973
3112
  color: vec4<f32>,
1974
3113
  emission: vec4<f32>,
1975
3114
  material: vec4<f32>,
3115
+ materialResponse: vec4<f32>,
3116
+ materialExtension: vec4<f32>,
3117
+ specularColor: vec4<f32>,
3118
+ baseColorAtlas: vec4<f32>,
3119
+ metallicRoughnessAtlas: vec4<f32>,
3120
+ normalAtlas: vec4<f32>,
3121
+ occlusionAtlas: vec4<f32>,
3122
+ emissiveAtlas: vec4<f32>,
3123
+ textureSettings: vec4<f32>,
1976
3124
  };
1977
3125
 
1978
3126
  struct BvhNode {
@@ -1993,10 +3141,10 @@ struct BvhLeafRef {
1993
3141
 
1994
3142
  struct ScatterResult {
1995
3143
  direction: vec4<f32>,
3144
+ pdf: f32,
1996
3145
  flags: u32,
1997
3146
  pad0: u32,
1998
3147
  pad1: u32,
1999
- pad2: u32,
2000
3148
  };
2001
3149
 
2002
3150
  struct MeshVertex {
@@ -2017,10 +3165,19 @@ struct MeshRange {
2017
3165
  triangleCount: u32,
2018
3166
  firstVertex: u32,
2019
3167
  vertexCount: u32,
2020
- pad0: u32,
3168
+ materialSlot: u32,
2021
3169
  color: vec4<f32>,
2022
3170
  emission: vec4<f32>,
2023
3171
  material: vec4<f32>,
3172
+ materialResponse: vec4<f32>,
3173
+ materialExtension: vec4<f32>,
3174
+ specularColor: vec4<f32>,
3175
+ baseColorAtlas: vec4<f32>,
3176
+ metallicRoughnessAtlas: vec4<f32>,
3177
+ normalAtlas: vec4<f32>,
3178
+ occlusionAtlas: vec4<f32>,
3179
+ emissiveAtlas: vec4<f32>,
3180
+ textureSettings: vec4<f32>,
2024
3181
  };
2025
3182
 
2026
3183
  struct FrameConfig {
@@ -2061,6 +3218,7 @@ struct FrameConfig {
2061
3218
  _portalPad1: u32,
2062
3219
  environmentMapSettings: vec4<f32>,
2063
3220
  pathResolveSettings: vec4<f32>,
3221
+ environmentMapMeta: vec4<f32>,
2064
3222
  };
2065
3223
 
2066
3224
  struct Counters {
@@ -2123,6 +3281,15 @@ struct EnvironmentPortal {
2123
3281
  @group(0) @binding(20) var environmentMapTexture: texture_2d<f32>;
2124
3282
  @group(0) @binding(21) var environmentMapSampler: sampler;
2125
3283
  @group(0) @binding(22) var<storage, read_write> pathVertices: array<vec4<f32>>;
3284
+ @group(0) @binding(23) var baseColorAtlasTexture: texture_2d<f32>;
3285
+ @group(0) @binding(24) var metallicRoughnessAtlasTexture: texture_2d<f32>;
3286
+ @group(0) @binding(25) var normalAtlasTexture: texture_2d<f32>;
3287
+ @group(0) @binding(26) var occlusionAtlasTexture: texture_2d<f32>;
3288
+ @group(0) @binding(27) var emissiveAtlasTexture: texture_2d<f32>;
3289
+ @group(0) @binding(28) var materialAtlasSampler: sampler;
3290
+ @group(0) @binding(29) var brdfLutTexture: texture_2d<f32>;
3291
+ @group(0) @binding(30) var brdfLutSampler: sampler;
3292
+ @group(0) @binding(31) var environmentSamplingTexture: texture_2d<f32>;
2126
3293
 
2127
3294
  fn hash_u32(value: u32) -> u32 {
2128
3295
  var x = value;
@@ -2159,6 +3326,146 @@ fn safe_normalize(value: vec3<f32>, fallback: vec3<f32>) -> vec3<f32> {
2159
3326
  return value / len;
2160
3327
  }
2161
3328
 
3329
+ struct TangentBasis {
3330
+ tangent: vec3<f32>,
3331
+ bitangent: vec3<f32>,
3332
+ };
3333
+
3334
+ struct SurfaceMaterialSample {
3335
+ color: vec4<f32>,
3336
+ emission: vec4<f32>,
3337
+ material: vec4<f32>,
3338
+ materialResponse: vec4<f32>,
3339
+ materialExtension: vec4<f32>,
3340
+ specularColor: vec4<f32>,
3341
+ shadingNormal: vec3<f32>,
3342
+ occlusion: f32,
3343
+ };
3344
+
3345
+ fn srgb_to_linear_channel(value: f32) -> f32 {
3346
+ if (value <= 0.04045) {
3347
+ return value / 12.92;
3348
+ }
3349
+ return pow((value + 0.055) / 1.055, 2.4);
3350
+ }
3351
+
3352
+ fn srgb_to_linear_vec3(value: vec3<f32>) -> vec3<f32> {
3353
+ return vec3<f32>(
3354
+ srgb_to_linear_channel(value.x),
3355
+ srgb_to_linear_channel(value.y),
3356
+ srgb_to_linear_channel(value.z)
3357
+ );
3358
+ }
3359
+
3360
+ fn wrap_uv(uv: vec2<f32>) -> vec2<f32> {
3361
+ return fract(fract(uv) + vec2<f32>(1.0));
3362
+ }
3363
+
3364
+ fn atlas_sample_uv(rect: vec4<f32>, uv: vec2<f32>) -> vec2<f32> {
3365
+ let local = wrap_uv(uv);
3366
+ let clamped = clamp(local, vec2<f32>(0.001), vec2<f32>(0.999));
3367
+ return rect.xy + clamped * rect.zw;
3368
+ }
3369
+
3370
+ fn sample_atlas(textureRef: texture_2d<f32>, rect: vec4<f32>, uv: vec2<f32>) -> vec4<f32> {
3371
+ return textureSampleLevel(textureRef, materialAtlasSampler, atlas_sample_uv(rect, uv), 0.0);
3372
+ }
3373
+
3374
+ fn build_triangle_tangent_basis(
3375
+ triangle: TriangleRecord,
3376
+ fallbackNormal: vec3<f32>
3377
+ ) -> TangentBasis {
3378
+ let edge1 = triangle.v1.xyz - triangle.v0.xyz;
3379
+ let edge2 = triangle.v2.xyz - triangle.v0.xyz;
3380
+ let uv0 = triangle.uv0uv1.xy;
3381
+ let uv1 = triangle.uv0uv1.zw;
3382
+ let uv2 = triangle.uv2Pad.xy;
3383
+ let deltaUv1 = uv1 - uv0;
3384
+ let deltaUv2 = uv2 - uv0;
3385
+ let determinant = deltaUv1.x * deltaUv2.y - deltaUv1.y * deltaUv2.x;
3386
+ if (abs(determinant) <= 0.000001) {
3387
+ let tangentFallback = select(vec3<f32>(0.0, 1.0, 0.0), vec3<f32>(1.0, 0.0, 0.0), abs(fallbackNormal.y) >= 0.999);
3388
+ let tangent = safe_normalize(cross(tangentFallback, fallbackNormal), vec3<f32>(1.0, 0.0, 0.0));
3389
+ let bitangent = safe_normalize(cross(fallbackNormal, tangent), vec3<f32>(0.0, 0.0, 1.0));
3390
+ return TangentBasis(tangent, bitangent);
3391
+ }
3392
+ let inverse = 1.0 / determinant;
3393
+ let tangent = safe_normalize(
3394
+ inverse * (edge1 * deltaUv2.y - edge2 * deltaUv1.y),
3395
+ vec3<f32>(1.0, 0.0, 0.0)
3396
+ );
3397
+ let bitangent = safe_normalize(
3398
+ inverse * (-edge1 * deltaUv2.x + edge2 * deltaUv1.x),
3399
+ vec3<f32>(0.0, 0.0, 1.0)
3400
+ );
3401
+ return TangentBasis(tangent, bitangent);
3402
+ }
3403
+
3404
+ fn sample_surface_material(
3405
+ triangle: TriangleRecord,
3406
+ uv: vec2<f32>,
3407
+ geometricNormal: vec3<f32>,
3408
+ shadingNormal: vec3<f32>
3409
+ ) -> SurfaceMaterialSample {
3410
+ let baseColorTexel = sample_atlas(baseColorAtlasTexture, triangle.baseColorAtlas, uv);
3411
+ let baseColor = vec4<f32>(
3412
+ clamp(triangle.color.rgb * srgb_to_linear_vec3(baseColorTexel.rgb), vec3<f32>(0.0), vec3<f32>(1.0)),
3413
+ clamp(triangle.color.a * baseColorTexel.a, 0.0, 1.0)
3414
+ );
3415
+ let metallicRoughnessTexel = sample_atlas(
3416
+ metallicRoughnessAtlasTexture,
3417
+ triangle.metallicRoughnessAtlas,
3418
+ uv
3419
+ );
3420
+ let normalTexel = sample_atlas(normalAtlasTexture, triangle.normalAtlas, uv);
3421
+ let occlusionTexel = sample_atlas(occlusionAtlasTexture, triangle.occlusionAtlas, uv);
3422
+ let emissiveTexel = sample_atlas(emissiveAtlasTexture, triangle.emissiveAtlas, uv);
3423
+ let normalScale = clamp(triangle.textureSettings.x, 0.0, 1.0);
3424
+ let tangentBasis = build_triangle_tangent_basis(triangle, geometricNormal);
3425
+ let tangentNormal = safe_normalize(
3426
+ vec3<f32>(
3427
+ (normalTexel.x * 2.0 - 1.0) * normalScale,
3428
+ (normalTexel.y * 2.0 - 1.0) * normalScale,
3429
+ 1.0 + ((normalTexel.z * 2.0 - 1.0) - 1.0) * normalScale
3430
+ ),
3431
+ vec3<f32>(0.0, 0.0, 1.0)
3432
+ );
3433
+ let mappedNormal = safe_normalize(
3434
+ tangentBasis.tangent * tangentNormal.x +
3435
+ tangentBasis.bitangent * tangentNormal.y +
3436
+ shadingNormal * tangentNormal.z,
3437
+ shadingNormal
3438
+ );
3439
+ let emission = vec4<f32>(
3440
+ max(
3441
+ triangle.emission.rgb *
3442
+ srgb_to_linear_vec3(emissiveTexel.rgb) *
3443
+ max(triangle.textureSettings.z, 0.0),
3444
+ vec3<f32>(0.0)
3445
+ ),
3446
+ clamp(triangle.emission.a * emissiveTexel.a, 0.0, 1.0)
3447
+ );
3448
+ return SurfaceMaterialSample(
3449
+ baseColor,
3450
+ emission,
3451
+ vec4<f32>(
3452
+ clamp(triangle.material.x * metallicRoughnessTexel.y, 0.0, 1.0),
3453
+ clamp(triangle.material.y * metallicRoughnessTexel.z, 0.0, 1.0),
3454
+ clamp(triangle.material.z * baseColor.a, 0.0, 1.0),
3455
+ clamp(triangle.material.w, 1.0, 3.0)
3456
+ ),
3457
+ triangle.materialResponse,
3458
+ triangle.materialExtension,
3459
+ triangle.specularColor,
3460
+ repair_shading_normal(geometricNormal, mappedNormal),
3461
+ clamp(
3462
+ mix(1.0, occlusionTexel.x, clamp(triangle.textureSettings.y, 0.0, 1.0)),
3463
+ 0.0,
3464
+ 1.0
3465
+ )
3466
+ );
3467
+ }
3468
+
2162
3469
  fn saturate(value: f32) -> f32 {
2163
3470
  return clamp(value, 0.0, 1.0);
2164
3471
  }
@@ -2167,6 +3474,10 @@ fn max_component(value: vec3<f32>) -> f32 {
2167
3474
  return max(max(value.x, value.y), value.z);
2168
3475
  }
2169
3476
 
3477
+ fn radiance_luminance(value: vec3<f32>) -> f32 {
3478
+ return dot(value, vec3<f32>(0.2126, 0.7152, 0.0722));
3479
+ }
3480
+
2170
3481
  fn environment_map_enabled() -> bool {
2171
3482
  return config.environmentMapSettings.x > 0.5;
2172
3483
  }
@@ -2295,6 +3606,343 @@ fn environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
2295
3606
  select(vec3<f32>(1.0), portalScale, portalHit);
2296
3607
  }
2297
3608
 
3609
+ fn direct_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
3610
+ let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
3611
+ let portalScale = environment_portal_radiance_scale(origin, rayDirection);
3612
+ let portalHit = max_component(portalScale) > 0.0001;
3613
+ if (
3614
+ config.environmentPortalCount > 0u &&
3615
+ config.environmentPortalMode == 2u &&
3616
+ !portalHit
3617
+ ) {
3618
+ return vec3<f32>(0.0);
3619
+ }
3620
+ return base_environment_radiance(rayDirection) *
3621
+ select(vec3<f32>(1.0), portalScale, portalHit);
3622
+ }
3623
+
3624
+ fn radical_inverse_vdc(bitsValue: u32) -> f32 {
3625
+ var bits = bitsValue;
3626
+ bits = (bits << 16u) | (bits >> 16u);
3627
+ bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xaaaaaaaau) >> 1u);
3628
+ bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xccccccccu) >> 2u);
3629
+ bits = ((bits & 0x0f0f0f0fu) << 4u) | ((bits & 0xf0f0f0f0u) >> 4u);
3630
+ bits = ((bits & 0x00ff00ffu) << 8u) | ((bits & 0xff00ff00u) >> 8u);
3631
+ return f32(bits) * 2.3283064365386963e-10;
3632
+ }
3633
+
3634
+ fn hammersley_2d(index: u32, count: u32) -> vec2<f32> {
3635
+ return vec2<f32>(f32(index) / max(f32(count), 1.0), radical_inverse_vdc(index));
3636
+ }
3637
+
3638
+ fn build_basis_tangent(normal: vec3<f32>) -> vec3<f32> {
3639
+ let tangentFallback = select(vec3<f32>(0.0, 1.0, 0.0), vec3<f32>(1.0, 0.0, 0.0), abs(normal.y) >= 0.999);
3640
+ return safe_normalize(cross(tangentFallback, normal), vec3<f32>(1.0, 0.0, 0.0));
3641
+ }
3642
+
3643
+ fn local_to_world(local: vec3<f32>, normal: vec3<f32>) -> vec3<f32> {
3644
+ let tangent = build_basis_tangent(normal);
3645
+ let bitangent = safe_normalize(cross(normal, tangent), vec3<f32>(0.0, 0.0, 1.0));
3646
+ return safe_normalize(tangent * local.x + bitangent * local.y + normal * local.z, normal);
3647
+ }
3648
+
3649
+ fn cosine_sample_hemisphere(sample: vec2<f32>, normal: vec3<f32>) -> vec3<f32> {
3650
+ let phi = 6.28318530718 * sample.x;
3651
+ let radius = sqrt(sample.y);
3652
+ let x = cos(phi) * radius;
3653
+ let y = sin(phi) * radius;
3654
+ let z = sqrt(max(0.0, 1.0 - sample.y));
3655
+ return local_to_world(vec3<f32>(x, y, z), normal);
3656
+ }
3657
+
3658
+ fn importance_sample_ggx(sample: vec2<f32>, roughness: f32, normal: vec3<f32>) -> vec3<f32> {
3659
+ let alpha = max(roughness * roughness, 0.0001);
3660
+ let phi = 6.28318530718 * sample.x;
3661
+ let cosTheta = sqrt((1.0 - sample.y) / max(1.0 + (alpha * alpha - 1.0) * sample.y, 0.0001));
3662
+ let sinTheta = sqrt(max(0.0, 1.0 - cosTheta * cosTheta));
3663
+ let localHalf = vec3<f32>(cos(phi) * sinTheta, sin(phi) * sinTheta, cosTheta);
3664
+ return local_to_world(localHalf, normal);
3665
+ }
3666
+
3667
+ fn distribution_ggx(normal: vec3<f32>, halfVector: vec3<f32>, roughness: f32) -> f32 {
3668
+ let alpha = max(roughness * roughness, 0.0001);
3669
+ let alpha2 = alpha * alpha;
3670
+ let nDotH = saturate(dot(normal, halfVector));
3671
+ let denominator = nDotH * nDotH * (alpha2 - 1.0) + 1.0;
3672
+ return alpha2 / max(3.14159265359 * denominator * denominator, 0.000001);
3673
+ }
3674
+
3675
+ fn geometry_schlick_ggx(nDotValue: f32, roughness: f32) -> f32 {
3676
+ let k = ((roughness + 1.0) * (roughness + 1.0)) / 8.0;
3677
+ return nDotValue / max(nDotValue * (1.0 - k) + k, 0.000001);
3678
+ }
3679
+
3680
+ fn geometry_smith(normal: vec3<f32>, viewDirection: vec3<f32>, lightDirection: vec3<f32>, roughness: f32) -> f32 {
3681
+ let nDotV = saturate(dot(normal, viewDirection));
3682
+ let nDotL = saturate(dot(normal, lightDirection));
3683
+ return geometry_schlick_ggx(nDotV, roughness) * geometry_schlick_ggx(nDotL, roughness);
3684
+ }
3685
+
3686
+ fn fresnel_schlick(cosine: f32, f0: vec3<f32>) -> vec3<f32> {
3687
+ return f0 + (vec3<f32>(1.0) - f0) * pow(1.0 - cosine, 5.0);
3688
+ }
3689
+
3690
+ fn sample_brdf_lut(nDotV: f32, roughness: f32) -> vec2<f32> {
3691
+ let uv = vec2<f32>(clamp(nDotV, 0.0, 1.0), clamp(roughness, 0.0, 1.0));
3692
+ return textureSampleLevel(brdfLutTexture, brdfLutSampler, uv, 0.0).xy;
3693
+ }
3694
+
3695
+ fn prefiltered_environment_radiance(direction: vec3<f32>, roughness: f32) -> vec3<f32> {
3696
+ let uv = environment_map_uv(direction);
3697
+ let maxLevel = max(config.environmentMapMeta.z - 1.0, 0.0);
3698
+ let lod = clamp(roughness, 0.0, 1.0) * maxLevel;
3699
+ let texel = max(textureSampleLevel(environmentMapTexture, environmentMapSampler, uv, lod).rgb, vec3<f32>(0.0));
3700
+ return texel * max(config.environmentMapSettings.y, 0.0);
3701
+ }
3702
+
3703
+ fn environment_pdf_dimensions() -> vec2<u32> {
3704
+ return vec2<u32>(
3705
+ max(u32(config.environmentMapMeta.x), 1u),
3706
+ max(u32(config.environmentMapMeta.y), 1u)
3707
+ );
3708
+ }
3709
+
3710
+ fn environment_importance_sampling_enabled() -> bool {
3711
+ return config.environmentMapMeta.w > 0.5;
3712
+ }
3713
+
3714
+ fn uniform_sphere_pdf() -> f32 {
3715
+ return 1.0 / (4.0 * 3.14159265359);
3716
+ }
3717
+
3718
+ fn sample_uniform_sphere_direction(sample: vec2<f32>) -> vec3<f32> {
3719
+ let z = 1.0 - 2.0 * sample.y;
3720
+ let radial = sqrt(max(1.0 - z * z, 0.0));
3721
+ let phi = sample.x * 6.28318530718;
3722
+ return vec3<f32>(cos(phi) * radial, z, sin(phi) * radial);
3723
+ }
3724
+
3725
+ fn environment_sampling_texel(x: u32, y: u32) -> vec4<f32> {
3726
+ return textureLoad(environmentSamplingTexture, vec2<i32>(i32(x), i32(y)), 0);
3727
+ }
3728
+
3729
+ fn environment_pdf_texel(x: u32, y: u32) -> f32 {
3730
+ return environment_sampling_texel(x, y).x;
3731
+ }
3732
+
3733
+ fn environment_row_cdf_texel(y: u32) -> f32 {
3734
+ return environment_sampling_texel(0u, y).z;
3735
+ }
3736
+
3737
+ fn environment_column_cdf_texel(x: u32, y: u32) -> f32 {
3738
+ return environment_sampling_texel(x, y).y;
3739
+ }
3740
+
3741
+ fn environment_direction_pdf(direction: vec3<f32>) -> f32 {
3742
+ if (!environment_importance_sampling_enabled()) {
3743
+ return uniform_sphere_pdf();
3744
+ }
3745
+ let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
3746
+ let uv = environment_map_uv(rayDirection);
3747
+ let dimensions = environment_pdf_dimensions();
3748
+ let width = max(f32(dimensions.x), 1.0);
3749
+ let height = max(f32(dimensions.y), 1.0);
3750
+ let x = min(u32(uv.x * width), dimensions.x - 1u);
3751
+ let y = min(u32(uv.y * height), dimensions.y - 1u);
3752
+ let discretePdf = max(environment_pdf_texel(x, y), 0.0);
3753
+ let sinTheta = sqrt(max(1.0 - rayDirection.y * rayDirection.y, 0.0));
3754
+ let solidAngle = max((2.0 * 3.14159265359 * 3.14159265359 * sinTheta) / (width * height), 0.000001);
3755
+ return discretePdf / solidAngle;
3756
+ }
3757
+
3758
+ fn sample_row_cdf(count: u32, sampleValue: f32) -> u32 {
3759
+ if (count == 0u) {
3760
+ return 0u;
3761
+ }
3762
+ var low = 0u;
3763
+ var high = count - 1u;
3764
+ loop {
3765
+ if (low >= high) {
3766
+ break;
3767
+ }
3768
+ let mid = (low + high) / 2u;
3769
+ let cdfValue = environment_row_cdf_texel(mid);
3770
+ if (sampleValue <= cdfValue) {
3771
+ high = mid;
3772
+ } else {
3773
+ low = mid + 1u;
3774
+ }
3775
+ }
3776
+ return min(low, count - 1u);
3777
+ }
3778
+
3779
+ fn sample_column_cdf(row: u32, count: u32, sampleValue: f32) -> u32 {
3780
+ if (count == 0u) {
3781
+ return 0u;
3782
+ }
3783
+ var low = 0u;
3784
+ var high = count - 1u;
3785
+ loop {
3786
+ if (low >= high) {
3787
+ break;
3788
+ }
3789
+ let mid = (low + high) / 2u;
3790
+ let cdfValue = environment_column_cdf_texel(mid, row);
3791
+ if (sampleValue <= cdfValue) {
3792
+ high = mid;
3793
+ } else {
3794
+ low = mid + 1u;
3795
+ }
3796
+ }
3797
+ return min(low, count - 1u);
3798
+ }
3799
+
3800
+ struct EnvironmentSample {
3801
+ direction: vec3<f32>,
3802
+ radiance: vec3<f32>,
3803
+ pdf: f32,
3804
+ };
3805
+
3806
+ fn sample_environment_importance(sample: vec2<f32>) -> EnvironmentSample {
3807
+ if (!environment_importance_sampling_enabled()) {
3808
+ let direction = sample_uniform_sphere_direction(sample);
3809
+ return EnvironmentSample(direction, base_environment_radiance(direction), uniform_sphere_pdf());
3810
+ }
3811
+ let dimensions = environment_pdf_dimensions();
3812
+ let row = sample_row_cdf(dimensions.y, sample.y);
3813
+ let column = sample_column_cdf(row, dimensions.x, sample.x);
3814
+ let uv = vec2<f32>(
3815
+ (f32(column) + 0.5) / max(f32(dimensions.x), 1.0),
3816
+ (f32(row) + 0.5) / max(f32(dimensions.y), 1.0)
3817
+ );
3818
+ let theta = uv.y * 3.14159265359;
3819
+ let phi = (uv.x - 0.5 - config.environmentMapSettings.z / 6.28318530718) * 6.28318530718;
3820
+ let sinTheta = sin(theta);
3821
+ let direction = vec3<f32>(cos(phi) * sinTheta, cos(theta), sin(phi) * sinTheta);
3822
+ let pdf = environment_direction_pdf(direction);
3823
+ return EnvironmentSample(direction, base_environment_radiance(direction), pdf);
3824
+ }
3825
+
3826
+ fn power_heuristic(pdfA: f32, pdfB: f32) -> f32 {
3827
+ let a2 = pdfA * pdfA;
3828
+ let b2 = pdfB * pdfB;
3829
+ return a2 / max(a2 + b2, 0.000001);
3830
+ }
3831
+
3832
+ fn visible_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
3833
+ let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
3834
+ let visible = !scene_visibility_blocked(origin, rayDirection, 1000000.0);
3835
+ return select(vec3<f32>(0.0), direct_environment_radiance(origin, rayDirection), visible);
3836
+ }
3837
+
3838
+ fn glossy_environment_direction(
3839
+ incidentDirection: vec3<f32>,
3840
+ normal: vec3<f32>,
3841
+ roughness: f32,
3842
+ normalBlendScale: f32
3843
+ ) -> vec3<f32> {
3844
+ let reflectionDirection = reflect(incidentDirection, normal);
3845
+ let blend = clamp(roughness * roughness * normalBlendScale, 0.0, 0.92);
3846
+ return safe_normalize(mix(reflectionDirection, normal, blend), normal);
3847
+ }
3848
+
3849
+ fn surface_glossiness(hit: HitRecord) -> f32 {
3850
+ let roughness = clamp(hit.material.x, 0.0, 1.0);
3851
+ let metallic = clamp(hit.material.y, 0.0, 1.0);
3852
+ let sheen = clamp(max_component(hit.materialResponse.xyz), 0.0, 1.0);
3853
+ let clearcoat = clamp(hit.materialResponse.w, 0.0, 1.0);
3854
+ let specularWeight = clamp(hit.materialExtension.y, 0.0, 1.0);
3855
+ let transmission = clamp(hit.materialExtension.z, 0.0, 1.0);
3856
+ let baseGloss =
3857
+ max(
3858
+ clearcoat,
3859
+ max(sheen * 0.72, max(specularWeight * (0.38 + metallic * 0.62), transmission))
3860
+ );
3861
+ return clamp(baseGloss * (1.0 - roughness * 0.72) + metallic * (1.0 - roughness) * 0.35, 0.0, 1.0);
3862
+ }
3863
+
3864
+ fn surface_specular_f0(hit: HitRecord, surfaceColor: vec3<f32>) -> vec3<f32> {
3865
+ let metallic = clamp(hit.material.y, 0.0, 1.0);
3866
+ let specularWeight = clamp(hit.materialExtension.y, 0.0, 1.0);
3867
+ let specularColor = clamp(hit.specularColor.xyz, vec3<f32>(0.0), vec3<f32>(1.0));
3868
+ let dielectricF0 = vec3<f32>(0.04) * specularWeight * specularColor;
3869
+ return mix(dielectricF0, surfaceColor, metallic);
3870
+ }
3871
+
3872
+ fn surface_bsdf_sampling_weights(hit: HitRecord) -> vec3<f32> {
3873
+ let metallic = clamp(hit.material.y, 0.0, 1.0);
3874
+ let clearcoat = clamp(hit.materialResponse.w, 0.0, 1.0);
3875
+ let specularWeight = clamp(hit.materialExtension.y, 0.0, 1.0);
3876
+ let diffuseWeight = clamp(
3877
+ (1.0 - metallic) * max(1.0 - specularWeight * 0.5 - clearcoat * 0.25, 0.15),
3878
+ 0.0,
3879
+ 1.0
3880
+ );
3881
+ let specWeight = clamp(max(metallic, specularWeight * 0.75) * (1.0 - clearcoat * 0.5), 0.0, 1.0);
3882
+ let clearcoatWeight = clamp(clearcoat, 0.0, 1.0);
3883
+ let totalWeight = max(diffuseWeight + specWeight + clearcoatWeight, 0.000001);
3884
+ return vec3<f32>(
3885
+ diffuseWeight / totalWeight,
3886
+ specWeight / totalWeight,
3887
+ clearcoatWeight / totalWeight
3888
+ );
3889
+ }
3890
+
3891
+ fn evaluate_surface_bsdf(hit: HitRecord, viewDirection: vec3<f32>, lightDirection: vec3<f32>) -> vec3<f32> {
3892
+ let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
3893
+ let surfaceColor = clamp(max(hit.color.xyz, config.ambientColor.xyz * 0.35), vec3<f32>(0.0), vec3<f32>(1.0));
3894
+ let roughness = clamp(hit.material.x, 0.0, 1.0);
3895
+ let metallic = clamp(hit.material.y, 0.0, 1.0);
3896
+ let clearcoat = clamp(hit.materialResponse.w, 0.0, 1.0);
3897
+ let clearcoatRoughness = clamp(hit.materialExtension.x, 0.0, 1.0);
3898
+ let occlusion = clamp(hit.occlusion, 0.0, 1.0);
3899
+ let nDotV = saturate(dot(normal, viewDirection));
3900
+ let nDotL = saturate(dot(normal, lightDirection));
3901
+ if (nDotV <= 0.0 || nDotL <= 0.0) {
3902
+ return vec3<f32>(0.0);
3903
+ }
3904
+ let halfVector = safe_normalize(viewDirection + lightDirection, normal);
3905
+ let vDotH = saturate(dot(viewDirection, halfVector));
3906
+ let f0 = surface_specular_f0(hit, surfaceColor);
3907
+ let fresnel = fresnel_schlick(vDotH, f0);
3908
+ let distribution = distribution_ggx(normal, halfVector, roughness);
3909
+ let geometry = geometry_smith(normal, viewDirection, lightDirection, roughness);
3910
+ let specular = (distribution * geometry * fresnel) / max(4.0 * nDotV * nDotL, 0.000001);
3911
+ let diffuseWeight = (1.0 - metallic) * (1.0 - clearcoat * 0.24) * (1.0 - clamp(max_component(fresnel), 0.0, 0.98));
3912
+ let diffuse = surfaceColor * diffuseWeight / 3.14159265359;
3913
+ let clearcoatHalf = safe_normalize(viewDirection + lightDirection, normal);
3914
+ let clearcoatDistribution = distribution_ggx(normal, clearcoatHalf, max(clearcoatRoughness, 0.02));
3915
+ let clearcoatGeometry = geometry_smith(normal, viewDirection, lightDirection, max(clearcoatRoughness, 0.02));
3916
+ let clearcoatFresnel = fresnel_schlick(saturate(dot(viewDirection, clearcoatHalf)), vec3<f32>(0.04));
3917
+ let clearcoatTerm =
3918
+ (clearcoatDistribution * clearcoatGeometry * clearcoatFresnel) /
3919
+ max(4.0 * nDotV * nDotL, 0.000001) *
3920
+ clearcoat;
3921
+ return (diffuse + specular + clearcoatTerm) * mix(0.42, 1.0, occlusion);
3922
+ }
3923
+
3924
+ fn diffuse_pdf(normal: vec3<f32>, lightDirection: vec3<f32>) -> f32 {
3925
+ return saturate(dot(normal, lightDirection)) / 3.14159265359;
3926
+ }
3927
+
3928
+ fn ggx_pdf(normal: vec3<f32>, viewDirection: vec3<f32>, lightDirection: vec3<f32>, roughness: f32) -> f32 {
3929
+ let halfVector = safe_normalize(viewDirection + lightDirection, normal);
3930
+ let nDotH = saturate(dot(normal, halfVector));
3931
+ let vDotH = saturate(dot(viewDirection, halfVector));
3932
+ let distribution = distribution_ggx(normal, halfVector, roughness);
3933
+ return (distribution * nDotH) / max(4.0 * vDotH, 0.000001);
3934
+ }
3935
+
3936
+ fn evaluate_surface_bsdf_pdf(hit: HitRecord, viewDirection: vec3<f32>, lightDirection: vec3<f32>) -> f32 {
3937
+ let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
3938
+ let roughness = clamp(hit.material.x, 0.0, 1.0);
3939
+ let weights = surface_bsdf_sampling_weights(hit);
3940
+ let diffuseTerm = diffuse_pdf(normal, lightDirection);
3941
+ let specTerm = ggx_pdf(normal, viewDirection, lightDirection, max(roughness, 0.02));
3942
+ let clearcoatTerm = ggx_pdf(normal, viewDirection, lightDirection, max(clamp(hit.materialExtension.x, 0.0, 1.0), 0.02));
3943
+ return weights.x * diffuseTerm + weights.y * specTerm + weights.z * clearcoatTerm;
3944
+ }
3945
+
2298
3946
  fn gated_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
2299
3947
  let portalScale = environment_portal_radiance_scale(origin, safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0)));
2300
3948
  if (
@@ -2310,9 +3958,45 @@ fn gated_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f
2310
3958
  fn surface_path_response(hit: HitRecord) -> vec3<f32> {
2311
3959
  let color = clamp(hit.color.xyz, vec3<f32>(0.0), vec3<f32>(1.0));
2312
3960
  let opacity = clamp(hit.material.z, 0.0, 1.0);
3961
+ let occlusion = clamp(hit.occlusion, 0.0, 1.0);
2313
3962
  let materialEnergy = select(0.68, 0.92, hit.materialKind == 1u || hit.materialKind == 2u);
2314
3963
  let transparentEnergy = select(materialEnergy, 0.9, hit.hitType == 3u);
2315
- return mix(vec3<f32>(1.0), color, max(opacity, 0.18)) * transparentEnergy;
3964
+ return mix(vec3<f32>(1.0), color, max(opacity, 0.18)) * transparentEnergy * mix(0.55, 1.0, occlusion);
3965
+ }
3966
+
3967
+ fn bounded_path_response_luminance(ray: RayRecord, hit: HitRecord) -> f32 {
3968
+ let daylightFloor = max(config.pathResolveSettings.y, 0.0) * 0.08;
3969
+ let hdriFloor = max(config.environmentMapSettings.w, 0.0) * 0.02;
3970
+ let sceneFloor = max(daylightFloor, hdriFloor);
3971
+ if (sceneFloor <= 0.000001) {
3972
+ return 0.0;
3973
+ }
3974
+ let bounceRatio = select(
3975
+ 0.0,
3976
+ f32(ray.bounce) / max(f32(config.maxDepth - 1u), 1.0),
3977
+ config.maxDepth > 1u
3978
+ );
3979
+ let bounceScale = 1.0 - bounceRatio * 0.55;
3980
+ let materialScale = select(1.0, 0.34, hit.materialKind == 1u || hit.materialKind == 2u);
3981
+ let transparentScale = select(materialScale, 0.58, hit.hitType == 3u);
3982
+ let opacityScale = mix(0.55, 1.0, clamp(hit.material.z, 0.0, 1.0));
3983
+ return sceneFloor * bounceScale * transparentScale * opacityScale;
3984
+ }
3985
+
3986
+ fn stabilize_surface_path_response(ray: RayRecord, hit: HitRecord, response: vec3<f32>) -> vec3<f32> {
3987
+ let minimumLuminance = bounded_path_response_luminance(ray, hit);
3988
+ let responseLuminance = radiance_luminance(response);
3989
+ if (minimumLuminance <= 0.000001 || responseLuminance >= minimumLuminance) {
3990
+ return response;
3991
+ }
3992
+ let tintBase = max(response, max(hit.color.xyz * 0.65, config.ambientColor.xyz * 0.35));
3993
+ let tint = tintBase / max(max_component(tintBase), 0.0001);
3994
+ let lifted = select(
3995
+ tint * minimumLuminance,
3996
+ response * (minimumLuminance / max(responseLuminance, 0.0001)),
3997
+ responseLuminance > 0.0001
3998
+ );
3999
+ return clamp(lifted, vec3<f32>(0.0), vec3<f32>(0.98));
2316
4000
  }
2317
4001
 
2318
4002
  fn sunlit_baseline_radiance(normal: vec3<f32>) -> vec3<f32> {
@@ -2331,12 +4015,24 @@ fn sunlit_baseline_radiance(normal: vec3<f32>) -> vec3<f32> {
2331
4015
  return clamp_sample_radiance(sunTint * baseline * skyFacing * directionalWeight * 0.04);
2332
4016
  }
2333
4017
 
2334
- fn terminal_surface_environment_source(hit: HitRecord) -> vec3<f32> {
4018
+ fn terminal_surface_environment_source(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
2335
4019
  let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
2336
- let normalEnvironment = gated_environment_radiance(
2337
- hit.position.xyz + normal * 0.003,
2338
- normal
4020
+ let origin = hit.position.xyz + normal * 0.003;
4021
+ let roughness = clamp(hit.material.x, 0.0, 1.0);
4022
+ let glossiness = surface_glossiness(hit);
4023
+ let normalEnvironment = gated_environment_radiance(origin, normal);
4024
+ let viewDirection = safe_normalize(-ray.direction.xyz, normal);
4025
+ let reflectionDirection = glossy_environment_direction(
4026
+ ray.direction.xyz,
4027
+ normal,
4028
+ roughness,
4029
+ mix(0.88, 0.38, glossiness)
2339
4030
  );
4031
+ let reflectionEnvironment = prefiltered_environment_radiance(reflectionDirection, roughness);
4032
+ let surfaceColor = clamp(max(hit.color.xyz, config.ambientColor.xyz * 0.35), vec3<f32>(0.0), vec3<f32>(1.0));
4033
+ let f0 = surface_specular_f0(hit, surfaceColor);
4034
+ let brdfTerm = sample_brdf_lut(saturate(dot(normal, viewDirection)), roughness);
4035
+ let specularEnvironment = reflectionEnvironment * (f0 * brdfTerm.x + vec3<f32>(brdfTerm.y));
2340
4036
  let sunlitFloor = sunlit_baseline_radiance(normal);
2341
4037
  let ambientFloor = select(
2342
4038
  max(config.ambientColor.xyz, sunlitFloor * 0.82),
@@ -2348,17 +4044,23 @@ fn terminal_surface_environment_source(hit: HitRecord) -> vec3<f32> {
2348
4044
  max(config.environmentMapSettings.w, max(0.12, config.pathResolveSettings.y * 0.42)),
2349
4045
  environment_map_enabled()
2350
4046
  );
2351
- let environmentFloor = max(ambientFloor, max(sunlitFloor, normalEnvironment * environmentInfluence));
4047
+ let glossyEnvironment = max(
4048
+ normalEnvironment,
4049
+ max(reflectionEnvironment * mix(0.24, 0.92, glossiness), specularEnvironment)
4050
+ );
4051
+ let environmentFloor = max(ambientFloor, max(sunlitFloor, glossyEnvironment * environmentInfluence));
2352
4052
  let materialFloor = select(0.7, 1.0, hit.materialKind == 0u || hit.materialKind == 3u);
2353
4053
  return clamp_sample_radiance(environmentFloor * materialFloor);
2354
4054
  }
2355
4055
 
2356
4056
  fn terminal_surface_environment_contribution(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
2357
4057
  let surfaceColor = max(hit.color.xyz, config.ambientColor.xyz);
4058
+ let occlusion = mix(0.75, 1.0, clamp(hit.occlusion, 0.0, 1.0));
2358
4059
  return clamp_sample_radiance(
2359
4060
  ray.throughput.xyz *
2360
4061
  surfaceColor *
2361
- terminal_surface_environment_source(hit)
4062
+ terminal_surface_environment_source(ray, hit) *
4063
+ occlusion
2362
4064
  );
2363
4065
  }
2364
4066
 
@@ -2391,6 +4093,10 @@ fn direct_environment_portal_irradiance(origin: vec3<f32>, normal: vec3<f32>) ->
2391
4093
  );
2392
4094
  let area = max(portal.position.w, 0.0001);
2393
4095
  let distanceFalloff = clamp(area / max(distanceSquared, area * 0.25), 0.0, 2.5);
4096
+ let traceDistance = max(sqrt(distanceSquared) - 0.01, 0.01);
4097
+ if (scene_visibility_blocked(origin, direction, traceDistance)) {
4098
+ continue;
4099
+ }
2394
4100
  irradiance = irradiance +
2395
4101
  portal.color.rgb *
2396
4102
  portal.normal.w *
@@ -2402,48 +4108,79 @@ fn direct_environment_portal_irradiance(origin: vec3<f32>, normal: vec3<f32>) ->
2402
4108
  return irradiance;
2403
4109
  }
2404
4110
 
2405
- fn surface_direct_environment_contribution(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
2406
- let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
2407
- let origin = hit.position.xyz + normal * 0.003;
2408
- let viewDirection = safe_normalize(-ray.direction.xyz, normal);
2409
- let surfaceColor = clamp(max(hit.color.xyz, config.ambientColor.xyz * 0.35), vec3<f32>(0.0), vec3<f32>(1.0));
2410
- let roughness = clamp(hit.material.x, 0.0, 1.0);
2411
- let metallic = clamp(hit.material.y, 0.0, 1.0);
2412
-
2413
- let normalEnvironment = gated_environment_radiance(origin, normal);
2414
- let skyVisibility = 0.35 + saturate(normal.y * 0.5 + 0.5) * 0.45;
2415
- let sunlitFloor = sunlit_baseline_radiance(normal);
2416
- let ambientIrradiance = max(
2417
- select(config.ambientColor.xyz * 0.72, config.ambientColor.xyz * 0.28, environment_map_enabled()),
2418
- sunlitFloor * select(0.72, 0.45, environment_map_enabled())
2419
- );
2420
- let environmentIrradianceScale = select(
2421
- max(0.16, config.pathResolveSettings.y * 0.45),
2422
- max(config.environmentMapSettings.w, max(0.16, config.pathResolveSettings.y * 0.45)),
2423
- environment_map_enabled()
4111
+ fn visibility_test_ray(origin: vec3<f32>, direction: vec3<f32>) -> RayRecord {
4112
+ let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
4113
+ return RayRecord(
4114
+ 0u,
4115
+ 0u,
4116
+ 0u,
4117
+ 0u,
4118
+ 0u,
4119
+ 0u,
4120
+ 0u,
4121
+ 0u,
4122
+ vec4<f32>(origin, 1.0),
4123
+ vec4<f32>(rayDirection, 0.0),
4124
+ vec4<f32>(1.0, 1.0, 1.0, 1.0)
2424
4125
  );
2425
- let skyIrradiance = max(ambientIrradiance, normalEnvironment * skyVisibility * environmentIrradianceScale);
4126
+ }
2426
4127
 
2427
- let sunDirection = safe_normalize(
2428
- config.environmentSunDirectionIntensity.xyz,
2429
- vec3<f32>(0.0, 1.0, 0.0)
2430
- );
2431
- let sunFacing = saturate(dot(normal, sunDirection));
2432
- let sunRadiance = gated_environment_radiance(origin, sunDirection);
2433
- let sunIrradiance = sunRadiance * sunFacing * 0.2;
2434
- let portalIrradiance = direct_environment_portal_irradiance(origin, normal);
4128
+ fn scene_visibility_blocked(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> bool {
4129
+ let testRay = visibility_test_ray(origin, direction);
4130
+ let nearest = max(maxDistance, 0.001);
2435
4131
 
2436
- let diffuseWeight = select(1.0 - metallic * 0.65, 0.22, hit.materialKind == 1u);
2437
- let diffuse = surfaceColor * (skyIrradiance + sunIrradiance + portalIrradiance) * diffuseWeight;
4132
+ for (var objectIndex = 0u; objectIndex < config.sceneObjectCount; objectIndex = objectIndex + 1u) {
4133
+ let object = sceneObjects[objectIndex];
4134
+ var current = no_candidate();
4135
+ if (object.kind == 1u) {
4136
+ current = intersect_sphere(testRay, object);
4137
+ } else if (object.kind == 2u) {
4138
+ current = intersect_box(testRay, object);
4139
+ }
4140
+ if (current.hit == 1u && current.distance < nearest) {
4141
+ return true;
4142
+ }
4143
+ }
2438
4144
 
2439
- let halfVector = safe_normalize(sunDirection + viewDirection, normal);
2440
- let specularPower = 8.0 + (1.0 - roughness) * 96.0;
2441
- let specularFacing = pow(saturate(dot(normal, halfVector)), specularPower) * sunFacing;
2442
- let specularTint = mix(vec3<f32>(0.04), surfaceColor, metallic);
2443
- let specular = specularTint * sunRadiance * specularFacing * select(0.16, 0.48, hit.materialKind == 1u || hit.materialKind == 2u);
4145
+ let meshCandidate = intersect_bvh(testRay, nearest);
4146
+ return meshCandidate.hit == 1u && meshCandidate.distance < nearest;
4147
+ }
2444
4148
 
2445
- let bounceWeight = select(1.0, 0.38, ray.bounce > 0u);
2446
- return clamp_sample_radiance(ray.throughput.xyz * (diffuse + specular) * bounceWeight);
4149
+ fn surface_direct_environment_contribution(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
4150
+ let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
4151
+ let origin = hit.position.xyz + normal * 0.003;
4152
+ let viewDirection = safe_normalize(-ray.direction.xyz, normal);
4153
+ let lightSample = sample_environment_importance(vec2<f32>(
4154
+ random01(mix_seed(ray.sourcePixelId, ray.sampleId, ray.bounce, config.frameIndex, 41u)),
4155
+ random01(mix_seed(ray.sourcePixelId, ray.sampleId, ray.bounce, config.frameIndex, 43u))
4156
+ ));
4157
+ if (lightSample.pdf <= 0.000001) {
4158
+ return vec3<f32>(0.0);
4159
+ }
4160
+ let lightDirection = safe_normalize(lightSample.direction, normal);
4161
+ let nDotL = saturate(dot(normal, lightDirection));
4162
+ if (nDotL <= 0.000001) {
4163
+ return vec3<f32>(0.0);
4164
+ }
4165
+ if (scene_visibility_blocked(origin, lightDirection, 1000000.0)) {
4166
+ return vec3<f32>(0.0);
4167
+ }
4168
+ let incidentRadiance = direct_environment_radiance(origin, lightDirection);
4169
+ if (max_component(incidentRadiance) <= 0.000001) {
4170
+ return vec3<f32>(0.0);
4171
+ }
4172
+ let bsdf = evaluate_surface_bsdf(hit, viewDirection, lightDirection);
4173
+ if (max_component(bsdf) <= 0.000001) {
4174
+ return vec3<f32>(0.0);
4175
+ }
4176
+ let bsdfPdf = evaluate_surface_bsdf_pdf(hit, viewDirection, lightDirection);
4177
+ let misWeight = power_heuristic(lightSample.pdf, bsdfPdf);
4178
+ let contribution =
4179
+ ray.throughput.xyz *
4180
+ bsdf *
4181
+ incidentRadiance *
4182
+ (nDotL * misWeight / max(lightSample.pdf, 0.000001));
4183
+ return clamp_sample_radiance(contribution);
2447
4184
  }
2448
4185
 
2449
4186
  fn default_mesh_range() -> MeshRange {
@@ -2462,7 +4199,16 @@ fn default_mesh_range() -> MeshRange {
2462
4199
  0u,
2463
4200
  vec4<f32>(0.72, 0.72, 0.68, 1.0),
2464
4201
  vec4<f32>(0.0),
2465
- vec4<f32>(0.72, 0.0, 1.0, 1.45)
4202
+ vec4<f32>(0.72, 0.0, 1.0, 1.45),
4203
+ vec4<f32>(0.0, 0.0, 0.0, 0.08),
4204
+ vec4<f32>(1.0, 1.0, 1.0, 1.0),
4205
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4206
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4207
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4208
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4209
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4210
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4211
+ vec4<f32>(1.0, 1.0, 1.0, 0.0)
2466
4212
  );
2467
4213
  }
2468
4214
 
@@ -2558,7 +4304,7 @@ fn prepareMeshTrianglesAndLeaves(@builtin(global_invocation_id) globalId: vec3<u
2558
4304
  mesh.flags,
2559
4305
  mesh.materialRefId,
2560
4306
  mesh.mediumRefId,
2561
- 0u,
4307
+ mesh.materialSlot,
2562
4308
  0u,
2563
4309
  vec4<f32>(vertex0.position.xyz, 0.0),
2564
4310
  vec4<f32>(vertex1.position.xyz, 0.0),
@@ -2570,7 +4316,16 @@ fn prepareMeshTrianglesAndLeaves(@builtin(global_invocation_id) globalId: vec3<u
2570
4316
  vec4<f32>(uv2, 0.0, 0.0),
2571
4317
  mesh.color,
2572
4318
  mesh.emission,
2573
- mesh.material
4319
+ mesh.material,
4320
+ mesh.materialResponse,
4321
+ mesh.materialExtension,
4322
+ mesh.specularColor,
4323
+ mesh.baseColorAtlas,
4324
+ mesh.metallicRoughnessAtlas,
4325
+ mesh.normalAtlas,
4326
+ mesh.occlusionAtlas,
4327
+ mesh.emissiveAtlas,
4328
+ mesh.textureSettings
2574
4329
  );
2575
4330
 
2576
4331
  let leafBase = config.triangleCount - 1u;
@@ -2729,7 +4484,8 @@ fn make_miss(ray: RayRecord) -> HitRecord {
2729
4484
  0u,
2730
4485
  0u,
2731
4486
  -1.0,
2732
- vec3<f32>(0.0),
4487
+ 1.0,
4488
+ vec2<f32>(0.0),
2733
4489
  vec4<f32>(ray.origin.xyz + ray.direction.xyz * 1000.0, 1.0),
2734
4490
  vec4<f32>(-ray.direction.xyz, 0.0),
2735
4491
  vec4<f32>(-ray.direction.xyz, 0.0),
@@ -2737,7 +4493,10 @@ fn make_miss(ray: RayRecord) -> HitRecord {
2737
4493
  vec4<f32>(0.0),
2738
4494
  vec4<f32>(radiance, 1.0),
2739
4495
  vec4<f32>(0.0),
2740
- vec4<f32>(1.0, 0.0, 1.0, 1.0)
4496
+ vec4<f32>(1.0, 0.0, 1.0, 1.0),
4497
+ vec4<f32>(0.0, 0.0, 0.0, 0.08),
4498
+ vec4<f32>(0.08, 1.0, 0.0, 0.0),
4499
+ vec4<f32>(1.0, 1.0, 1.0, 1.0)
2741
4500
  );
2742
4501
  }
2743
4502
 
@@ -3032,6 +4791,19 @@ fn denoise_range_space(value: vec3<f32>) -> vec3<f32> {
3032
4791
  return value / (vec3<f32>(1.0) + value);
3033
4792
  }
3034
4793
 
4794
+ fn denoise_sample_count() -> f32 {
4795
+ return clamp(1.0 / max(config.projectionAndSampling.z, 0.000001), 1.0, 256.0);
4796
+ }
4797
+
4798
+ fn denoise_strength() -> f32 {
4799
+ let spp = denoise_sample_count();
4800
+ return clamp(0.44 / sqrt(spp), 0.08, 0.44);
4801
+ }
4802
+
4803
+ fn denoise_kernel_radius() -> i32 {
4804
+ return select(1i, 2i, denoise_sample_count() < 2.5);
4805
+ }
4806
+
3035
4807
  @compute @workgroup_size(64)
3036
4808
  fn generatePrimaryRays(@builtin(global_invocation_id) globalId: vec3<u32>) {
3037
4809
  let index = globalId.x;
@@ -3070,7 +4842,10 @@ fn intersectActiveQueue(@builtin(global_invocation_id) globalId: vec3<u32>) {
3070
4842
  vec4<f32>(0.0),
3071
4843
  vec4<f32>(0.0),
3072
4844
  vec4<f32>(0.0),
3073
- vec4<f32>(1.0, 0.0, 1.0, 1.0)
4845
+ vec4<f32>(1.0, 0.0, 1.0, 1.0),
4846
+ vec4<f32>(0.0, 0.0, 0.0, 0.08),
4847
+ vec4<f32>(0.08, 1.0, 0.0, 0.0),
4848
+ vec4<f32>(1.0, 1.0, 1.0, 1.0)
3074
4849
  );
3075
4850
  var candidate = no_candidate();
3076
4851
  var hitTriangle = TriangleRecord(
@@ -3092,7 +4867,16 @@ fn intersectActiveQueue(@builtin(global_invocation_id) globalId: vec3<u32>) {
3092
4867
  vec4<f32>(0.0),
3093
4868
  vec4<f32>(0.0),
3094
4869
  vec4<f32>(0.0),
3095
- vec4<f32>(1.0, 0.0, 1.0, 1.0)
4870
+ vec4<f32>(1.0, 0.0, 1.0, 1.0),
4871
+ vec4<f32>(0.0, 0.0, 0.0, 0.08),
4872
+ vec4<f32>(0.08, 1.0, 0.0, 0.0),
4873
+ vec4<f32>(1.0, 1.0, 1.0, 1.0),
4874
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4875
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4876
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4877
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4878
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4879
+ vec4<f32>(1.0, 1.0, 1.0, 0.0)
3096
4880
  );
3097
4881
 
3098
4882
  for (var objectIndex = 0u; objectIndex < config.sceneObjectCount; objectIndex = objectIndex + 1u) {
@@ -3125,16 +4909,28 @@ fn intersectActiveQueue(@builtin(global_invocation_id) globalId: vec3<u32>) {
3125
4909
  let position = ray.origin.xyz + ray.direction.xyz * candidate.distance;
3126
4910
  let hitMaterialKind = select(hitObject.materialKind, hitTriangle.materialKind, candidate.triangleIndex != 0xffffffffu);
3127
4911
  let hitObjectId = select(hitObject.objectId, hitTriangle.meshId, candidate.triangleIndex != 0xffffffffu);
3128
- let hitColor = select(hitObject.color, hitTriangle.color, candidate.triangleIndex != 0xffffffffu);
3129
- let hitEmission = select(hitObject.emission, hitTriangle.emission, candidate.triangleIndex != 0xffffffffu);
3130
- let hitMaterial = select(hitObject.material, hitTriangle.material, candidate.triangleIndex != 0xffffffffu);
4912
+ let meshSurface = sample_surface_material(
4913
+ hitTriangle,
4914
+ candidate.uv,
4915
+ candidate.geometricNormal,
4916
+ candidate.shadingNormal
4917
+ );
4918
+ let hitColor = select(hitObject.color, meshSurface.color, candidate.triangleIndex != 0xffffffffu);
4919
+ let hitEmission = select(hitObject.emission, meshSurface.emission, candidate.triangleIndex != 0xffffffffu);
4920
+ let hitMaterial = select(hitObject.material, meshSurface.material, candidate.triangleIndex != 0xffffffffu);
4921
+ let hitMaterialResponse = select(hitObject.materialResponse, meshSurface.materialResponse, candidate.triangleIndex != 0xffffffffu);
4922
+ let hitMaterialExtension = select(hitObject.materialExtension, meshSurface.materialExtension, candidate.triangleIndex != 0xffffffffu);
4923
+ let hitSpecularColor = select(hitObject.specularColor, meshSurface.specularColor, candidate.triangleIndex != 0xffffffffu);
4924
+ let hitShadingNormal = select(candidate.shadingNormal, meshSurface.shadingNormal, candidate.triangleIndex != 0xffffffffu);
3131
4925
  let hitPrimitiveId = select(candidate.primitiveId, hitTriangle.triangleId, candidate.triangleIndex != 0xffffffffu);
3132
4926
  let hitMaterialRefId = select(candidate.materialRefId, hitTriangle.materialRefId, candidate.triangleIndex != 0xffffffffu);
3133
4927
  let hitMediumRefId = select(candidate.mediumRefId, hitTriangle.mediumRefId, candidate.triangleIndex != 0xffffffffu);
4928
+ let hitMaterialSlot = select(0u, hitTriangle.materialSlot, candidate.triangleIndex != 0xffffffffu);
4929
+ let hitOcclusion = select(1.0, meshSurface.occlusion, candidate.triangleIndex != 0xffffffffu);
3134
4930
  var hitType = 0u;
3135
4931
  if (hitMaterialKind == 4u || emission_power(hitEmission) > 0.0001) {
3136
4932
  hitType = 1u;
3137
- } else if (hitMaterialKind == 3u || hitMaterial.z < 0.999) {
4933
+ } else if (hitMaterialKind == 3u || hitMaterial.z < 0.999 || hitMaterialExtension.z > 0.001) {
3138
4934
  hitType = 3u;
3139
4935
  }
3140
4936
  atomicAdd(&counters.hitCount, 1u);
@@ -3148,19 +4944,23 @@ fn intersectActiveQueue(@builtin(global_invocation_id) globalId: vec3<u32>) {
3148
4944
  hitPrimitiveId,
3149
4945
  hitMaterialRefId,
3150
4946
  hitMediumRefId,
3151
- 0u,
4947
+ hitMaterialSlot,
3152
4948
  0u,
3153
4949
  0u,
3154
4950
  candidate.distance,
3155
- vec3<f32>(0.0),
4951
+ hitOcclusion,
4952
+ vec2<f32>(0.0),
3156
4953
  vec4<f32>(position, 1.0),
3157
4954
  vec4<f32>(candidate.geometricNormal, 0.0),
3158
- vec4<f32>(candidate.shadingNormal, 0.0),
4955
+ vec4<f32>(hitShadingNormal, 0.0),
3159
4956
  vec4<f32>(candidate.barycentric, 0.0),
3160
4957
  vec4<f32>(candidate.uv, 0.0, 0.0),
3161
4958
  hitColor,
3162
4959
  hitEmission,
3163
- hitMaterial
4960
+ hitMaterial,
4961
+ hitMaterialResponse,
4962
+ hitMaterialExtension,
4963
+ hitSpecularColor
3164
4964
  );
3165
4965
  }
3166
4966
 
@@ -3229,60 +5029,106 @@ fn sample_environment_portal_direction(hit: HitRecord, seed: u32, fallback: vec3
3229
5029
  }
3230
5030
 
3231
5031
  fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult {
5032
+ let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
5033
+ let viewDirection = safe_normalize(-ray.direction.xyz, normal);
3232
5034
  let roughness = clamp(hit.material.x, 0.0, 1.0);
3233
- if (hit.materialKind == 1u) {
5035
+ let transmission = clamp(hit.materialExtension.z, 0.0, 1.0);
5036
+ if (hit.materialKind == 1u && roughness <= 0.02) {
3234
5037
  return ScatterResult(
3235
- vec4<f32>(
3236
- safe_normalize(
3237
- reflect(ray.direction.xyz, hit.shadingNormal.xyz) + random_unit_vector(seed) * roughness,
3238
- hit.shadingNormal.xyz
3239
- ),
3240
- 0.0
3241
- ),
3242
- 0u,
3243
- 0u,
5038
+ vec4<f32>(reflect(ray.direction.xyz, normal), 0.0),
5039
+ 1.0,
5040
+ RAY_FLAG_DELTA_SAMPLE,
3244
5041
  0u,
3245
5042
  0u
3246
5043
  );
3247
5044
  }
3248
5045
 
3249
- if (hit.materialKind == 2u || hit.materialKind == 3u) {
5046
+ if (hit.materialKind == 2u || hit.materialKind == 3u || transmission > 0.001) {
3250
5047
  let ior = max(hit.material.w, 1.01);
3251
5048
  let etaRatio = select(ior, 1.0 / ior, hit.frontFace == 1u);
3252
- let cosTheta = min(dot(-ray.direction.xyz, hit.shadingNormal.xyz), 1.0);
5049
+ let cosTheta = min(dot(-ray.direction.xyz, normal), 1.0);
3253
5050
  let sinTheta = sqrt(max(0.0, 1.0 - cosTheta * cosTheta));
3254
5051
  let cannotRefract = etaRatio * sinTheta > 1.0;
3255
5052
  let reflectChance = schlick(cosTheta, etaRatio);
3256
- if (cannotRefract || random01(seed + 23u) < reflectChance) {
3257
- return ScatterResult(vec4<f32>(reflect(ray.direction.xyz, hit.shadingNormal.xyz), 0.0), 0u, 0u, 0u, 0u);
5053
+ let transmissionReflectChance = select(
5054
+ reflectChance,
5055
+ max(reflectChance, 1.0 - transmission),
5056
+ transmission > 0.001
5057
+ );
5058
+ if (cannotRefract || random01(seed + 23u) < transmissionReflectChance) {
5059
+ return ScatterResult(
5060
+ vec4<f32>(reflect(ray.direction.xyz, normal), 0.0),
5061
+ 1.0,
5062
+ RAY_FLAG_DELTA_SAMPLE,
5063
+ 0u,
5064
+ 0u
5065
+ );
3258
5066
  }
3259
- return ScatterResult(vec4<f32>(refract_direction(ray.direction.xyz, hit.shadingNormal.xyz, etaRatio), 0.0), 0u, 0u, 0u, 0u);
3260
- }
3261
-
3262
- let randomDiffuse = safe_normalize(
3263
- hit.shadingNormal.xyz + random_unit_vector(seed),
3264
- hit.shadingNormal.xyz
3265
- );
3266
- let guidedLight = sample_emissive_triangle_direction(hit, seed, randomDiffuse);
3267
- let canSampleLight = dot(hit.shadingNormal.xyz, guidedLight) > -0.04;
3268
- let guideProbability = select(0.38, 0.72, ray.bounce == 0u);
3269
- let useGuidedLight = canSampleLight && random01(seed + 37u) < guideProbability;
3270
- let guidedPortal = sample_environment_portal_direction(hit, seed, randomDiffuse);
3271
- let canSamplePortal = dot(hit.shadingNormal.xyz, guidedPortal) > -0.04;
3272
- let useGuidedPortal =
3273
- !useGuidedLight &&
3274
- canSamplePortal &&
3275
- config.environmentPortalCount > 0u &&
3276
- config.environmentPortalMode > 0u &&
3277
- random01(seed + 89u) < 0.58;
3278
- let guidedDirection = select(randomDiffuse, guidedPortal, useGuidedPortal);
3279
- return ScatterResult(
3280
- vec4<f32>(select(guidedDirection, guidedLight, useGuidedLight), 0.0),
3281
- select(0u, RAY_FLAG_GUIDED_EMISSIVE, useGuidedLight),
3282
- 0u,
3283
- 0u,
3284
- 0u
3285
- );
5067
+ return ScatterResult(
5068
+ vec4<f32>(refract_direction(ray.direction.xyz, normal, etaRatio), 0.0),
5069
+ 1.0,
5070
+ RAY_FLAG_DELTA_SAMPLE,
5071
+ 0u,
5072
+ 0u
5073
+ );
5074
+ }
5075
+
5076
+ let guidedEmissiveAvailable = config.emissiveTriangleCount > 0u;
5077
+ let guidedPortalAvailable =
5078
+ config.environmentPortalCount > 0u && config.environmentPortalMode != 0u;
5079
+ let guidedSelector = random01(seed + 17u);
5080
+ if (guidedEmissiveAvailable && guidedSelector < 0.18) {
5081
+ let guidedDirection = sample_emissive_triangle_direction(hit, seed + 101u, normal);
5082
+ if (dot(normal, guidedDirection) > 0.000001) {
5083
+ let guidedPdf = max(evaluate_surface_bsdf_pdf(hit, viewDirection, guidedDirection), 0.000001);
5084
+ return ScatterResult(
5085
+ vec4<f32>(guidedDirection, 0.0),
5086
+ guidedPdf,
5087
+ RAY_FLAG_GUIDED_EMISSIVE,
5088
+ 0u,
5089
+ 0u
5090
+ );
5091
+ }
5092
+ }
5093
+ if (guidedPortalAvailable && guidedSelector < 0.32) {
5094
+ let guidedDirection = sample_environment_portal_direction(hit, seed + 131u, normal);
5095
+ if (dot(normal, guidedDirection) > 0.000001) {
5096
+ let guidedPdf = max(evaluate_surface_bsdf_pdf(hit, viewDirection, guidedDirection), 0.000001);
5097
+ return ScatterResult(vec4<f32>(guidedDirection, 0.0), guidedPdf, 0u, 0u, 0u);
5098
+ }
5099
+ }
5100
+
5101
+ let weights = surface_bsdf_sampling_weights(hit);
5102
+ let selector = random01(seed + 31u);
5103
+ var lightDirection = normal;
5104
+ if (selector < weights.x) {
5105
+ lightDirection = cosine_sample_hemisphere(
5106
+ vec2<f32>(random01(seed + 37u), random01(seed + 41u)),
5107
+ normal
5108
+ );
5109
+ } else if (selector < weights.x + weights.y) {
5110
+ let halfVector = importance_sample_ggx(
5111
+ vec2<f32>(random01(seed + 47u), random01(seed + 53u)),
5112
+ max(roughness, 0.02),
5113
+ normal
5114
+ );
5115
+ lightDirection = safe_normalize(reflect(-viewDirection, halfVector), normal);
5116
+ } else {
5117
+ let halfVector = importance_sample_ggx(
5118
+ vec2<f32>(random01(seed + 59u), random01(seed + 61u)),
5119
+ max(clamp(hit.materialExtension.x, 0.0, 1.0), 0.02),
5120
+ normal
5121
+ );
5122
+ lightDirection = safe_normalize(reflect(-viewDirection, halfVector), normal);
5123
+ }
5124
+ if (dot(normal, lightDirection) <= 0.000001) {
5125
+ lightDirection = cosine_sample_hemisphere(
5126
+ vec2<f32>(random01(seed + 67u), random01(seed + 71u)),
5127
+ normal
5128
+ );
5129
+ }
5130
+ let pdf = max(evaluate_surface_bsdf_pdf(hit, viewDirection, lightDirection), 0.000001);
5131
+ return ScatterResult(vec4<f32>(lightDirection, 0.0), pdf, 0u, 0u, 0u);
3286
5132
  }
3287
5133
 
3288
5134
  @compute @workgroup_size(64)
@@ -3312,10 +5158,17 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
3312
5158
  }
3313
5159
 
3314
5160
  if (hit.hitType == 2u) {
5161
+ var sourceRadiance = hit.color.xyz;
5162
+ if ((ray.flags & RAY_FLAG_DELTA_SAMPLE) == 0u) {
5163
+ let bsdfPdf = max(ray.throughput.w, 0.000001);
5164
+ let lightPdf = environment_direction_pdf(ray.direction.xyz);
5165
+ let misWeight = power_heuristic(bsdfPdf, lightPdf);
5166
+ sourceRadiance = sourceRadiance * misWeight;
5167
+ }
3315
5168
  if (deferred_path_resolve_enabled()) {
3316
- record_deferred_terminal_source(ray, hit.color.xyz);
5169
+ record_deferred_terminal_source(ray, sourceRadiance);
3317
5170
  } else {
3318
- contribution = clamp_sample_radiance(ray.throughput.xyz * max(hit.color.xyz, config.ambientColor.xyz));
5171
+ contribution = clamp_sample_radiance(ray.throughput.xyz * sourceRadiance);
3319
5172
  accumulation[ray.rayId] =
3320
5173
  accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
3321
5174
  }
@@ -3323,13 +5176,13 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
3323
5176
  return;
3324
5177
  }
3325
5178
 
3326
- let response = surface_path_response(hit);
5179
+ let response = stabilize_surface_path_response(ray, hit, surface_path_response(hit));
3327
5180
  record_deferred_path_response(ray, response);
3328
5181
 
3329
5182
  let shouldEstimateDirectEnvironment =
3330
- !deferred_path_resolve_enabled() &&
3331
5183
  (hit.materialKind == 0u || hit.materialKind == 1u) &&
3332
- hit.material.z >= 0.95;
5184
+ hit.material.z >= 0.95 &&
5185
+ ray.bounce < 2u;
3333
5186
  if (shouldEstimateDirectEnvironment) {
3334
5187
  let directEnvironment = surface_direct_environment_contribution(ray, hit);
3335
5188
  accumulation[ray.rayId] =
@@ -3338,7 +5191,7 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
3338
5191
 
3339
5192
  if (ray.bounce + 1u >= config.maxDepth) {
3340
5193
  if (deferred_path_resolve_enabled()) {
3341
- record_deferred_terminal_source(ray, terminal_surface_environment_source(hit));
5194
+ record_deferred_terminal_source(ray, terminal_surface_environment_source(ray, hit));
3342
5195
  } else {
3343
5196
  let terminalEnvironment = terminal_surface_environment_contribution(ray, hit);
3344
5197
  accumulation[ray.rayId] =
@@ -3353,7 +5206,7 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
3353
5206
  let nextIndex = atomicAdd(&counters.nextCount, 1u);
3354
5207
  if (nextIndex >= config.tilePixelCount) {
3355
5208
  if (deferred_path_resolve_enabled()) {
3356
- record_deferred_terminal_source(ray, terminal_surface_environment_source(hit));
5209
+ record_deferred_terminal_source(ray, terminal_surface_environment_source(ray, hit));
3357
5210
  } else {
3358
5211
  let overflowEnvironment = terminal_surface_environment_contribution(ray, hit);
3359
5212
  accumulation[ray.rayId] =
@@ -3374,7 +5227,7 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
3374
5227
  0u,
3375
5228
  vec4<f32>(offset_origin(hit.position.xyz, hit.shadingNormal.xyz), 1.0),
3376
5229
  scatter.direction,
3377
- vec4<f32>(throughput, ray.throughput.w)
5230
+ vec4<f32>(throughput, scatter.pdf)
3378
5231
  );
3379
5232
  }
3380
5233
 
@@ -3443,8 +5296,11 @@ fn denoiseLinearRadiance(@builtin(global_invocation_id) globalId: vec3<u32>) {
3443
5296
 
3444
5297
  let pixel = vec2<i32>(i32(x), i32(y));
3445
5298
  let center = textureLoad(denoiseInputRadiance, pixel, 0).xyz;
3446
- var sum = center * 1.4;
3447
- var totalWeight = 1.4;
5299
+ let strength = denoise_strength();
5300
+ let kernelRadius = denoise_kernel_radius();
5301
+ let centerWeight = 1.7 - strength * 0.35;
5302
+ var sum = center * centerWeight;
5303
+ var totalWeight = centerWeight;
3448
5304
  let centerRange = denoise_range_space(center);
3449
5305
 
3450
5306
  for (var oy = -2i; oy <= 2i; oy = oy + 1i) {
@@ -3452,13 +5308,16 @@ fn denoiseLinearRadiance(@builtin(global_invocation_id) globalId: vec3<u32>) {
3452
5308
  if (ox == 0i && oy == 0i) {
3453
5309
  continue;
3454
5310
  }
5311
+ if (abs(ox) > kernelRadius || abs(oy) > kernelRadius) {
5312
+ continue;
5313
+ }
3455
5314
  let sx = clamp(i32(x) + ox, 0i, i32(config.canvasWidth) - 1i);
3456
5315
  let sy = clamp(i32(y) + oy, 0i, i32(config.canvasHeight) - 1i);
3457
5316
  let sampleColor = textureLoad(denoiseInputRadiance, vec2<i32>(sx, sy), 0).xyz;
3458
5317
  let colorDistance = length(denoise_range_space(sampleColor) - centerRange);
3459
- let rangeWeight = 1.0 / (1.0 + colorDistance * 7.0);
3460
- let distanceWeight = 1.0 / (1.0 + f32(ox * ox + oy * oy) * 0.24);
3461
- let diagonalWeight = select(1.0, 0.78, abs(ox) + abs(oy) > 2i);
5318
+ let rangeWeight = 1.0 / (1.0 + colorDistance * (11.0 + strength * 6.0));
5319
+ let distanceWeight = 1.0 / (1.0 + f32(ox * ox + oy * oy) * (0.62 + strength * 0.24));
5320
+ let diagonalWeight = select(1.0, 0.92, abs(ox) + abs(oy) > 1i);
3462
5321
  let weight = rangeWeight * diagonalWeight * distanceWeight;
3463
5322
  sum = sum + sampleColor * weight;
3464
5323
  totalWeight = totalWeight + weight;
@@ -3466,8 +5325,9 @@ fn denoiseLinearRadiance(@builtin(global_invocation_id) globalId: vec3<u32>) {
3466
5325
  }
3467
5326
 
3468
5327
  let filtered = sum / max(totalWeight, 0.0001);
3469
- let outlier = saturate(length(denoise_range_space(center) - denoise_range_space(filtered)) * 2.4);
3470
- let color = min(mix(center, filtered, 0.52 + outlier * 0.18), vec3<f32>(16.0));
5328
+ let outlier = saturate(length(denoise_range_space(center) - denoise_range_space(filtered)) * 2.1);
5329
+ let blend = min(0.3, strength * (0.62 + outlier * 0.12));
5330
+ let color = min(mix(center, filtered, blend), vec3<f32>(16.0));
3471
5331
  textureStore(denoisedRadianceImage, pixel, vec4<f32>(color, 1.0));
3472
5332
  }
3473
5333
 
@@ -3481,8 +5341,10 @@ fn resolveDenoisedOutputImage(@builtin(global_invocation_id) globalId: vec3<u32>
3481
5341
 
3482
5342
  let pixel = vec2<i32>(i32(x), i32(y));
3483
5343
  let center = textureLoad(finalDenoiseInputRadiance, pixel, 0).xyz;
3484
- var sum = center * 1.25;
3485
- var totalWeight = 1.25;
5344
+ let strength = denoise_strength();
5345
+ let centerWeight = 1.35 - strength * 0.25;
5346
+ var sum = center * centerWeight;
5347
+ var totalWeight = centerWeight;
3486
5348
  let centerRange = denoise_range_space(center);
3487
5349
 
3488
5350
  for (var oy = -1i; oy <= 1i; oy = oy + 1i) {
@@ -3494,8 +5356,8 @@ fn resolveDenoisedOutputImage(@builtin(global_invocation_id) globalId: vec3<u32>
3494
5356
  let sy = clamp(i32(y) + oy, 0i, i32(config.canvasHeight) - 1i);
3495
5357
  let sampleColor = textureLoad(finalDenoiseInputRadiance, vec2<i32>(sx, sy), 0).xyz;
3496
5358
  let colorDistance = length(denoise_range_space(sampleColor) - centerRange);
3497
- let rangeWeight = 1.0 / (1.0 + colorDistance * 9.0);
3498
- let distanceWeight = 1.0 / (1.0 + f32(ox * ox + oy * oy) * 0.4);
5359
+ let rangeWeight = 1.0 / (1.0 + colorDistance * (12.0 + strength * 8.0));
5360
+ let distanceWeight = 1.0 / (1.0 + f32(ox * ox + oy * oy) * (0.82 + strength * 0.28));
3499
5361
  let weight = rangeWeight * distanceWeight;
3500
5362
  sum = sum + sampleColor * weight;
3501
5363
  totalWeight = totalWeight + weight;
@@ -3503,8 +5365,9 @@ fn resolveDenoisedOutputImage(@builtin(global_invocation_id) globalId: vec3<u32>
3503
5365
  }
3504
5366
 
3505
5367
  let filtered = sum / max(totalWeight, 0.0001);
3506
- let outlier = saturate(length(denoise_range_space(center) - denoise_range_space(filtered)) * 2.8);
3507
- let radiance = min(mix(center, filtered, 0.28 + outlier * 0.12), vec3<f32>(16.0));
5368
+ let outlier = saturate(length(denoise_range_space(center) - denoise_range_space(filtered)) * 2.2);
5369
+ let blend = min(0.18, strength * (0.42 + outlier * 0.08));
5370
+ let radiance = min(mix(center, filtered, blend), vec3<f32>(16.0));
3508
5371
  textureStore(denoisedOutputImage, pixel, vec4<f32>(tone_map_radiance(radiance), 1.0));
3509
5372
  }
3510
5373
  `;
@@ -3571,104 +5434,34 @@ function readGpuLimit(adapter, device, name) {
3571
5434
  const deviceValue = Number(device?.limits?.[name]);
3572
5435
  return Number.isFinite(deviceValue) ? deviceValue : null;
3573
5436
  }
3574
- function createAdapterInfoSnapshot(adapter) {
3575
- const info = adapter?.info;
3576
- if (!info || typeof info !== "object") {
3577
- return null;
3578
- }
3579
- return Object.freeze({
3580
- vendor: typeof info.vendor === "string" ? info.vendor : "",
3581
- architecture: typeof info.architecture === "string" ? info.architecture : "",
3582
- device: typeof info.device === "string" ? info.device : "",
3583
- description: typeof info.description === "string" ? info.description : ""
3584
- });
3585
- }
3586
- function createGpuAdapterParallelismDiagnostics(adapter, device) {
3587
- return Object.freeze({
3588
- physicalCoreCount: null,
3589
- physicalCoreCountAvailable: false,
3590
- physicalCoreCountUnavailableReason: "WebGPU does not expose physical GPU core counts.",
3591
- adapterInfo: createAdapterInfoSnapshot(adapter),
3592
- adapterLimits: Object.freeze({
3593
- maxComputeInvocationsPerWorkgroup: readGpuLimit(adapter, device, "maxComputeInvocationsPerWorkgroup"),
3594
- maxComputeWorkgroupSizeX: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeX"),
3595
- maxComputeWorkgroupSizeY: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeY"),
3596
- maxComputeWorkgroupSizeZ: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeZ"),
3597
- maxComputeWorkgroupsPerDimension: readGpuLimit(adapter, device, "maxComputeWorkgroupsPerDimension"),
3598
- maxStorageBuffersPerShaderStage: readGpuLimit(adapter, device, "maxStorageBuffersPerShaderStage"),
3599
- maxStorageBufferBindingSize: readGpuLimit(adapter, device, "maxStorageBufferBindingSize")
3600
- }),
3601
- configuredWorkgroupSize: WORKGROUP_SIZE
3602
- });
3603
- }
3604
- function createGpuParallelismCounters() {
3605
- return {
3606
- directDispatches: 0,
3607
- directWorkgroups: 0,
3608
- directShaderInvocations: 0,
3609
- multiWorkgroupDispatches: 0,
3610
- largestDirectWorkgroupsPerDispatch: 0,
3611
- indirectDispatches: 0,
3612
- estimatedIndirectWorkgroupsUpperBound: 0,
3613
- estimatedIndirectShaderInvocationsUpperBound: 0,
3614
- indirectDispatchesWithMultiWorkgroupCapacity: 0,
3615
- largestEstimatedIndirectWorkgroupsPerDispatch: 0
3616
- };
3617
- }
3618
- function countDispatchWorkgroups(groups) {
3619
- return groups.reduce((product, value) => {
3620
- const numeric = Number(value ?? 1);
3621
- const count = Number.isFinite(numeric) ? Math.max(1, Math.trunc(numeric)) : 1;
3622
- return product * count;
3623
- }, 1);
3624
- }
3625
- function recordDirectDispatch(parallelism, groups, invocationsPerWorkgroup = WORKGROUP_SIZE) {
3626
- const workgroups = countDispatchWorkgroups(groups);
3627
- parallelism.directDispatches += 1;
3628
- parallelism.directWorkgroups += workgroups;
3629
- parallelism.directShaderInvocations += workgroups * invocationsPerWorkgroup;
3630
- parallelism.largestDirectWorkgroupsPerDispatch = Math.max(
3631
- parallelism.largestDirectWorkgroupsPerDispatch,
3632
- workgroups
3633
- );
3634
- if (workgroups > 1) {
3635
- parallelism.multiWorkgroupDispatches += 1;
3636
- }
3637
- }
3638
- function recordIndirectDispatch(parallelism, estimatedWorkgroupsUpperBound, invocationsPerWorkgroup = WORKGROUP_SIZE) {
3639
- const workgroups = Math.max(1, Math.trunc(Number(estimatedWorkgroupsUpperBound) || 1));
3640
- parallelism.indirectDispatches += 1;
3641
- parallelism.estimatedIndirectWorkgroupsUpperBound += workgroups;
3642
- parallelism.estimatedIndirectShaderInvocationsUpperBound += workgroups * invocationsPerWorkgroup;
3643
- parallelism.largestEstimatedIndirectWorkgroupsPerDispatch = Math.max(
3644
- parallelism.largestEstimatedIndirectWorkgroupsPerDispatch,
3645
- workgroups
3646
- );
3647
- if (workgroups > 1) {
3648
- parallelism.indirectDispatchesWithMultiWorkgroupCapacity += 1;
3649
- }
3650
- }
3651
- function createGpuParallelismDiagnostics(adapterDiagnostics, counters) {
3652
- const totalEstimatedWorkgroupsUpperBound = counters.directWorkgroups + counters.estimatedIndirectWorkgroupsUpperBound;
3653
- const totalEstimatedShaderInvocationsUpperBound = counters.directShaderInvocations + counters.estimatedIndirectShaderInvocationsUpperBound;
3654
- const exposesMultiWorkgroupParallelism = counters.multiWorkgroupDispatches > 0 || counters.indirectDispatchesWithMultiWorkgroupCapacity > 0;
3655
- return Object.freeze({
3656
- ...adapterDiagnostics,
3657
- directDispatches: counters.directDispatches,
3658
- directWorkgroups: counters.directWorkgroups,
3659
- directShaderInvocations: counters.directShaderInvocations,
3660
- multiWorkgroupDispatches: counters.multiWorkgroupDispatches,
3661
- largestDirectWorkgroupsPerDispatch: counters.largestDirectWorkgroupsPerDispatch,
3662
- indirectDispatches: counters.indirectDispatches,
3663
- estimatedIndirectWorkgroupsUpperBound: counters.estimatedIndirectWorkgroupsUpperBound,
3664
- estimatedIndirectShaderInvocationsUpperBound: counters.estimatedIndirectShaderInvocationsUpperBound,
3665
- indirectDispatchesWithMultiWorkgroupCapacity: counters.indirectDispatchesWithMultiWorkgroupCapacity,
3666
- largestEstimatedIndirectWorkgroupsPerDispatch: counters.largestEstimatedIndirectWorkgroupsPerDispatch,
3667
- totalEstimatedWorkgroupsUpperBound,
3668
- totalEstimatedShaderInvocationsUpperBound,
3669
- exposesMultiWorkgroupParallelism,
3670
- likelyUsesMoreThanOnePhysicalGpuCore: null,
3671
- coreUtilizationStatus: "not-exposed-by-webgpu"
5437
+ function createAdapterInfoSnapshot(adapter) {
5438
+ const info = adapter?.info;
5439
+ if (!info || typeof info !== "object") {
5440
+ return null;
5441
+ }
5442
+ return Object.freeze({
5443
+ vendor: typeof info.vendor === "string" ? info.vendor : "",
5444
+ architecture: typeof info.architecture === "string" ? info.architecture : "",
5445
+ device: typeof info.device === "string" ? info.device : "",
5446
+ description: typeof info.description === "string" ? info.description : ""
5447
+ });
5448
+ }
5449
+ function createGpuAdapterParallelismDiagnostics(adapter, device) {
5450
+ return Object.freeze({
5451
+ physicalCoreCount: null,
5452
+ physicalCoreCountAvailable: false,
5453
+ physicalCoreCountUnavailableReason: "WebGPU does not expose physical GPU core counts.",
5454
+ adapterInfo: createAdapterInfoSnapshot(adapter),
5455
+ adapterLimits: Object.freeze({
5456
+ maxComputeInvocationsPerWorkgroup: readGpuLimit(adapter, device, "maxComputeInvocationsPerWorkgroup"),
5457
+ maxComputeWorkgroupSizeX: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeX"),
5458
+ maxComputeWorkgroupSizeY: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeY"),
5459
+ maxComputeWorkgroupSizeZ: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeZ"),
5460
+ maxComputeWorkgroupsPerDimension: readGpuLimit(adapter, device, "maxComputeWorkgroupsPerDimension"),
5461
+ maxStorageBuffersPerShaderStage: readGpuLimit(adapter, device, "maxStorageBuffersPerShaderStage"),
5462
+ maxStorageBufferBindingSize: readGpuLimit(adapter, device, "maxStorageBufferBindingSize")
5463
+ }),
5464
+ configuredWorkgroupSize: WORKGROUP_SIZE
3672
5465
  });
3673
5466
  }
3674
5467
  function createEnvironmentMapSnapshot(environmentMap) {
@@ -3676,12 +5469,38 @@ function createEnvironmentMapSnapshot(environmentMap) {
3676
5469
  enabled: environmentMap.enabled,
3677
5470
  width: environmentMap.width,
3678
5471
  height: environmentMap.height,
5472
+ mipLevelCount: environmentMap.mipLevelCount ?? 1,
3679
5473
  projection: environmentMap.projection,
3680
5474
  intensity: environmentMap.intensity,
3681
5475
  rotationRadians: environmentMap.rotationRadians,
3682
- ambientStrength: environmentMap.ambientStrength
5476
+ ambientStrength: environmentMap.ambientStrength,
5477
+ hasImportanceData: environmentMap.hasImportanceData === true
3683
5478
  });
3684
5479
  }
5480
+ function nowMs() {
5481
+ if (typeof performance !== "undefined" && typeof performance.now === "function") {
5482
+ return performance.now();
5483
+ }
5484
+ return Date.now();
5485
+ }
5486
+ function estimateSubmittedGpuWorkTimeoutMs(config, tileCount, overrideTimeoutMs = null) {
5487
+ if (Number.isFinite(overrideTimeoutMs)) {
5488
+ return Math.max(1, Math.trunc(Number(overrideTimeoutMs)));
5489
+ }
5490
+ const samplesPerPixel = Math.max(
5491
+ 1,
5492
+ Number(config?.renderedSamplesPerPixel ?? config?.samplesPerPixel ?? 1)
5493
+ );
5494
+ const maxDepth = Math.max(1, Number(config?.maxDepth ?? 1));
5495
+ const deferredResolvePasses = config?.deferredPathResolve ? 1 : 0;
5496
+ const denoisePasses = config?.denoise ? samplesPerPixel < 4 ? 2 : 1 : 0;
5497
+ const tiles = Math.max(1, Number(tileCount ?? 1));
5498
+ const estimatedPasses = tiles * (samplesPerPixel * (maxDepth + 1 + deferredResolvePasses) + denoisePasses + 1);
5499
+ return Math.min(
5500
+ GPU_MAX_SUBMITTED_WORK_TIMEOUT_MS,
5501
+ GPU_SUBMITTED_WORK_TIMEOUT_MS + estimatedPasses * 5
5502
+ );
5503
+ }
3685
5504
  async function createWavefrontPathTracingComputeRenderer(options = {}) {
3686
5505
  assertAnalyticDisplayQualityPolicy(options);
3687
5506
  const constants = getGpuUsageConstants();
@@ -3886,6 +5705,60 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3886
5705
  config.environmentMap,
3887
5706
  config.environmentColor
3888
5707
  );
5708
+ const environmentSamplingResource = createEnvironmentSamplingTextureResource(
5709
+ device,
5710
+ constants,
5711
+ config.environmentMap,
5712
+ config.environmentColor
5713
+ );
5714
+ config = Object.freeze({
5715
+ ...config,
5716
+ environmentMap: Object.freeze({
5717
+ ...config.environmentMap,
5718
+ width: environmentMapResource.width,
5719
+ height: environmentMapResource.height,
5720
+ mipLevelCount: environmentMapResource.mipLevelCount,
5721
+ hasImportanceData: environmentSamplingResource.hasImportanceData
5722
+ })
5723
+ });
5724
+ const brdfLutResource = createBrdfLutResource(device, constants);
5725
+ const baseColorAtlasResource = createAtlasTextureResource(
5726
+ device,
5727
+ constants,
5728
+ config.gpuMaterialSource.baseColorAtlas,
5729
+ "plasius.wavefront.materialAtlas.baseColor"
5730
+ );
5731
+ const metallicRoughnessAtlasResource = createAtlasTextureResource(
5732
+ device,
5733
+ constants,
5734
+ config.gpuMaterialSource.metallicRoughnessAtlas,
5735
+ "plasius.wavefront.materialAtlas.metallicRoughness"
5736
+ );
5737
+ const normalAtlasResource = createAtlasTextureResource(
5738
+ device,
5739
+ constants,
5740
+ config.gpuMaterialSource.normalAtlas,
5741
+ "plasius.wavefront.materialAtlas.normal"
5742
+ );
5743
+ const occlusionAtlasResource = createAtlasTextureResource(
5744
+ device,
5745
+ constants,
5746
+ config.gpuMaterialSource.occlusionAtlas,
5747
+ "plasius.wavefront.materialAtlas.occlusion"
5748
+ );
5749
+ const emissiveAtlasResource = createAtlasTextureResource(
5750
+ device,
5751
+ constants,
5752
+ config.gpuMaterialSource.emissiveAtlas,
5753
+ "plasius.wavefront.materialAtlas.emissive"
5754
+ );
5755
+ const materialAtlasSampler = device.createSampler({
5756
+ label: "plasius.wavefront.materialAtlasSampler",
5757
+ addressModeU: "clamp-to-edge",
5758
+ addressModeV: "clamp-to-edge",
5759
+ magFilter: "linear",
5760
+ minFilter: "linear"
5761
+ });
3889
5762
  const traceBindGroupLayout = device.createBindGroupLayout({
3890
5763
  label: "plasius.wavefront.traceBindGroupLayout",
3891
5764
  entries: [
@@ -3915,7 +5788,16 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3915
5788
  { binding: 19, visibility: constants.shader.COMPUTE, buffer: { type: "read-only-storage" } },
3916
5789
  { binding: 20, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
3917
5790
  { binding: 21, visibility: constants.shader.COMPUTE, sampler: { type: "filtering" } },
3918
- { binding: 22, visibility: constants.shader.COMPUTE, buffer: { type: "storage" } }
5791
+ { binding: 22, visibility: constants.shader.COMPUTE, buffer: { type: "storage" } },
5792
+ { binding: 23, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
5793
+ { binding: 24, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
5794
+ { binding: 25, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
5795
+ { binding: 26, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
5796
+ { binding: 27, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
5797
+ { binding: 28, visibility: constants.shader.COMPUTE, sampler: { type: "filtering" } },
5798
+ { binding: 29, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
5799
+ { binding: 30, visibility: constants.shader.COMPUTE, sampler: { type: "filtering" } },
5800
+ { binding: 31, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } }
3919
5801
  ]
3920
5802
  });
3921
5803
  const accelerationBindGroupLayout = device.createBindGroupLayout({
@@ -3994,6 +5876,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3994
5876
  label: "plasius.wavefront.computeShader",
3995
5877
  code: WAVEFRONT_COMPUTE_WGSL
3996
5878
  });
5879
+ await assertShaderModuleCompiles(computeShader, "plasius.wavefront.computeShader");
3997
5880
  const pipelines = {
3998
5881
  prepareMeshTrianglesAndLeaves: await createComputePipeline(
3999
5882
  device,
@@ -4092,7 +5975,16 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4092
5975
  { binding: 19, resource: { buffer: environmentPortalBuffer } },
4093
5976
  { binding: 20, resource: environmentMapResource.view },
4094
5977
  { binding: 21, resource: environmentMapResource.sampler },
4095
- { binding: 22, resource: { buffer: pathVertexBuffer } }
5978
+ { binding: 22, resource: { buffer: pathVertexBuffer } },
5979
+ { binding: 23, resource: baseColorAtlasResource.view },
5980
+ { binding: 24, resource: metallicRoughnessAtlasResource.view },
5981
+ { binding: 25, resource: normalAtlasResource.view },
5982
+ { binding: 26, resource: occlusionAtlasResource.view },
5983
+ { binding: 27, resource: emissiveAtlasResource.view },
5984
+ { binding: 28, resource: materialAtlasSampler },
5985
+ { binding: 29, resource: brdfLutResource.view },
5986
+ { binding: 30, resource: brdfLutResource.sampler },
5987
+ { binding: 31, resource: environmentSamplingResource.view }
4096
5988
  ]
4097
5989
  });
4098
5990
  }
@@ -4145,6 +6037,11 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4145
6037
  outputView,
4146
6038
  "plasius.wavefront.bind.denoise.scratchToOutput"
4147
6039
  );
6040
+ const denoiseDirectResolveBindGroup = createDenoiseResolveBindGroup(
6041
+ radianceView,
6042
+ outputView,
6043
+ "plasius.wavefront.bind.denoise.radianceToOutput"
6044
+ );
4148
6045
  const presentBindGroupLayout = device.createBindGroupLayout({
4149
6046
  label: "plasius.wavefront.presentBindGroupLayout",
4150
6047
  entries: [
@@ -4182,23 +6079,128 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4182
6079
  let accelerationBuilt = !config.gpuAccelerationBuildRequired;
4183
6080
  let accelerationBuildCount = 0;
4184
6081
  let activeCameraOptions = options.camera ?? DEFAULT_CAMERA;
6082
+ let lastCompletedFrameTimeMs = null;
6083
+ let lastCompletedSamplesPerPixel = Math.max(1, config.samplesPerPixel);
4185
6084
  let lastGpuParallelism = createGpuParallelismDiagnostics(
4186
6085
  gpuAdapterParallelism,
4187
6086
  createGpuParallelismCounters()
4188
6087
  );
6088
+ function resolveRenderedSamplesPerPixel(renderOptions = {}, awaitGPUCompletion = true) {
6089
+ const targetSamplesPerPixel = clamp(
6090
+ readPositiveInteger(
6091
+ "samplesPerPixel",
6092
+ renderOptions.samplesPerPixel,
6093
+ config.samplesPerPixel
6094
+ ),
6095
+ 1,
6096
+ config.samplesPerPixel
6097
+ );
6098
+ const frameTimeBudgetMs = Number.isFinite(renderOptions.frameTimeBudgetMs) ? Math.max(0, Number(renderOptions.frameTimeBudgetMs)) : null;
6099
+ const minimumSamplesPerPixel = clamp(
6100
+ readPositiveInteger(
6101
+ "minimumSamplesPerPixel",
6102
+ renderOptions.minimumSamplesPerPixel,
6103
+ frameTimeBudgetMs !== null && targetSamplesPerPixel > 1 ? 1 : targetSamplesPerPixel
6104
+ ),
6105
+ 1,
6106
+ targetSamplesPerPixel
6107
+ );
6108
+ if (frameTimeBudgetMs === null || !awaitGPUCompletion || targetSamplesPerPixel <= minimumSamplesPerPixel) {
6109
+ return Object.freeze({
6110
+ renderedSamplesPerPixel: targetSamplesPerPixel,
6111
+ targetSamplesPerPixel,
6112
+ minimumSamplesPerPixel,
6113
+ frameTimeBudgetMs,
6114
+ budgetConstrained: false
6115
+ });
6116
+ }
6117
+ const estimatedSampleTimeMs = Number.isFinite(lastCompletedFrameTimeMs) && lastCompletedFrameTimeMs > 0 ? lastCompletedFrameTimeMs / Math.max(1, lastCompletedSamplesPerPixel) : null;
6118
+ if (!Number.isFinite(estimatedSampleTimeMs) || estimatedSampleTimeMs <= 0) {
6119
+ return Object.freeze({
6120
+ renderedSamplesPerPixel: minimumSamplesPerPixel,
6121
+ targetSamplesPerPixel,
6122
+ minimumSamplesPerPixel,
6123
+ frameTimeBudgetMs,
6124
+ budgetConstrained: minimumSamplesPerPixel < targetSamplesPerPixel
6125
+ });
6126
+ }
6127
+ const budgetLimitedSamples = clamp(
6128
+ Math.floor(frameTimeBudgetMs / estimatedSampleTimeMs),
6129
+ minimumSamplesPerPixel,
6130
+ targetSamplesPerPixel
6131
+ );
6132
+ return Object.freeze({
6133
+ renderedSamplesPerPixel: budgetLimitedSamples,
6134
+ targetSamplesPerPixel,
6135
+ minimumSamplesPerPixel,
6136
+ frameTimeBudgetMs,
6137
+ budgetConstrained: budgetLimitedSamples < targetSamplesPerPixel
6138
+ });
6139
+ }
6140
+ function createFrameStats({
6141
+ frameIndex,
6142
+ accelerationBuildSubmitted,
6143
+ frameSubmissionCount,
6144
+ parallelismCounters,
6145
+ renderedSamplesPerPixel,
6146
+ targetSamplesPerPixel,
6147
+ frameTimeBudgetMs,
6148
+ budgetConstrained
6149
+ }) {
6150
+ lastGpuParallelism = createGpuParallelismDiagnostics(gpuAdapterParallelism, parallelismCounters);
6151
+ const commandSubmissions = frameSubmissionCount + (accelerationBuildSubmitted ? 1 : 0);
6152
+ return Object.freeze({
6153
+ frame,
6154
+ frameIndex,
6155
+ width: config.width,
6156
+ height: config.height,
6157
+ maxDepth: config.maxDepth,
6158
+ tiles: tiles.length,
6159
+ tileSize: config.tileSize,
6160
+ samplesPerPixel: targetSamplesPerPixel,
6161
+ renderedSamplesPerPixel,
6162
+ frameTimeBudgetMs,
6163
+ budgetConstrained,
6164
+ maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
6165
+ screenRays: config.width * config.height,
6166
+ primaryRays: config.width * config.height * renderedSamplesPerPixel,
6167
+ sceneObjectCount: config.sceneObjectCount,
6168
+ triangleCount: config.triangleCount,
6169
+ emissiveTriangleCount: config.emissiveTriangleCount,
6170
+ environmentPortalCount: config.environmentPortalCount,
6171
+ environmentPortalMode: config.environmentPortalMode,
6172
+ environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
6173
+ deferredPathResolve: config.deferredPathResolve,
6174
+ bvhNodeCount: config.bvhNodeCount,
6175
+ displayQuality: config.displayQuality,
6176
+ accelerationBuildMode: config.accelerationBuildMode,
6177
+ gpuAccelerationBuildRequired: config.gpuAccelerationBuildRequired,
6178
+ accelerationBuildSubmitted,
6179
+ accelerationBuilt,
6180
+ accelerationBuildCount,
6181
+ commandSubmissions,
6182
+ frameConfigSlots: frameConfigSlotCount,
6183
+ gpuParallelism: lastGpuParallelism,
6184
+ memory: config.memory
6185
+ });
6186
+ }
6187
+ function writeFrameConfigSlot(slot, tile, frameIndex, buildRange = {}) {
6188
+ if (slot >= frameConfigSlotCount) {
6189
+ throw new Error("Wavefront frame config slot capacity exceeded.");
6190
+ }
6191
+ const offset = slot * configBufferStride;
6192
+ device.queue.writeBuffer(
6193
+ configBuffer,
6194
+ offset,
6195
+ createConfigPayload(config, tile, frameIndex, buildRange)
6196
+ );
6197
+ return offset;
6198
+ }
4189
6199
  function createFrameConfigWriter(frameIndex) {
4190
6200
  let slot = 0;
4191
6201
  return (tile, buildRange = {}) => {
4192
- if (slot >= frameConfigSlotCount) {
4193
- throw new Error("Wavefront frame config slot capacity exceeded.");
4194
- }
4195
- const offset = slot * configBufferStride;
6202
+ const offset = writeFrameConfigSlot(slot, tile, frameIndex, buildRange);
4196
6203
  slot += 1;
4197
- device.queue.writeBuffer(
4198
- configBuffer,
4199
- offset,
4200
- createConfigPayload(config, tile, frameIndex, buildRange)
4201
- );
4202
6204
  return offset;
4203
6205
  };
4204
6206
  }
@@ -4243,7 +6245,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4243
6245
  passEncoder.setPipeline(pipelines.prepareMeshTrianglesAndLeaves);
4244
6246
  const prepareWorkgroups = Math.ceil(config.bvhLeafSortCapacity / WORKGROUP_SIZE);
4245
6247
  passEncoder.dispatchWorkgroups(prepareWorkgroups);
4246
- recordDirectDispatch(parallelism, [prepareWorkgroups]);
6248
+ recordDirectDispatch(parallelism, [prepareWorkgroups], WORKGROUP_SIZE);
4247
6249
  passEncoder.setPipeline(pipelines.sortBvhLeafRefs);
4248
6250
  for (let stageIndex = 0; stageIndex < config.bvhSortStages.length; stageIndex += 1) {
4249
6251
  passEncoder.setBindGroup(0, bvhBuildBindGroup, [
@@ -4251,13 +6253,13 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4251
6253
  ]);
4252
6254
  const sortWorkgroups = Math.ceil(config.bvhLeafSortCapacity / WORKGROUP_SIZE);
4253
6255
  passEncoder.dispatchWorkgroups(sortWorkgroups);
4254
- recordDirectDispatch(parallelism, [sortWorkgroups]);
6256
+ recordDirectDispatch(parallelism, [sortWorkgroups], WORKGROUP_SIZE);
4255
6257
  }
4256
6258
  passEncoder.setBindGroup(0, bvhBuildBindGroup, [0]);
4257
6259
  passEncoder.setPipeline(pipelines.writeSortedBvhLeaves);
4258
6260
  const leafWriteWorkgroups = Math.ceil(config.triangleCount / WORKGROUP_SIZE);
4259
6261
  passEncoder.dispatchWorkgroups(leafWriteWorkgroups);
4260
- recordDirectDispatch(parallelism, [leafWriteWorkgroups]);
6262
+ recordDirectDispatch(parallelism, [leafWriteWorkgroups], WORKGROUP_SIZE);
4261
6263
  passEncoder.setPipeline(pipelines.buildBvhInternalLevel);
4262
6264
  for (let levelIndex = 0; levelIndex < config.bvhBuildLevels.length; levelIndex += 1) {
4263
6265
  const buildLevel = config.bvhBuildLevels[levelIndex];
@@ -4266,7 +6268,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4266
6268
  ]);
4267
6269
  const levelWorkgroups = Math.ceil(buildLevel.count / WORKGROUP_SIZE);
4268
6270
  passEncoder.dispatchWorkgroups(levelWorkgroups);
4269
- recordDirectDispatch(parallelism, [levelWorkgroups]);
6271
+ recordDirectDispatch(parallelism, [levelWorkgroups], WORKGROUP_SIZE);
4270
6272
  }
4271
6273
  passEncoder.end();
4272
6274
  device.queue.submit([encoder.finish()]);
@@ -4282,7 +6284,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4282
6284
  generatePass.setBindGroup(0, bindGroups[0], [configOffset]);
4283
6285
  generatePass.setPipeline(pipelines.generatePrimaryRays);
4284
6286
  generatePass.dispatchWorkgroups(tileWorkgroups);
4285
- recordDirectDispatch(parallelism, [tileWorkgroups]);
6287
+ recordDirectDispatch(parallelism, [tileWorkgroups], WORKGROUP_SIZE);
4286
6288
  generatePass.end();
4287
6289
  for (let bounceIndex = 0; bounceIndex < config.maxDepth; bounceIndex += 1) {
4288
6290
  encoder.copyBufferToBuffer(
@@ -4298,10 +6300,10 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4298
6300
  passEncoder.setBindGroup(0, bindGroups[bounceIndex % 2], [configOffset]);
4299
6301
  passEncoder.setPipeline(pipelines.intersectActiveQueue);
4300
6302
  passEncoder.dispatchWorkgroupsIndirect(activeDispatchBuffer, 0);
4301
- recordIndirectDispatch(parallelism, tileWorkgroups);
6303
+ recordIndirectDispatch(parallelism, tileWorkgroups, WORKGROUP_SIZE);
4302
6304
  passEncoder.setPipeline(pipelines.resolveSurfaceRecords);
4303
6305
  passEncoder.dispatchWorkgroupsIndirect(activeDispatchBuffer, 0);
4304
- recordIndirectDispatch(parallelism, tileWorkgroups);
6306
+ recordIndirectDispatch(parallelism, tileWorkgroups, WORKGROUP_SIZE);
4305
6307
  passEncoder.setPipeline(pipelines.compactAndSwapQueues);
4306
6308
  passEncoder.dispatchWorkgroups(1);
4307
6309
  recordDirectDispatch(parallelism, [1], 1);
@@ -4316,30 +6318,45 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4316
6318
  passEncoder.setBindGroup(0, bindGroups[0], [configOffset]);
4317
6319
  passEncoder.setPipeline(pipelines.accumulateTerminalRadiance);
4318
6320
  passEncoder.dispatchWorkgroups(tileWorkgroups);
4319
- recordDirectDispatch(parallelism, [tileWorkgroups]);
6321
+ recordDirectDispatch(parallelism, [tileWorkgroups], WORKGROUP_SIZE);
4320
6322
  passEncoder.end();
4321
6323
  }
4322
- function encodeDenoise(encoder, configOffset, parallelism) {
6324
+ function encodeDenoise(encoder, configOffset, parallelism, renderedSamplesPerPixel = config.samplesPerPixel) {
4323
6325
  if (!config.denoise) {
4324
6326
  return;
4325
6327
  }
4326
6328
  const denoiseWorkgroupsX = Math.ceil(config.width / 8);
4327
6329
  const denoiseWorkgroupsY = Math.ceil(config.height / 8);
4328
- const radiancePass = encoder.beginComputePass({
4329
- label: "plasius.wavefront.denoiseRadiancePass"
4330
- });
4331
- radiancePass.setBindGroup(0, denoiseRadianceBindGroup, [configOffset]);
4332
- radiancePass.setPipeline(pipelines.denoiseLinearRadiance);
4333
- radiancePass.dispatchWorkgroups(denoiseWorkgroupsX, denoiseWorkgroupsY);
4334
- recordDirectDispatch(parallelism, [denoiseWorkgroupsX, denoiseWorkgroupsY]);
4335
- radiancePass.end();
6330
+ const useTwoPassDenoise = renderedSamplesPerPixel < 4;
6331
+ if (useTwoPassDenoise) {
6332
+ const radiancePass = encoder.beginComputePass({
6333
+ label: "plasius.wavefront.denoiseRadiancePass"
6334
+ });
6335
+ radiancePass.setBindGroup(0, denoiseRadianceBindGroup, [configOffset]);
6336
+ radiancePass.setPipeline(pipelines.denoiseLinearRadiance);
6337
+ radiancePass.dispatchWorkgroups(denoiseWorkgroupsX, denoiseWorkgroupsY);
6338
+ recordDirectDispatch(
6339
+ parallelism,
6340
+ [denoiseWorkgroupsX, denoiseWorkgroupsY],
6341
+ WORKGROUP_SIZE
6342
+ );
6343
+ radiancePass.end();
6344
+ }
4336
6345
  const resolvePass = encoder.beginComputePass({
4337
6346
  label: "plasius.wavefront.denoiseResolvePass"
4338
6347
  });
4339
- resolvePass.setBindGroup(0, denoiseResolveBindGroup, [configOffset]);
6348
+ resolvePass.setBindGroup(
6349
+ 0,
6350
+ useTwoPassDenoise ? denoiseResolveBindGroup : denoiseDirectResolveBindGroup,
6351
+ [configOffset]
6352
+ );
4340
6353
  resolvePass.setPipeline(pipelines.resolveDenoisedOutputImage);
4341
6354
  resolvePass.dispatchWorkgroups(denoiseWorkgroupsX, denoiseWorkgroupsY);
4342
- recordDirectDispatch(parallelism, [denoiseWorkgroupsX, denoiseWorkgroupsY]);
6355
+ recordDirectDispatch(
6356
+ parallelism,
6357
+ [denoiseWorkgroupsX, denoiseWorkgroupsY],
6358
+ WORKGROUP_SIZE
6359
+ );
4343
6360
  resolvePass.end();
4344
6361
  }
4345
6362
  function encodePresent(encoder) {
@@ -4360,98 +6377,213 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4360
6377
  passEncoder.draw(3);
4361
6378
  passEncoder.end();
4362
6379
  }
4363
- function dispatchFrame(frameIndex, parallelism) {
6380
+ function dispatchFrame(frameIndex, parallelism, renderedSamplesPerPixel = config.samplesPerPixel) {
4364
6381
  const writeFrameConfig = createFrameConfigWriter(frameIndex);
4365
- let submissionCount = 0;
4366
- let encodedFramePasses = 0;
4367
- let encoder = device.createCommandEncoder({
4368
- label: `plasius.wavefront.frame.${frameIndex}.batched.${submissionCount + 1}`
6382
+ const batch = createGpuSubmissionBatcher({
6383
+ device,
6384
+ frameIndex,
6385
+ maxFramePassesPerSubmission: config.maxFramePassesPerSubmission
4369
6386
  });
4370
- function submitCurrentEncoder() {
4371
- if (encodedFramePasses <= 0) {
4372
- return;
4373
- }
4374
- device.queue.submit([encoder.finish()]);
4375
- submissionCount += 1;
4376
- encodedFramePasses = 0;
4377
- encoder = device.createCommandEncoder({
4378
- label: `plasius.wavefront.frame.${frameIndex}.batched.${submissionCount + 1}`
4379
- });
4380
- }
4381
- function reserveEncoder(passCount = 1) {
4382
- if (encodedFramePasses > 0 && encodedFramePasses + passCount > config.maxFramePassesPerSubmission) {
4383
- submitCurrentEncoder();
4384
- }
4385
- encodedFramePasses += passCount;
4386
- return encoder;
4387
- }
4388
6387
  for (const tile of tiles) {
4389
- for (let sampleIndex = 0; sampleIndex < config.samplesPerPixel; sampleIndex += 1) {
6388
+ for (let sampleIndex = 0; sampleIndex < renderedSamplesPerPixel; sampleIndex += 1) {
4390
6389
  const configOffset = writeFrameConfig(tile, {
4391
6390
  sampleIndex,
4392
- sampleWeight: 1 / config.samplesPerPixel
6391
+ sampleWeight: 1 / renderedSamplesPerPixel
4393
6392
  });
4394
- encodeTileSample(reserveEncoder(), tile, configOffset, parallelism);
6393
+ encodeTileSample(
6394
+ batch.reserve(config.maxDepth + 1),
6395
+ tile,
6396
+ configOffset,
6397
+ parallelism
6398
+ );
4395
6399
  if (config.deferredPathResolve) {
4396
- encodeTileOutput(reserveEncoder(), tile, configOffset, parallelism);
6400
+ encodeTileOutput(batch.reserve(1), tile, configOffset, parallelism);
4397
6401
  }
4398
6402
  }
4399
6403
  if (!config.deferredPathResolve) {
4400
6404
  const outputConfigOffset = writeFrameConfig(tile, {
4401
6405
  sampleIndex: 0,
4402
- sampleWeight: 1 / config.samplesPerPixel
6406
+ sampleWeight: 1 / renderedSamplesPerPixel
4403
6407
  });
4404
- encodeTileOutput(reserveEncoder(), tile, outputConfigOffset, parallelism);
6408
+ encodeTileOutput(batch.reserve(1), tile, outputConfigOffset, parallelism);
4405
6409
  }
4406
6410
  }
4407
6411
  if (config.denoise) {
4408
6412
  const denoiseConfigOffset = writeFrameConfig(
4409
6413
  { x: 0, y: 0, width: config.width, height: config.height },
4410
- { sampleIndex: 0, sampleWeight: 1 / config.samplesPerPixel }
6414
+ { sampleIndex: 0, sampleWeight: 1 / renderedSamplesPerPixel }
6415
+ );
6416
+ const denoisePassCount = renderedSamplesPerPixel < 4 ? 2 : 1;
6417
+ encodeDenoise(
6418
+ batch.reserve(denoisePassCount),
6419
+ denoiseConfigOffset,
6420
+ parallelism,
6421
+ renderedSamplesPerPixel
4411
6422
  );
4412
- encodeDenoise(reserveEncoder(), denoiseConfigOffset, parallelism);
4413
6423
  }
4414
- encodePresent(reserveEncoder());
4415
- submitCurrentEncoder();
4416
- return submissionCount;
6424
+ encodePresent(batch.reserve(1));
6425
+ return batch.flush();
4417
6426
  }
4418
- function renderOnce() {
6427
+ function renderOnce(renderOptions = {}, resolvedSamplingPlan = null) {
6428
+ const frameStartTimeMs = nowMs();
4419
6429
  frame += 1;
4420
6430
  const frameIndex = frame + config.frameIndex;
6431
+ const samplingPlan = resolvedSamplingPlan ?? resolveRenderedSamplesPerPixel(renderOptions, false);
4421
6432
  const parallelismCounters = createGpuParallelismCounters();
4422
6433
  const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex, parallelismCounters);
4423
- const frameSubmissionCount = dispatchFrame(frameIndex, parallelismCounters);
4424
- lastGpuParallelism = createGpuParallelismDiagnostics(gpuAdapterParallelism, parallelismCounters);
6434
+ const frameSubmissionCount = dispatchFrame(
6435
+ frameIndex,
6436
+ parallelismCounters,
6437
+ samplingPlan.renderedSamplesPerPixel
6438
+ );
6439
+ const frameTimeMs = Math.max(0, nowMs() - frameStartTimeMs);
4425
6440
  return Object.freeze({
4426
- frame,
4427
- width: config.width,
4428
- height: config.height,
4429
- maxDepth: config.maxDepth,
4430
- tiles: tiles.length,
4431
- tileSize: config.tileSize,
4432
- samplesPerPixel: config.samplesPerPixel,
6441
+ ...createFrameStats({
6442
+ frameIndex,
6443
+ accelerationBuildSubmitted,
6444
+ frameSubmissionCount,
6445
+ parallelismCounters,
6446
+ renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel,
6447
+ targetSamplesPerPixel: samplingPlan.targetSamplesPerPixel,
6448
+ frameTimeBudgetMs: samplingPlan.frameTimeBudgetMs,
6449
+ budgetConstrained: samplingPlan.budgetConstrained
6450
+ }),
6451
+ gpuWorkerJobs: createGpuWorkerJobDiagnostics(
6452
+ lastGpuParallelism,
6453
+ frameSubmissionCount + (accelerationBuildSubmitted ? 1 : 0),
6454
+ frameTimeMs,
6455
+ false
6456
+ )
6457
+ });
6458
+ }
6459
+ async function waitForSubmittedGpuWork(options2 = {}) {
6460
+ if (typeof device.queue.onSubmittedWorkDone !== "function") {
6461
+ return true;
6462
+ }
6463
+ const timeoutMs = Math.max(
6464
+ 1,
6465
+ Number.isFinite(options2.timeoutMs) ? Number(options2.timeoutMs) : GPU_SUBMITTED_WORK_TIMEOUT_MS
6466
+ );
6467
+ const allowTimeout = options2.allowTimeout !== false;
6468
+ const completionPromise = device.queue.onSubmittedWorkDone().then(
6469
+ () => ({ status: "done" }),
6470
+ (error) => {
6471
+ throw error;
6472
+ }
6473
+ );
6474
+ const lossPromise = typeof device.lost?.then === "function" ? device.lost.then((info) => {
6475
+ throw new Error(
6476
+ `WebGPU device lost while waiting for submitted work (${info?.reason ?? "unknown"}).`
6477
+ );
6478
+ }) : null;
6479
+ let timeoutHandle = null;
6480
+ let resolveTimeoutPromise = null;
6481
+ let timeoutSettled = false;
6482
+ const settleTimeoutPromise = (value) => {
6483
+ if (timeoutSettled) {
6484
+ return;
6485
+ }
6486
+ timeoutSettled = true;
6487
+ resolveTimeoutPromise?.(value);
6488
+ };
6489
+ const timeoutPromise = new Promise((resolve) => {
6490
+ resolveTimeoutPromise = resolve;
6491
+ timeoutHandle = setTimeout(() => settleTimeoutPromise({ status: "timeout" }), timeoutMs);
6492
+ });
6493
+ let result;
6494
+ try {
6495
+ result = await Promise.race(
6496
+ [completionPromise, timeoutPromise, lossPromise].filter(Boolean)
6497
+ );
6498
+ } finally {
6499
+ if (timeoutHandle !== null) {
6500
+ clearTimeout(timeoutHandle);
6501
+ settleTimeoutPromise({ status: "cancelled" });
6502
+ }
6503
+ }
6504
+ if (result?.status === "timeout") {
6505
+ if (!allowTimeout) {
6506
+ throw new Error(`Timed out after ${timeoutMs} ms waiting for submitted GPU work.`);
6507
+ }
6508
+ console.warn(
6509
+ `[plasius.wavefront] Submitted GPU work did not report completion within ${timeoutMs} ms; continuing.`
6510
+ );
6511
+ return false;
6512
+ }
6513
+ return true;
6514
+ }
6515
+ function dispatchFrameAwaitingGpu(frameIndex, parallelism, renderedSamplesPerPixel = config.samplesPerPixel) {
6516
+ const samplePassesPerSample = config.maxDepth + 1 + (config.deferredPathResolve ? 1 : 0);
6517
+ const denoisePassCount = config.denoise ? renderedSamplesPerPixel < 4 ? 2 : 1 : 0;
6518
+ const tailPassCount = denoisePassCount + 1;
6519
+ const sampleBatchSize = Math.max(
6520
+ 1,
6521
+ Math.floor(
6522
+ Math.max(config.maxFramePassesPerSubmission - tailPassCount, 1) / Math.max(samplePassesPerSample, 1)
6523
+ )
6524
+ );
6525
+ let submissionCount = 0;
6526
+ for (const tile of tiles) {
6527
+ for (let sampleStart = 0; sampleStart < renderedSamplesPerPixel; sampleStart += sampleBatchSize) {
6528
+ const sampleEnd = Math.min(renderedSamplesPerPixel, sampleStart + sampleBatchSize);
6529
+ const batch = createGpuSubmissionBatcher({
6530
+ device,
6531
+ frameIndex,
6532
+ maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
6533
+ startingSubmissionCount: submissionCount
6534
+ });
6535
+ let slot = 0;
6536
+ for (let sampleIndex = sampleStart; sampleIndex < sampleEnd; sampleIndex += 1) {
6537
+ const configOffset = writeFrameConfigSlot(slot, tile, frameIndex, {
6538
+ sampleIndex,
6539
+ sampleWeight: 1 / renderedSamplesPerPixel
6540
+ });
6541
+ slot += 1;
6542
+ encodeTileSample(
6543
+ batch.reserve(config.maxDepth + 1),
6544
+ tile,
6545
+ configOffset,
6546
+ parallelism
6547
+ );
6548
+ if (config.deferredPathResolve) {
6549
+ encodeTileOutput(batch.reserve(1), tile, configOffset, parallelism);
6550
+ }
6551
+ }
6552
+ if (!config.deferredPathResolve && sampleEnd >= renderedSamplesPerPixel) {
6553
+ const outputConfigOffset = writeFrameConfigSlot(slot, tile, frameIndex, {
6554
+ sampleIndex: 0,
6555
+ sampleWeight: 1 / renderedSamplesPerPixel
6556
+ });
6557
+ encodeTileOutput(batch.reserve(1), tile, outputConfigOffset, parallelism);
6558
+ }
6559
+ batch.flush();
6560
+ submissionCount += batch.getSubmissionCount();
6561
+ }
6562
+ }
6563
+ const tail = createGpuSubmissionBatcher({
6564
+ device,
6565
+ frameIndex,
4433
6566
  maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
4434
- screenRays: config.width * config.height,
4435
- primaryRays: config.width * config.height * config.samplesPerPixel,
4436
- sceneObjectCount: config.sceneObjectCount,
4437
- triangleCount: config.triangleCount,
4438
- emissiveTriangleCount: config.emissiveTriangleCount,
4439
- environmentPortalCount: config.environmentPortalCount,
4440
- environmentPortalMode: config.environmentPortalMode,
4441
- environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
4442
- deferredPathResolve: config.deferredPathResolve,
4443
- bvhNodeCount: config.bvhNodeCount,
4444
- displayQuality: config.displayQuality,
4445
- accelerationBuildMode: config.accelerationBuildMode,
4446
- gpuAccelerationBuildRequired: config.gpuAccelerationBuildRequired,
4447
- accelerationBuildSubmitted,
4448
- accelerationBuilt,
4449
- accelerationBuildCount,
4450
- commandSubmissions: frameSubmissionCount + (accelerationBuildSubmitted ? 1 : 0),
4451
- frameConfigSlots: frameConfigSlotCount,
4452
- gpuParallelism: lastGpuParallelism,
4453
- memory: config.memory
6567
+ startingSubmissionCount: submissionCount
4454
6568
  });
6569
+ if (config.denoise) {
6570
+ const denoiseConfigOffset = writeFrameConfigSlot(
6571
+ 0,
6572
+ { x: 0, y: 0, width: config.width, height: config.height },
6573
+ frameIndex,
6574
+ { sampleIndex: 0, sampleWeight: 1 / renderedSamplesPerPixel }
6575
+ );
6576
+ encodeDenoise(
6577
+ tail.reserve(denoisePassCount),
6578
+ denoiseConfigOffset,
6579
+ parallelism,
6580
+ renderedSamplesPerPixel
6581
+ );
6582
+ }
6583
+ encodePresent(tail.reserve(1));
6584
+ tail.flush();
6585
+ submissionCount += tail.getSubmissionCount();
6586
+ return submissionCount;
4455
6587
  }
4456
6588
  async function readOutputProbe(optionsForProbe = {}) {
4457
6589
  const mapMode = constants.map;
@@ -4465,6 +6597,10 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4465
6597
  size: 256,
4466
6598
  usage: constants.buffer.COPY_DST | constants.buffer.MAP_READ
4467
6599
  });
6600
+ await waitForSubmittedGpuWork({
6601
+ timeoutMs: GPU_READBACK_COMPLETION_TIMEOUT_MS,
6602
+ allowTimeout: false
6603
+ });
4468
6604
  const encoder = device.createCommandEncoder({
4469
6605
  label: "plasius.wavefront.outputProbe.copy"
4470
6606
  });
@@ -4474,6 +6610,10 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4474
6610
  { width: 1, height: 1, depthOrArrayLayers: 1 }
4475
6611
  );
4476
6612
  device.queue.submit([encoder.finish()]);
6613
+ await waitForSubmittedGpuWork({
6614
+ timeoutMs: GPU_READBACK_COMPLETION_TIMEOUT_MS,
6615
+ allowTimeout: false
6616
+ });
4477
6617
  await readback.mapAsync(mapMode.READ);
4478
6618
  const bytes = new Uint8Array(readback.getMappedRange()).slice(0, 4);
4479
6619
  readback.unmap();
@@ -4486,7 +6626,57 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4486
6626
  });
4487
6627
  }
4488
6628
  async function renderFrame(renderOptions = {}) {
4489
- const frameStats = renderOnce();
6629
+ const awaitGPUCompletion = renderOptions.awaitGPUCompletion !== false;
6630
+ const samplingPlan = resolveRenderedSamplesPerPixel(renderOptions, awaitGPUCompletion);
6631
+ const useThrottledHighSamplePath = awaitGPUCompletion && samplingPlan.renderedSamplesPerPixel >= 8;
6632
+ const submittedWorkTimeoutMs = estimateSubmittedGpuWorkTimeoutMs(
6633
+ { ...config, renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel },
6634
+ tiles.length,
6635
+ renderOptions.submittedWorkTimeoutMs
6636
+ );
6637
+ const frameStartTimeMs = nowMs();
6638
+ const submissionWaitOptions = awaitGPUCompletion ? { timeoutMs: submittedWorkTimeoutMs, allowTimeout: false } : { timeoutMs: submittedWorkTimeoutMs };
6639
+ let frameStats;
6640
+ if (useThrottledHighSamplePath) {
6641
+ frame += 1;
6642
+ const frameIndex = frame + config.frameIndex;
6643
+ const parallelismCounters = createGpuParallelismCounters();
6644
+ const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex, parallelismCounters);
6645
+ const frameSubmissionCount = dispatchFrameAwaitingGpu(
6646
+ frameIndex,
6647
+ parallelismCounters,
6648
+ samplingPlan.renderedSamplesPerPixel
6649
+ );
6650
+ frameStats = createFrameStats({
6651
+ frameIndex,
6652
+ accelerationBuildSubmitted,
6653
+ frameSubmissionCount,
6654
+ parallelismCounters,
6655
+ renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel,
6656
+ targetSamplesPerPixel: samplingPlan.targetSamplesPerPixel,
6657
+ frameTimeBudgetMs: samplingPlan.frameTimeBudgetMs,
6658
+ budgetConstrained: samplingPlan.budgetConstrained
6659
+ });
6660
+ } else {
6661
+ frameStats = renderOnce(renderOptions, samplingPlan);
6662
+ }
6663
+ if (awaitGPUCompletion) {
6664
+ await waitForSubmittedGpuWork(submissionWaitOptions);
6665
+ }
6666
+ const frameTimeMs = Math.max(0, nowMs() - frameStartTimeMs);
6667
+ if (awaitGPUCompletion) {
6668
+ lastCompletedFrameTimeMs = frameTimeMs;
6669
+ lastCompletedSamplesPerPixel = frameStats.renderedSamplesPerPixel ?? frameStats.samplesPerPixel;
6670
+ }
6671
+ frameStats = Object.freeze({
6672
+ ...frameStats,
6673
+ gpuWorkerJobs: createGpuWorkerJobDiagnostics(
6674
+ frameStats.gpuParallelism,
6675
+ frameStats.commandSubmissions,
6676
+ frameTimeMs,
6677
+ awaitGPUCompletion
6678
+ )
6679
+ });
4490
6680
  const probe = renderOptions.readOutputProbe === false ? null : await readOutputProbe(renderOptions.probe);
4491
6681
  const maxChannel = probe ? Math.max(...probe.rgba.slice(0, 3)) : 0;
4492
6682
  return Object.freeze({
@@ -4507,10 +6697,8 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4507
6697
  queueOverflow: 0
4508
6698
  });
4509
6699
  }
4510
- function updateSceneObjects(sceneObjects) {
4511
- const nextPackedScene = packWavefrontSceneObjects(sceneObjects, config.sceneObjectCapacity);
4512
- packedScene = nextPackedScene;
4513
- config = createWavefrontPathTracingComputeConfig({
6700
+ function rebuildLiveConfig(overrides = {}) {
6701
+ return createWavefrontPathTracingComputeConfig({
4514
6702
  ...options,
4515
6703
  canvas,
4516
6704
  width: config.width,
@@ -4521,26 +6709,23 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4521
6709
  sceneObjectCapacity: config.sceneObjectCapacity,
4522
6710
  sceneObjects: packedScene.objects,
4523
6711
  camera: activeCameraOptions,
4524
- frameIndex: config.frameIndex
6712
+ environmentMap: {
6713
+ ...config.environmentMap
6714
+ },
6715
+ frameIndex: config.frameIndex,
6716
+ ...overrides
4525
6717
  });
6718
+ }
6719
+ function updateSceneObjects(sceneObjects) {
6720
+ const nextPackedScene = packWavefrontSceneObjects(sceneObjects, config.sceneObjectCapacity);
6721
+ packedScene = nextPackedScene;
6722
+ config = rebuildLiveConfig();
4526
6723
  device.queue.writeBuffer(sceneObjectBuffer, 0, packedScene.buffer);
4527
6724
  return config;
4528
6725
  }
4529
6726
  function updateCamera(cameraOptions = {}) {
4530
6727
  activeCameraOptions = cameraOptions;
4531
- config = createWavefrontPathTracingComputeConfig({
4532
- ...options,
4533
- canvas,
4534
- width: config.width,
4535
- height: config.height,
4536
- maxDepth: config.maxDepth,
4537
- tileSize: config.tileSize,
4538
- samplesPerPixel: config.samplesPerPixel,
4539
- sceneObjectCapacity: config.sceneObjectCapacity,
4540
- sceneObjects: packedScene.objects,
4541
- camera: activeCameraOptions,
4542
- frameIndex: config.frameIndex
4543
- });
6728
+ config = rebuildLiveConfig();
4544
6729
  return config;
4545
6730
  }
4546
6731
  function getSnapshot() {
@@ -4595,6 +6780,25 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4595
6780
  if (environmentMapResource.ownsTexture) {
4596
6781
  environmentMapResource.texture?.destroy?.();
4597
6782
  }
6783
+ if (environmentSamplingResource.ownsTexture) {
6784
+ environmentSamplingResource.texture?.destroy?.();
6785
+ }
6786
+ brdfLutResource.texture?.destroy?.();
6787
+ if (baseColorAtlasResource.ownsTexture) {
6788
+ baseColorAtlasResource.texture?.destroy?.();
6789
+ }
6790
+ if (metallicRoughnessAtlasResource.ownsTexture) {
6791
+ metallicRoughnessAtlasResource.texture?.destroy?.();
6792
+ }
6793
+ if (normalAtlasResource.ownsTexture) {
6794
+ normalAtlasResource.texture?.destroy?.();
6795
+ }
6796
+ if (occlusionAtlasResource.ownsTexture) {
6797
+ occlusionAtlasResource.texture?.destroy?.();
6798
+ }
6799
+ if (emissiveAtlasResource.ownsTexture) {
6800
+ emissiveAtlasResource.texture?.destroy?.();
6801
+ }
4598
6802
  context.unconfigure?.();
4599
6803
  }
4600
6804
  return Object.freeze({
@@ -4747,6 +6951,48 @@ var rendererAccelerationStructurePolicies = Object.freeze(
4747
6951
  })
4748
6952
  )
4749
6953
  );
6954
+ function clampWavefrontAdaptiveSamplesPerPixel(value) {
6955
+ if (!Number.isFinite(value)) {
6956
+ return 1;
6957
+ }
6958
+ return Math.max(1, Math.min(256, Math.round(value)));
6959
+ }
6960
+ function createWavefrontAdaptiveSamplingLevels(options = {}) {
6961
+ const requestedSamplesPerPixel = clampWavefrontAdaptiveSamplesPerPixel(
6962
+ options.samplesPerPixel ?? 1
6963
+ );
6964
+ const minimumSamplesPerPixel = Math.min(
6965
+ requestedSamplesPerPixel,
6966
+ clampWavefrontAdaptiveSamplesPerPixel(options.minimumSamplesPerPixel ?? 1)
6967
+ );
6968
+ const frameTimeBudgetMs = Number.isFinite(options.frameTimeBudgetMs) ? Math.max(0, Number(options.frameTimeBudgetMs)) : 0;
6969
+ const levels = /* @__PURE__ */ new Set([minimumSamplesPerPixel, requestedSamplesPerPixel]);
6970
+ let currentSamplesPerPixel = minimumSamplesPerPixel;
6971
+ while (currentSamplesPerPixel < requestedSamplesPerPixel) {
6972
+ levels.add(currentSamplesPerPixel);
6973
+ currentSamplesPerPixel *= 2;
6974
+ }
6975
+ levels.add(Math.min(currentSamplesPerPixel, requestedSamplesPerPixel));
6976
+ return Object.freeze({
6977
+ requestedSamplesPerPixel,
6978
+ minimumSamplesPerPixel,
6979
+ frameTimeBudgetMs,
6980
+ levels: Object.freeze(
6981
+ [...levels].sort((left, right) => left - right).map(
6982
+ (samplesPerPixel) => Object.freeze({
6983
+ id: `${samplesPerPixel}spp`,
6984
+ label: `${samplesPerPixel} spp`,
6985
+ estimatedCostMs: samplesPerPixel,
6986
+ config: Object.freeze({
6987
+ samplesPerPixel,
6988
+ frameTimeBudgetMs,
6989
+ minimumSamplesPerPixel
6990
+ })
6991
+ })
6992
+ )
6993
+ )
6994
+ });
6995
+ }
4750
6996
  function createWavefrontField(name, type, description) {
4751
6997
  return Object.freeze({
4752
6998
  name,
@@ -5924,9 +8170,11 @@ var defaultRendererClearColor = DEFAULT_CLEAR_COLOR;
5924
8170
  createGpuRenderer,
5925
8171
  createRayTracingRenderPlan,
5926
8172
  createRendererDebugHooks,
8173
+ createWavefrontAdaptiveSamplingLevels,
5927
8174
  createWavefrontBvhBuildLevels,
5928
8175
  createWavefrontBvhSortStages,
5929
8176
  createWavefrontEmissiveTriangleIndexSource,
8177
+ createWavefrontGpuMaterialSource,
5930
8178
  createWavefrontGpuMeshSource,
5931
8179
  createWavefrontMeshAcceleration,
5932
8180
  createWavefrontPathTracingComputeConfig,