@plasius/gpu-renderer 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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,36 +72,179 @@ __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 = 128;
216
+ var DEFAULT_BRDF_LUT_SAMPLE_COUNT = 256;
79
217
  var DEFAULT_MAX_FRAME_PASSES_PER_SUBMISSION = 256;
80
218
  var DEFAULT_SCENE_OBJECT_CAPACITY = 128;
81
219
  var DEFAULT_ENVIRONMENT_PORTAL_CAPACITY = 32;
220
+ var DEFAULT_MEDIUM_PHASE_MODEL = 0;
82
221
  var WORKGROUP_SIZE = 64;
83
222
  var rendererWavefrontComputeMode = "webgpu-compute";
84
223
  var rendererWavefrontComputeWorkgroupSize = WORKGROUP_SIZE;
85
224
  var rendererWavefrontComputeStatsStride = 8;
86
225
  var RAY_RECORD_BYTES = 80;
87
- var HIT_RECORD_BYTES = 208;
88
- var SCENE_OBJECT_RECORD_BYTES = 96;
226
+ var HIT_RECORD_BYTES = 256;
227
+ var SCENE_OBJECT_RECORD_BYTES = 160;
89
228
  var MESH_VERTEX_RECORD_BYTES = 48;
90
- var MESH_RANGE_RECORD_BYTES = 96;
91
- var TRIANGLE_RECORD_BYTES = 208;
229
+ var MESH_RANGE_RECORD_BYTES = 240;
230
+ var TRIANGLE_RECORD_BYTES = 352;
231
+ var GPU_MATERIAL_RECORD_BYTES = 192;
92
232
  var BVH_NODE_RECORD_BYTES = 48;
93
233
  var BVH_LEAF_REF_RECORD_BYTES = 16;
94
234
  var EMISSIVE_TRIANGLE_INDEX_BYTES = 4;
95
235
  var ENVIRONMENT_PORTAL_RECORD_BYTES = 96;
236
+ var MEDIUM_TABLE_ROWS = 2;
96
237
  var ACCUMULATION_RECORD_BYTES = 16;
97
238
  var PATH_VERTEX_RECORD_BYTES = 16;
98
- var CONFIG_BUFFER_BYTES = 304;
239
+ var GPU_SUBMITTED_WORK_TIMEOUT_MS = 5e3;
240
+ var GPU_READBACK_COMPLETION_TIMEOUT_MS = 6e4;
241
+ var GPU_MAX_SUBMITTED_WORK_TIMEOUT_MS = 6e4;
242
+ var CONFIG_BUFFER_BYTES = 320;
99
243
  var COUNTER_DISPATCH_ARGS_OFFSET = 16;
100
244
  var INDIRECT_DISPATCH_ARGS_BYTES = 12;
101
245
  var COUNTER_BUFFER_BYTES = 32;
102
246
  var TRACE_STORAGE_BUFFER_BINDINGS = 10;
247
+ var BRDF_LUT_UPLOAD_CACHE = /* @__PURE__ */ new Map();
103
248
  var MATERIAL_DIFFUSE = 0;
104
249
  var MATERIAL_METAL = 1;
105
250
  var MATERIAL_DIELECTRIC = 2;
@@ -134,6 +279,7 @@ var wavefrontPathTracingComputeLimits = Object.freeze({
134
279
  meshVertexRecordBytes: MESH_VERTEX_RECORD_BYTES,
135
280
  meshRangeRecordBytes: MESH_RANGE_RECORD_BYTES,
136
281
  triangleRecordBytes: TRIANGLE_RECORD_BYTES,
282
+ materialRecordBytes: GPU_MATERIAL_RECORD_BYTES,
137
283
  bvhNodeRecordBytes: BVH_NODE_RECORD_BYTES,
138
284
  bvhLeafReferenceRecordBytes: BVH_LEAF_REF_RECORD_BYTES,
139
285
  emissiveTriangleIndexBytes: EMISSIVE_TRIANGLE_INDEX_BYTES,
@@ -217,6 +363,32 @@ function asColor(value, fallback = [1, 1, 1, 1]) {
217
363
  clamp(readFiniteNumber("color[3]", value[3], fallback[3] ?? 1), 0, 1)
218
364
  ];
219
365
  }
366
+ function deriveLegacySheenColor(baseColor, sheen, sheenTint) {
367
+ const sheenStrength = clamp(Number(sheen) || 0, 0, 1);
368
+ if (sheenStrength <= 0) {
369
+ return [0, 0, 0, 1];
370
+ }
371
+ const tint = clamp(Number(sheenTint) || 0, 0, 1);
372
+ const base = asColor(baseColor, [1, 1, 1, 1]);
373
+ return [
374
+ clamp((1 - tint) * sheenStrength + base[0] * tint * sheenStrength, 0, 1),
375
+ clamp((1 - tint) * sheenStrength + base[1] * tint * sheenStrength, 0, 1),
376
+ clamp((1 - tint) * sheenStrength + base[2] * tint * sheenStrength, 0, 1),
377
+ 1
378
+ ];
379
+ }
380
+ function resolveSheenColor(input, fallbackBaseColor) {
381
+ if (input?.sheenColor || input?.material?.sheenColor) {
382
+ return asColor(input.sheenColor ?? input.material?.sheenColor, [0, 0, 0, 1]).map(
383
+ (value, index) => index < 3 ? clamp(value, 0, 1) : 1
384
+ );
385
+ }
386
+ return deriveLegacySheenColor(
387
+ fallbackBaseColor,
388
+ input?.sheen ?? input?.material?.sheen,
389
+ input?.sheenTint ?? input?.material?.sheenTint
390
+ );
391
+ }
220
392
  function resolveEnvironmentMap(input = null) {
221
393
  const source = input && typeof input === "object" ? input : null;
222
394
  const hasTexture = Boolean(source?.view || source?.texture || source?.data);
@@ -226,6 +398,11 @@ function resolveEnvironmentMap(input = null) {
226
398
  enabled: hasTexture && source?.enabled !== false,
227
399
  width,
228
400
  height,
401
+ mipLevelCount: readPositiveInteger(
402
+ "environmentMap.mipLevelCount",
403
+ source?.mipLevelCount,
404
+ 1
405
+ ),
229
406
  format: typeof source?.format === "string" ? source.format : "rgba16float",
230
407
  projection: typeof source?.projection === "string" ? source.projection : "equirectangular",
231
408
  texture: source?.texture ?? null,
@@ -237,7 +414,8 @@ function resolveEnvironmentMap(input = null) {
237
414
  ambientStrength: Math.max(
238
415
  0,
239
416
  readFiniteNumber("environmentMap.ambientStrength", source?.ambientStrength, 0.32)
240
- )
417
+ ),
418
+ hasImportanceData: source?.hasImportanceData === true
241
419
  });
242
420
  }
243
421
  function resolveDeferredPathResolve(options = {}) {
@@ -402,6 +580,156 @@ function deriveBounds(input) {
402
580
  }
403
581
  return null;
404
582
  }
583
+ function deriveBeerLambertAbsorptionFromAttenuationColor(attenuationColor, attenuationDistance, density = 1) {
584
+ const distance = Number(attenuationDistance);
585
+ const densityScale = Math.max(0, Number(density) || 0);
586
+ if (!Number.isFinite(distance) || distance <= 0 || densityScale <= 0) {
587
+ return [0, 0, 0];
588
+ }
589
+ return attenuationColor.slice(0, 3).map((channel) => {
590
+ const clamped = clamp(Number(channel) || 0, 1e-4, 1);
591
+ return Math.max(0, -Math.log(clamped) / distance * densityScale);
592
+ });
593
+ }
594
+ function readMediumPhaseModel(value) {
595
+ if (typeof value === "number" && Number.isFinite(value)) {
596
+ return Math.max(0, Math.trunc(value));
597
+ }
598
+ switch (String(value ?? "").trim().toLowerCase()) {
599
+ case "isotropic":
600
+ default:
601
+ return DEFAULT_MEDIUM_PHASE_MODEL;
602
+ }
603
+ }
604
+ function resolveWavefrontVolumeInput(input) {
605
+ return input?.volume ?? input?.material?.volume ?? null;
606
+ }
607
+ function normalizeWavefrontThickness(input, label) {
608
+ const volume = resolveWavefrontVolumeInput(input);
609
+ return Math.max(
610
+ 0,
611
+ readFiniteNumber(
612
+ label,
613
+ input?.thickness ?? volume?.thickness ?? input?.material?.thickness,
614
+ 0
615
+ )
616
+ );
617
+ }
618
+ function resolveWavefrontMediumId(input, fallbackId = 1) {
619
+ return input?.mediumRefId ?? input?.mediumId ?? input?.material?.mediumId ?? input?.materialRefId ?? input?.material?.id ?? input?.materialId ?? input?.id ?? fallbackId;
620
+ }
621
+ function deriveWavefrontTransportMedium(input, fallbackId = 1) {
622
+ const resolvedId = resolveWavefrontMediumId(input, fallbackId);
623
+ if (input?.medium) {
624
+ return normalizeWavefrontMedium(
625
+ {
626
+ ...input.medium,
627
+ id: input.medium.id ?? input.medium.mediumId ?? resolvedId
628
+ },
629
+ fallbackId
630
+ );
631
+ }
632
+ const volume = resolveWavefrontVolumeInput(input);
633
+ if (!volume) {
634
+ return null;
635
+ }
636
+ return normalizeWavefrontMedium(
637
+ {
638
+ id: resolvedId,
639
+ phaseModel: volume.phaseModel,
640
+ density: volume.density,
641
+ attenuationColor: volume.attenuationColor,
642
+ attenuationDistance: volume.attenuationDistance,
643
+ absorption: volume.absorption,
644
+ scattering: volume.scattering
645
+ },
646
+ fallbackId
647
+ );
648
+ }
649
+ function normalizeWavefrontMedium(input = {}, index = 0) {
650
+ const id = readNonNegativeInteger("medium id", input.id ?? input.mediumId, index);
651
+ const density = Math.max(0, readFiniteNumber("medium density", input.density, 1));
652
+ const attenuationColor = asColor(
653
+ input.attenuationColor ?? input.color ?? input.medium?.attenuationColor,
654
+ [1, 1, 1, 1]
655
+ );
656
+ const attenuationDistance = readFiniteNumber(
657
+ "medium attenuationDistance",
658
+ input.attenuationDistance ?? input.distance ?? input.medium?.attenuationDistance,
659
+ 0
660
+ );
661
+ const absorption = Array.isArray(input.absorption) || Array.isArray(input.medium?.absorption) ? asVec3(input.absorption ?? input.medium?.absorption, [0, 0, 0]).map(
662
+ (value) => Math.max(0, Number(value) || 0)
663
+ ) : deriveBeerLambertAbsorptionFromAttenuationColor(
664
+ attenuationColor,
665
+ attenuationDistance,
666
+ density
667
+ );
668
+ const scattering = asVec3(
669
+ input.scattering ?? input.medium?.scattering,
670
+ [0, 0, 0]
671
+ ).map((value) => Math.max(0, Number(value) || 0));
672
+ return Object.freeze({
673
+ id,
674
+ phaseModel: readMediumPhaseModel(input.phaseModel ?? input.medium?.phaseModel),
675
+ density,
676
+ attenuationColor: Object.freeze(attenuationColor),
677
+ attenuationDistance,
678
+ absorption: Object.freeze(absorption),
679
+ scattering: Object.freeze(scattering)
680
+ });
681
+ }
682
+ function collectWavefrontMediums(options, meshes, sceneObjects = []) {
683
+ const mediumsById = /* @__PURE__ */ new Map();
684
+ mediumsById.set(
685
+ 0,
686
+ Object.freeze({
687
+ id: 0,
688
+ phaseModel: DEFAULT_MEDIUM_PHASE_MODEL,
689
+ density: 0,
690
+ attenuationColor: Object.freeze([1, 1, 1, 1]),
691
+ attenuationDistance: 0,
692
+ absorption: Object.freeze([0, 0, 0]),
693
+ scattering: Object.freeze([0, 0, 0])
694
+ })
695
+ );
696
+ const register = (input, fallbackId = mediumsById.size) => {
697
+ if (!input) {
698
+ return;
699
+ }
700
+ const normalized = normalizeWavefrontMedium(
701
+ typeof input === "object" ? { id: fallbackId, ...input } : { id: fallbackId },
702
+ fallbackId
703
+ );
704
+ const existing = mediumsById.get(normalized.id);
705
+ if (existing && JSON.stringify(existing) !== JSON.stringify(normalized)) {
706
+ throw new Error(`Medium id ${normalized.id} is defined more than once with different values.`);
707
+ }
708
+ mediumsById.set(normalized.id, normalized);
709
+ };
710
+ for (const medium of options.mediums ?? []) {
711
+ register(medium);
712
+ }
713
+ for (const mesh of meshes) {
714
+ register(mesh.medium, mesh.mediumRefId ?? mesh.medium?.id ?? 0);
715
+ }
716
+ for (const mesh of meshes) {
717
+ if ((mesh.mediumRefId ?? 0) > 0 && !mediumsById.has(mesh.mediumRefId)) {
718
+ register({ id: mesh.mediumRefId });
719
+ }
720
+ }
721
+ for (const object of sceneObjects) {
722
+ register(object.medium, object.mediumRefId ?? object.medium?.id ?? 0);
723
+ }
724
+ for (const object of sceneObjects) {
725
+ if ((object.mediumRefId ?? 0) > 0 && !mediumsById.has(object.mediumRefId)) {
726
+ register({ id: object.mediumRefId });
727
+ }
728
+ }
729
+ return Object.freeze(
730
+ Array.from(mediumsById.values()).sort((left, right) => left.id - right.id)
731
+ );
732
+ }
405
733
  function normalizeWavefrontSceneObject(input = {}, index = 0) {
406
734
  const bounds = deriveBounds(input);
407
735
  const kind = readObjectKind(input.kind ?? input.type ?? (bounds ? "box" : "sphere"));
@@ -411,7 +739,8 @@ function normalizeWavefrontSceneObject(input = {}, index = 0) {
411
739
  input.halfExtent ?? input.halfExtents ?? input.extents ?? bounds?.halfExtent,
412
740
  [0.5, 0.5, 0.5]
413
741
  ).map((value) => Math.max(value, 1e-3));
414
- const materialKind = readMaterialKind(input.materialKind ?? input.material?.kind);
742
+ const materialKindInput = input.materialKind ?? input.material?.kind;
743
+ const materialKind = readMaterialKind(materialKindInput);
415
744
  const color = asColor(
416
745
  input.color ?? input.baseColor ?? input.albedo ?? input.material?.color ?? input.material?.baseColor,
417
746
  [0.72, 0.72, 0.68, 1]
@@ -420,19 +749,55 @@ function normalizeWavefrontSceneObject(input = {}, index = 0) {
420
749
  input.emission ?? input.emissive ?? input.material?.emission ?? input.material?.emissive,
421
750
  [0, 0, 0, 1]
422
751
  );
752
+ const opacity = clamp(readFiniteNumber("opacity", input.opacity ?? input.material?.opacity, color[3] ?? 1), 0, 1);
753
+ const transmission = clamp(
754
+ readFiniteNumber("transmission", input.transmission ?? input.material?.transmission, 0),
755
+ 0,
756
+ 1
757
+ );
758
+ const sheenColor = resolveSheenColor(input, color);
759
+ const specularColor = asColor(
760
+ input.specularColor ?? input.material?.specularColor,
761
+ [1, 1, 1, 1]
762
+ ).map((value, componentIndex) => componentIndex < 3 ? clamp(value, 0, 1) : 1);
763
+ const medium = deriveWavefrontTransportMedium(input, index + 1);
764
+ 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
765
  return Object.freeze({
424
766
  id: readNonNegativeInteger("id", input.id, index + 1),
425
767
  kind,
426
- materialKind: emission[0] > 0 || emission[1] > 0 || emission[2] > 0 ? MATERIAL_EMISSIVE : materialKind,
768
+ materialKind: resolvedMaterialKind,
427
769
  flags: readNonNegativeInteger("flags", input.flags, 0),
770
+ mediumRefId: readNonNegativeInteger(
771
+ "mediumRefId",
772
+ input.mediumRefId ?? medium?.id ?? input.medium?.id ?? input.mediumId,
773
+ 0
774
+ ),
775
+ medium,
428
776
  center: Object.freeze(center),
429
777
  halfExtent: Object.freeze(halfExtent),
430
778
  color: Object.freeze(color),
431
779
  emission: Object.freeze(emission),
432
780
  roughness: clamp(readFiniteNumber("roughness", input.roughness ?? input.material?.roughness, 0.72), 0, 1),
433
781
  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)
782
+ opacity,
783
+ ior: clamp(readFiniteNumber("ior", input.ior ?? input.material?.ior, 1.45), 1, 3),
784
+ sheen: clamp(readFiniteNumber("sheen", input.sheen ?? input.material?.sheen, 0), 0, 1),
785
+ sheenTint: clamp(readFiniteNumber("sheenTint", input.sheenTint ?? input.material?.sheenTint, 0), 0, 1),
786
+ sheenColor: Object.freeze(sheenColor),
787
+ clearcoat: clamp(readFiniteNumber("clearcoat", input.clearcoat ?? input.material?.clearcoat, 0), 0, 1),
788
+ clearcoatRoughness: clamp(
789
+ readFiniteNumber(
790
+ "clearcoatRoughness",
791
+ input.clearcoatRoughness ?? input.material?.clearcoatRoughness,
792
+ 0.08
793
+ ),
794
+ 0,
795
+ 1
796
+ ),
797
+ specular: clamp(readFiniteNumber("specular", input.specular ?? input.material?.specular, 1), 0, 1),
798
+ specularColor: Object.freeze(specularColor),
799
+ thickness: normalizeWavefrontThickness(input, "thickness"),
800
+ transmission
436
801
  });
437
802
  }
438
803
  function createDefaultWavefrontSceneObjects() {
@@ -504,7 +869,8 @@ function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
504
869
  input.uvs ?? input.texcoords ?? input.uv,
505
870
  (value) => readFiniteNumber("mesh uv", value, 0)
506
871
  ) : null;
507
- const materialKind = readMaterialKind(input.materialKind ?? input.material?.kind);
872
+ const materialKindInput = input.materialKind ?? input.material?.kind;
873
+ const materialKind = readMaterialKind(materialKindInput);
508
874
  const color = asColor(
509
875
  input.color ?? input.baseColor ?? input.albedo ?? input.material?.color ?? input.material?.baseColor,
510
876
  [0.72, 0.72, 0.68, 1]
@@ -513,13 +879,26 @@ function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
513
879
  input.emission ?? input.emissive ?? input.material?.emission ?? input.material?.emissive,
514
880
  [0, 0, 0, 1]
515
881
  );
882
+ const opacity = clamp(readFiniteNumber("opacity", input.opacity ?? input.material?.opacity, color[3] ?? 1), 0, 1);
883
+ const transmission = clamp(
884
+ readFiniteNumber("transmission", input.transmission ?? input.material?.transmission, 0),
885
+ 0,
886
+ 1
887
+ );
888
+ const sheenColor = resolveSheenColor(input, color);
889
+ const specularColor = asColor(
890
+ input.specularColor ?? input.material?.specularColor,
891
+ [1, 1, 1, 1]
892
+ ).map((value, componentIndex) => componentIndex < 3 ? clamp(value, 0, 1) : 1);
893
+ const medium = deriveWavefrontTransportMedium(input, meshIndex + 1);
894
+ 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
895
  return Object.freeze({
517
896
  id: readNonNegativeInteger("mesh id", input.id, meshIndex + 1),
518
897
  positions: Object.freeze(Array.from(positions, (value) => readFiniteNumber("mesh position", value, 0))),
519
898
  indices: Object.freeze(indices),
520
899
  normals: normals ? Object.freeze(normals) : null,
521
900
  uvs: uvs ? Object.freeze(uvs) : null,
522
- materialKind: emission[0] > 0 || emission[1] > 0 || emission[2] > 0 ? MATERIAL_EMISSIVE : materialKind,
901
+ materialKind: resolvedMaterialKind,
523
902
  flags: readNonNegativeInteger("mesh flags", input.flags, 0),
524
903
  materialRefId: readNonNegativeInteger(
525
904
  "mesh materialRefId",
@@ -528,17 +907,176 @@ function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
528
907
  ),
529
908
  mediumRefId: readNonNegativeInteger(
530
909
  "mesh mediumRefId",
531
- input.mediumRefId ?? input.medium?.id ?? input.mediumId,
910
+ input.mediumRefId ?? medium?.id ?? input.medium?.id ?? input.mediumId ?? input.material?.mediumId,
532
911
  0
533
912
  ),
913
+ medium,
534
914
  color: Object.freeze(color),
535
915
  emission: Object.freeze(emission),
536
916
  roughness: clamp(readFiniteNumber("roughness", input.roughness ?? input.material?.roughness, 0.72), 0, 1),
537
917
  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)
918
+ opacity,
919
+ ior: clamp(readFiniteNumber("ior", input.ior ?? input.material?.ior, 1.45), 1, 3),
920
+ sheen: clamp(readFiniteNumber("sheen", input.sheen ?? input.material?.sheen, 0), 0, 1),
921
+ sheenTint: clamp(readFiniteNumber("sheenTint", input.sheenTint ?? input.material?.sheenTint, 0), 0, 1),
922
+ sheenColor: Object.freeze(sheenColor),
923
+ clearcoat: clamp(readFiniteNumber("clearcoat", input.clearcoat ?? input.material?.clearcoat, 0), 0, 1),
924
+ clearcoatRoughness: clamp(
925
+ readFiniteNumber(
926
+ "clearcoatRoughness",
927
+ input.clearcoatRoughness ?? input.material?.clearcoatRoughness,
928
+ 0.08
929
+ ),
930
+ 0,
931
+ 1
932
+ ),
933
+ specular: clamp(readFiniteNumber("specular", input.specular ?? input.material?.specular, 1), 0, 1),
934
+ specularColor: Object.freeze(specularColor),
935
+ thickness: normalizeWavefrontThickness(input, "mesh thickness"),
936
+ transmission,
937
+ baseColorTexture: input.baseColorTexture ?? input.material?.baseColorTexture ?? null,
938
+ metallicRoughnessTexture: input.metallicRoughnessTexture ?? input.material?.metallicRoughnessTexture ?? null,
939
+ normalTexture: input.normalTexture ?? input.material?.normalTexture ?? null,
940
+ occlusionTexture: input.occlusionTexture ?? input.material?.occlusionTexture ?? null,
941
+ emissiveTexture: input.emissiveTexture ?? input.material?.emissiveTexture ?? null
540
942
  });
541
943
  }
944
+ function clampUnit(value) {
945
+ return clamp(Number(value) || 0, 0, 1);
946
+ }
947
+ function srgbToLinear(value) {
948
+ const channel = clampUnit(value);
949
+ if (channel <= 0.04045) {
950
+ return channel / 12.92;
951
+ }
952
+ return ((channel + 0.055) / 1.055) ** 2.4;
953
+ }
954
+ function sampleTextureRgba(texture, uv = [0, 0], colorSpace = "linear") {
955
+ if (!texture || !Number.isFinite(texture.width) || !Number.isFinite(texture.height) || !texture.data || texture.width <= 0 || texture.height <= 0) {
956
+ return [1, 1, 1, 1];
957
+ }
958
+ const u = (uv[0] % 1 + 1) % 1;
959
+ const v = (uv[1] % 1 + 1) % 1;
960
+ const x = Math.min(texture.width - 1, Math.max(0, Math.round(u * (texture.width - 1))));
961
+ const y = Math.min(texture.height - 1, Math.max(0, Math.round((1 - v) * (texture.height - 1))));
962
+ const offset = (y * texture.width + x) * 4;
963
+ const data = texture.data;
964
+ const scale2 = resolveTextureSampleScale(data);
965
+ const defaultChannel = scale2 === 1 ? 1 : Math.round(1 / scale2);
966
+ const color = [
967
+ (data[offset] ?? defaultChannel) * scale2,
968
+ (data[offset + 1] ?? defaultChannel) * scale2,
969
+ (data[offset + 2] ?? defaultChannel) * scale2,
970
+ (data[offset + 3] ?? defaultChannel) * scale2
971
+ ];
972
+ if (colorSpace === "srgb") {
973
+ return [srgbToLinear(color[0]), srgbToLinear(color[1]), srgbToLinear(color[2]), color[3]];
974
+ }
975
+ return color;
976
+ }
977
+ function resolveTextureSampleScale(data) {
978
+ if (data instanceof Uint8Array || data instanceof Uint8ClampedArray) {
979
+ return 1 / 255;
980
+ }
981
+ if (data instanceof Uint16Array) {
982
+ return 1 / 65535;
983
+ }
984
+ if (Array.isArray(data) && data.some((value) => Number(value) > 1)) {
985
+ return 1 / 255;
986
+ }
987
+ return 1;
988
+ }
989
+ function normalizeVectorOrFallback(vector, fallback) {
990
+ return normalize(Array.isArray(vector) ? vector : fallback, fallback);
991
+ }
992
+ function buildTriangleTangentBasis(v0, v1, v2, uv0, uv1, uv2, fallbackNormal) {
993
+ const edge1 = subtract(v1, v0);
994
+ const edge2 = subtract(v2, v0);
995
+ const deltaUv1 = [uv1[0] - uv0[0], uv1[1] - uv0[1]];
996
+ const deltaUv2 = [uv2[0] - uv0[0], uv2[1] - uv0[1]];
997
+ const determinant = deltaUv1[0] * deltaUv2[1] - deltaUv1[1] * deltaUv2[0];
998
+ if (Math.abs(determinant) < 1e-6) {
999
+ const tangentFallback = Math.abs(fallbackNormal[1]) < 0.999 ? [0, 1, 0] : [1, 0, 0];
1000
+ const tangent2 = normalize(cross(tangentFallback, fallbackNormal), [1, 0, 0]);
1001
+ const bitangent2 = normalize(cross(fallbackNormal, tangent2), [0, 0, 1]);
1002
+ return { tangent: tangent2, bitangent: bitangent2 };
1003
+ }
1004
+ const inverse = 1 / determinant;
1005
+ const tangent = normalize(
1006
+ [
1007
+ inverse * (edge1[0] * deltaUv2[1] - edge2[0] * deltaUv1[1]),
1008
+ inverse * (edge1[1] * deltaUv2[1] - edge2[1] * deltaUv1[1]),
1009
+ inverse * (edge1[2] * deltaUv2[1] - edge2[2] * deltaUv1[1])
1010
+ ],
1011
+ [1, 0, 0]
1012
+ );
1013
+ const bitangent = normalize(
1014
+ [
1015
+ inverse * (-edge1[0] * deltaUv2[0] + edge2[0] * deltaUv1[0]),
1016
+ inverse * (-edge1[1] * deltaUv2[0] + edge2[1] * deltaUv1[0]),
1017
+ inverse * (-edge1[2] * deltaUv2[0] + edge2[2] * deltaUv1[0])
1018
+ ],
1019
+ [0, 0, 1]
1020
+ );
1021
+ return { tangent, bitangent };
1022
+ }
1023
+ function applyNormalMap(normal, tangent, bitangent, normalTexture, uv) {
1024
+ if (!normalTexture) {
1025
+ return normalizeVectorOrFallback(normal, [0, 1, 0]);
1026
+ }
1027
+ const sample = sampleTextureRgba(normalTexture, uv, "linear");
1028
+ const strength = clampUnit(normalTexture.scale ?? 1);
1029
+ const tangentNormal = normalize(
1030
+ [
1031
+ (sample[0] * 2 - 1) * strength,
1032
+ (sample[1] * 2 - 1) * strength,
1033
+ 1 + (sample[2] * 2 - 1 - 1) * strength
1034
+ ],
1035
+ [0, 0, 1]
1036
+ );
1037
+ return normalize(
1038
+ [
1039
+ tangent[0] * tangentNormal[0] + bitangent[0] * tangentNormal[1] + normal[0] * tangentNormal[2],
1040
+ tangent[1] * tangentNormal[0] + bitangent[1] * tangentNormal[1] + normal[1] * tangentNormal[2],
1041
+ tangent[2] * tangentNormal[0] + bitangent[2] * tangentNormal[1] + normal[2] * tangentNormal[2]
1042
+ ],
1043
+ normal
1044
+ );
1045
+ }
1046
+ function sampleBaseColor(mesh, uv) {
1047
+ const sample = mesh.baseColorTexture ? sampleTextureRgba(mesh.baseColorTexture, uv, "srgb") : [1, 1, 1, 1];
1048
+ return [
1049
+ clampUnit(mesh.color[0] * sample[0]),
1050
+ clampUnit(mesh.color[1] * sample[1]),
1051
+ clampUnit(mesh.color[2] * sample[2]),
1052
+ clampUnit((mesh.color[3] ?? 1) * sample[3])
1053
+ ];
1054
+ }
1055
+ function sampleSurfaceMaterial(mesh, uv) {
1056
+ const textureSample = mesh.metallicRoughnessTexture ? sampleTextureRgba(mesh.metallicRoughnessTexture, uv, "linear") : [1, 1, 1, 1];
1057
+ return {
1058
+ roughness: clamp(mesh.roughness * textureSample[1], 0, 1),
1059
+ metallic: clamp(mesh.metallic * textureSample[2], 0, 1)
1060
+ };
1061
+ }
1062
+ function averageColors(colors) {
1063
+ const count = Math.max(colors.length, 1);
1064
+ return colors.reduce(
1065
+ (accumulator, color) => [
1066
+ accumulator[0] + color[0] / count,
1067
+ accumulator[1] + color[1] / count,
1068
+ accumulator[2] + color[2] / count,
1069
+ accumulator[3] + color[3] / count
1070
+ ],
1071
+ [0, 0, 0, 0]
1072
+ );
1073
+ }
1074
+ function averageNumbers(values, fallback = 0) {
1075
+ if (!Array.isArray(values) || values.length === 0) {
1076
+ return fallback;
1077
+ }
1078
+ return values.reduce((sum, value) => sum + value, 0) / values.length;
1079
+ }
542
1080
  function createMeshTriangleRecords(meshes) {
543
1081
  const source = Array.isArray(meshes) ? meshes : [];
544
1082
  let nextTriangleId = 0;
@@ -559,6 +1097,16 @@ function createMeshTriangleRecords(meshes) {
559
1097
  const uv0 = mesh.uvs ? readVector2(mesh.uvs, a) : [0, 0];
560
1098
  const uv1 = mesh.uvs ? readVector2(mesh.uvs, b) : [0, 0];
561
1099
  const uv2 = mesh.uvs ? readVector2(mesh.uvs, c) : [0, 0];
1100
+ const tangentBasis = buildTriangleTangentBasis(v0, v1, v2, uv0, uv1, uv2, faceNormal);
1101
+ const shadedN0 = applyNormalMap(n0, tangentBasis.tangent, tangentBasis.bitangent, mesh.normalTexture, uv0);
1102
+ const shadedN1 = applyNormalMap(n1, tangentBasis.tangent, tangentBasis.bitangent, mesh.normalTexture, uv1);
1103
+ const shadedN2 = applyNormalMap(n2, tangentBasis.tangent, tangentBasis.bitangent, mesh.normalTexture, uv2);
1104
+ const sampledColors = [sampleBaseColor(mesh, uv0), sampleBaseColor(mesh, uv1), sampleBaseColor(mesh, uv2)];
1105
+ const sampledMaterials = [
1106
+ sampleSurfaceMaterial(mesh, uv0),
1107
+ sampleSurfaceMaterial(mesh, uv1),
1108
+ sampleSurfaceMaterial(mesh, uv2)
1109
+ ];
562
1110
  const bounds = triangleBounds(v0, v1, v2);
563
1111
  triangles.push(
564
1112
  Object.freeze({
@@ -568,18 +1116,42 @@ function createMeshTriangleRecords(meshes) {
568
1116
  flags: mesh.flags,
569
1117
  materialRefId: mesh.materialRefId,
570
1118
  mediumRefId: mesh.mediumRefId,
1119
+ materialSlot: meshIndex,
571
1120
  v0: Object.freeze(v0),
572
1121
  v1: Object.freeze(v1),
573
1122
  v2: Object.freeze(v2),
574
- n0: Object.freeze(n0),
575
- n1: Object.freeze(n1),
576
- n2: Object.freeze(n2),
1123
+ n0: Object.freeze(shadedN0),
1124
+ n1: Object.freeze(shadedN1),
1125
+ n2: Object.freeze(shadedN2),
577
1126
  uv0: Object.freeze(uv0),
578
1127
  uv1: Object.freeze(uv1),
579
1128
  uv2: Object.freeze(uv2),
580
- color: mesh.color,
1129
+ color: Object.freeze(averageColors(sampledColors)),
581
1130
  emission: mesh.emission,
582
- material: Object.freeze([mesh.roughness, mesh.metallic, mesh.opacity, mesh.ior]),
1131
+ material: Object.freeze([
1132
+ averageNumbers(sampledMaterials.map((sample) => sample.roughness), mesh.roughness),
1133
+ averageNumbers(sampledMaterials.map((sample) => sample.metallic), mesh.metallic),
1134
+ mesh.opacity,
1135
+ mesh.ior
1136
+ ]),
1137
+ materialResponse: Object.freeze([
1138
+ mesh.sheenColor[0] ?? 0,
1139
+ mesh.sheenColor[1] ?? 0,
1140
+ mesh.sheenColor[2] ?? 0,
1141
+ mesh.clearcoat
1142
+ ]),
1143
+ materialExtension: Object.freeze([
1144
+ mesh.clearcoatRoughness,
1145
+ mesh.specular,
1146
+ mesh.transmission,
1147
+ mesh.thickness
1148
+ ]),
1149
+ specularColor: Object.freeze([
1150
+ mesh.specularColor[0] ?? 1,
1151
+ mesh.specularColor[1] ?? 1,
1152
+ mesh.specularColor[2] ?? 1,
1153
+ 1
1154
+ ]),
583
1155
  bounds: Object.freeze({
584
1156
  min: Object.freeze(bounds.min),
585
1157
  max: Object.freeze(bounds.max)
@@ -688,50 +1260,264 @@ function nextPowerOfTwo(value) {
688
1260
  }
689
1261
  return 2 ** Math.ceil(Math.log2(value));
690
1262
  }
691
- function estimateBvhLeafSortCapacity(triangleCount) {
692
- return triangleCount <= 0 ? 0 : nextPowerOfTwo(triangleCount);
693
- }
694
- function createWavefrontBvhSortStages(itemCountInput) {
695
- const itemCount = readNonNegativeInteger("itemCount", itemCountInput, 0);
696
- const sortCount = estimateBvhLeafSortCapacity(itemCount);
697
- if (sortCount <= 1) {
698
- return Object.freeze([]);
1263
+ function textureComponentToByte(value, fallback) {
1264
+ const numeric = Number(value);
1265
+ if (!Number.isFinite(numeric)) {
1266
+ return fallback;
699
1267
  }
700
- const stages = [];
701
- for (let sequenceSize = 2; sequenceSize <= sortCount; sequenceSize *= 2) {
702
- for (let compareDistance = sequenceSize / 2; compareDistance >= 1; compareDistance /= 2) {
703
- stages.push(
704
- Object.freeze({
705
- compareDistance,
706
- sequenceSize
707
- })
708
- );
709
- }
1268
+ if (numeric >= 0 && numeric <= 1) {
1269
+ return Math.max(0, Math.min(255, Math.round(numeric * 255)));
710
1270
  }
711
- return Object.freeze(stages);
1271
+ return Math.max(0, Math.min(255, Math.round(numeric)));
712
1272
  }
713
- function createWavefrontBvhBuildLevels(triangleCountInput) {
714
- const triangleCount = readNonNegativeInteger("triangleCount", triangleCountInput, 0);
715
- const internalCount = Math.max(0, triangleCount - 1);
716
- if (internalCount === 0) {
717
- return Object.freeze([]);
1273
+ function createSolidTextureSample(width, height, rgba) {
1274
+ const data = new Uint8Array(width * height * 4);
1275
+ for (let offset = 0; offset < data.length; offset += 4) {
1276
+ data[offset] = rgba[0];
1277
+ data[offset + 1] = rgba[1];
1278
+ data[offset + 2] = rgba[2];
1279
+ data[offset + 3] = rgba[3];
718
1280
  }
719
- const levels = [];
720
- let depth = 0;
721
- while (Math.pow(2, depth) - 1 < internalCount) {
722
- depth += 1;
1281
+ return Object.freeze({
1282
+ width,
1283
+ height,
1284
+ data
1285
+ });
1286
+ }
1287
+ function normalizeTextureSampleInput(texture, fallbackColor) {
1288
+ if (!texture || !Number.isFinite(texture.width) || !Number.isFinite(texture.height) || texture.width <= 0 || texture.height <= 0) {
1289
+ return createSolidTextureSample(1, 1, fallbackColor);
723
1290
  }
724
- for (let level = depth - 1; level >= 0; level -= 1) {
725
- const start = Math.pow(2, level) - 1;
726
- const end = Math.min(Math.pow(2, level + 1) - 2, internalCount - 1);
727
- if (end >= start) {
728
- levels.push(
729
- Object.freeze({
730
- start,
731
- count: end - start + 1
732
- })
733
- );
734
- }
1291
+ const pixelCount = Math.trunc(texture.width) * Math.trunc(texture.height) * 4;
1292
+ const source = ArrayBuffer.isView(texture.data) || Array.isArray(texture.data) ? texture.data : null;
1293
+ if (!source || source.length < pixelCount) {
1294
+ return createSolidTextureSample(1, 1, fallbackColor);
1295
+ }
1296
+ const data = new Uint8Array(pixelCount);
1297
+ for (let index = 0; index < pixelCount; index += 1) {
1298
+ data[index] = textureComponentToByte(source[index], fallbackColor[index % 4]);
1299
+ }
1300
+ return Object.freeze({
1301
+ width: Math.trunc(texture.width),
1302
+ height: Math.trunc(texture.height),
1303
+ data
1304
+ });
1305
+ }
1306
+ function buildTextureAtlas(textures, fallbackColor) {
1307
+ const padding = 1;
1308
+ const defaultTexture = createSolidTextureSample(1, 1, fallbackColor);
1309
+ const uniqueEntries = [{ source: null, texture: defaultTexture }];
1310
+ const bySource = /* @__PURE__ */ new Map();
1311
+ for (const texture of Array.isArray(textures) ? textures : []) {
1312
+ if (!texture || bySource.has(texture)) {
1313
+ continue;
1314
+ }
1315
+ const normalized = normalizeTextureSampleInput(texture, fallbackColor);
1316
+ bySource.set(texture, uniqueEntries.length);
1317
+ uniqueEntries.push({ source: texture, texture: normalized });
1318
+ }
1319
+ const totalArea = uniqueEntries.reduce((sum, entry) => {
1320
+ return sum + (entry.texture.width + padding * 2) * (entry.texture.height + padding * 2);
1321
+ }, 0);
1322
+ const maxTileWidth = uniqueEntries.reduce((maxWidth, entry) => {
1323
+ return Math.max(maxWidth, entry.texture.width + padding * 2);
1324
+ }, 1);
1325
+ const targetWidth = Math.max(
1326
+ maxTileWidth,
1327
+ nextPowerOfTwo(Math.max(maxTileWidth, Math.ceil(Math.sqrt(totalArea))))
1328
+ );
1329
+ let cursorX = 0;
1330
+ let cursorY = 0;
1331
+ let rowHeight = 0;
1332
+ let atlasWidth = 0;
1333
+ const placements = uniqueEntries.map((entry) => {
1334
+ const tileWidth = entry.texture.width + padding * 2;
1335
+ const tileHeight = entry.texture.height + padding * 2;
1336
+ if (cursorX > 0 && cursorX + tileWidth > targetWidth) {
1337
+ cursorX = 0;
1338
+ cursorY += rowHeight;
1339
+ rowHeight = 0;
1340
+ }
1341
+ const placement = Object.freeze({
1342
+ x: cursorX,
1343
+ y: cursorY,
1344
+ tileWidth,
1345
+ tileHeight,
1346
+ width: entry.texture.width,
1347
+ height: entry.texture.height
1348
+ });
1349
+ cursorX += tileWidth;
1350
+ atlasWidth = Math.max(atlasWidth, cursorX);
1351
+ rowHeight = Math.max(rowHeight, tileHeight);
1352
+ return placement;
1353
+ });
1354
+ const atlasHeight = Math.max(1, cursorY + rowHeight);
1355
+ const atlasData = new Uint8Array(Math.max(1, atlasWidth * atlasHeight * 4));
1356
+ const writePixel = (x, y, rgba) => {
1357
+ const offset = (y * atlasWidth + x) * 4;
1358
+ atlasData[offset] = rgba[0];
1359
+ atlasData[offset + 1] = rgba[1];
1360
+ atlasData[offset + 2] = rgba[2];
1361
+ atlasData[offset + 3] = rgba[3];
1362
+ };
1363
+ const rects = placements.map((placement, entryIndex) => {
1364
+ const { texture } = uniqueEntries[entryIndex];
1365
+ for (let y = 0; y < placement.tileHeight; y += 1) {
1366
+ for (let x = 0; x < placement.tileWidth; x += 1) {
1367
+ const sampleX = Math.max(0, Math.min(texture.width - 1, x - padding));
1368
+ const sampleY = Math.max(0, Math.min(texture.height - 1, y - padding));
1369
+ const sourceOffset = (sampleY * texture.width + sampleX) * 4;
1370
+ writePixel(placement.x + x, placement.y + y, texture.data.slice(sourceOffset, sourceOffset + 4));
1371
+ }
1372
+ }
1373
+ return Object.freeze([
1374
+ (placement.x + padding) / Math.max(1, atlasWidth),
1375
+ (placement.y + padding) / Math.max(1, atlasHeight),
1376
+ placement.width / Math.max(1, atlasWidth),
1377
+ placement.height / Math.max(1, atlasHeight)
1378
+ ]);
1379
+ });
1380
+ const rectBySource = /* @__PURE__ */ new Map();
1381
+ uniqueEntries.forEach((entry, index) => {
1382
+ if (entry.source) {
1383
+ rectBySource.set(entry.source, rects[index]);
1384
+ }
1385
+ });
1386
+ return Object.freeze({
1387
+ width: Math.max(1, atlasWidth),
1388
+ height: Math.max(1, atlasHeight),
1389
+ data: atlasData,
1390
+ defaultRect: rects[0],
1391
+ resolveRect(texture) {
1392
+ return rectBySource.get(texture) ?? rects[0];
1393
+ }
1394
+ });
1395
+ }
1396
+ function createWavefrontGpuMaterialSource(meshes = []) {
1397
+ const source = Array.isArray(meshes) ? meshes : [meshes];
1398
+ const normalized = source.map((meshInput, meshIndex) => normalizeWavefrontMesh(meshInput, meshIndex));
1399
+ const baseColorAtlas = buildTextureAtlas(
1400
+ normalized.map((mesh) => mesh.baseColorTexture),
1401
+ [255, 255, 255, 255]
1402
+ );
1403
+ const metallicRoughnessAtlas = buildTextureAtlas(
1404
+ normalized.map((mesh) => mesh.metallicRoughnessTexture),
1405
+ [255, 255, 255, 255]
1406
+ );
1407
+ const normalAtlas = buildTextureAtlas(
1408
+ normalized.map((mesh) => mesh.normalTexture),
1409
+ [128, 128, 255, 255]
1410
+ );
1411
+ const occlusionAtlas = buildTextureAtlas(
1412
+ normalized.map((mesh) => mesh.occlusionTexture),
1413
+ [255, 255, 255, 255]
1414
+ );
1415
+ const emissiveAtlas = buildTextureAtlas(
1416
+ normalized.map((mesh) => mesh.emissiveTexture),
1417
+ [255, 255, 255, 255]
1418
+ );
1419
+ const bytes = new ArrayBuffer(Math.max(1, normalized.length) * GPU_MATERIAL_RECORD_BYTES);
1420
+ const floatView = new Float32Array(bytes);
1421
+ normalized.forEach((mesh, meshIndex) => {
1422
+ const byteOffset = meshIndex * GPU_MATERIAL_RECORD_BYTES;
1423
+ writeVec4(floatView, byteOffset, mesh.color);
1424
+ writeVec4(floatView, byteOffset + 16, mesh.emission);
1425
+ writeVec4(floatView, byteOffset + 32, [
1426
+ mesh.roughness,
1427
+ mesh.metallic,
1428
+ mesh.opacity,
1429
+ mesh.ior
1430
+ ]);
1431
+ writeVec4(floatView, byteOffset + 48, [
1432
+ mesh.sheenColor[0] ?? 0,
1433
+ mesh.sheenColor[1] ?? 0,
1434
+ mesh.sheenColor[2] ?? 0,
1435
+ mesh.clearcoat
1436
+ ]);
1437
+ writeVec4(floatView, byteOffset + 64, [
1438
+ mesh.clearcoatRoughness,
1439
+ mesh.specular,
1440
+ mesh.transmission,
1441
+ mesh.thickness
1442
+ ]);
1443
+ writeVec4(floatView, byteOffset + 80, [
1444
+ mesh.specularColor[0] ?? 1,
1445
+ mesh.specularColor[1] ?? 1,
1446
+ mesh.specularColor[2] ?? 1,
1447
+ 1
1448
+ ]);
1449
+ writeVec4(floatView, byteOffset + 96, baseColorAtlas.resolveRect(mesh.baseColorTexture));
1450
+ writeVec4(
1451
+ floatView,
1452
+ byteOffset + 112,
1453
+ metallicRoughnessAtlas.resolveRect(mesh.metallicRoughnessTexture)
1454
+ );
1455
+ writeVec4(floatView, byteOffset + 128, normalAtlas.resolveRect(mesh.normalTexture));
1456
+ writeVec4(floatView, byteOffset + 144, occlusionAtlas.resolveRect(mesh.occlusionTexture));
1457
+ writeVec4(floatView, byteOffset + 160, emissiveAtlas.resolveRect(mesh.emissiveTexture));
1458
+ writeVec4(floatView, byteOffset + 176, [
1459
+ clampUnit(mesh.normalTexture?.scale ?? mesh.normalTexture?.strength ?? 1),
1460
+ clampUnit(mesh.occlusionTexture?.strength ?? 1),
1461
+ clampUnit(mesh.emissiveTexture?.strength ?? 1),
1462
+ 0
1463
+ ]);
1464
+ });
1465
+ return Object.freeze({
1466
+ buffer: bytes,
1467
+ count: normalized.length,
1468
+ recordBytes: GPU_MATERIAL_RECORD_BYTES,
1469
+ records: Object.freeze(normalized),
1470
+ baseColorAtlas,
1471
+ metallicRoughnessAtlas,
1472
+ normalAtlas,
1473
+ occlusionAtlas,
1474
+ emissiveAtlas
1475
+ });
1476
+ }
1477
+ function estimateBvhLeafSortCapacity(triangleCount) {
1478
+ return triangleCount <= 0 ? 0 : nextPowerOfTwo(triangleCount);
1479
+ }
1480
+ function createWavefrontBvhSortStages(itemCountInput) {
1481
+ const itemCount = readNonNegativeInteger("itemCount", itemCountInput, 0);
1482
+ const sortCount = estimateBvhLeafSortCapacity(itemCount);
1483
+ if (sortCount <= 1) {
1484
+ return Object.freeze([]);
1485
+ }
1486
+ const stages = [];
1487
+ for (let sequenceSize = 2; sequenceSize <= sortCount; sequenceSize *= 2) {
1488
+ for (let compareDistance = sequenceSize / 2; compareDistance >= 1; compareDistance /= 2) {
1489
+ stages.push(
1490
+ Object.freeze({
1491
+ compareDistance,
1492
+ sequenceSize
1493
+ })
1494
+ );
1495
+ }
1496
+ }
1497
+ return Object.freeze(stages);
1498
+ }
1499
+ function createWavefrontBvhBuildLevels(triangleCountInput) {
1500
+ const triangleCount = readNonNegativeInteger("triangleCount", triangleCountInput, 0);
1501
+ const internalCount = Math.max(0, triangleCount - 1);
1502
+ if (internalCount === 0) {
1503
+ return Object.freeze([]);
1504
+ }
1505
+ const levels = [];
1506
+ let depth = 0;
1507
+ while (Math.pow(2, depth) - 1 < internalCount) {
1508
+ depth += 1;
1509
+ }
1510
+ for (let level = depth - 1; level >= 0; level -= 1) {
1511
+ const start = Math.pow(2, level) - 1;
1512
+ const end = Math.min(Math.pow(2, level + 1) - 2, internalCount - 1);
1513
+ if (end >= start) {
1514
+ levels.push(
1515
+ Object.freeze({
1516
+ start,
1517
+ count: end - start + 1
1518
+ })
1519
+ );
1520
+ }
735
1521
  }
736
1522
  return Object.freeze(levels);
737
1523
  }
@@ -745,9 +1531,10 @@ function resolveAccelerationBuildMode(options = {}) {
745
1531
  }
746
1532
  return mode;
747
1533
  }
748
- function createWavefrontGpuMeshSource(meshes = []) {
1534
+ function createWavefrontGpuMeshSource(meshes = [], gpuMaterialSourceInput = null) {
749
1535
  const source = Array.isArray(meshes) ? meshes : [meshes];
750
1536
  const normalized = source.map((meshInput, meshIndex) => normalizeWavefrontMesh(meshInput, meshIndex));
1537
+ const gpuMaterialSource = gpuMaterialSourceInput ?? createWavefrontGpuMaterialSource(normalized);
751
1538
  const vertexCount = normalized.reduce((count, mesh) => count + mesh.positions.length / 3, 0);
752
1539
  const indexCount = normalized.reduce((count, mesh) => count + mesh.indices.length, 0);
753
1540
  const triangleCount = Math.floor(indexCount / 3);
@@ -799,7 +1586,7 @@ function createWavefrontGpuMeshSource(meshes = []) {
799
1586
  meshUints[meshOffset + 8] = mesh.indices.length / 3;
800
1587
  meshUints[meshOffset + 9] = meshVertexBase;
801
1588
  meshUints[meshOffset + 10] = meshVertexCount;
802
- meshUints[meshOffset + 11] = 0;
1589
+ meshUints[meshOffset + 11] = meshIndex;
803
1590
  const floatOffset = meshOffset;
804
1591
  writeVec4(meshFloats, floatOffset * 4 + 48, mesh.color);
805
1592
  writeVec4(meshFloats, floatOffset * 4 + 64, mesh.emission);
@@ -809,6 +1596,55 @@ function createWavefrontGpuMeshSource(meshes = []) {
809
1596
  mesh.opacity,
810
1597
  mesh.ior
811
1598
  ]);
1599
+ writeVec4(meshFloats, floatOffset * 4 + 96, [
1600
+ mesh.sheenColor[0] ?? 0,
1601
+ mesh.sheenColor[1] ?? 0,
1602
+ mesh.sheenColor[2] ?? 0,
1603
+ mesh.clearcoat
1604
+ ]);
1605
+ writeVec4(meshFloats, floatOffset * 4 + 112, [
1606
+ mesh.clearcoatRoughness,
1607
+ mesh.specular,
1608
+ mesh.transmission,
1609
+ mesh.thickness
1610
+ ]);
1611
+ writeVec4(meshFloats, floatOffset * 4 + 128, [
1612
+ mesh.specularColor[0] ?? 1,
1613
+ mesh.specularColor[1] ?? 1,
1614
+ mesh.specularColor[2] ?? 1,
1615
+ 1
1616
+ ]);
1617
+ writeVec4(
1618
+ meshFloats,
1619
+ floatOffset * 4 + 144,
1620
+ gpuMaterialSource.baseColorAtlas.resolveRect(mesh.baseColorTexture)
1621
+ );
1622
+ writeVec4(
1623
+ meshFloats,
1624
+ floatOffset * 4 + 160,
1625
+ gpuMaterialSource.metallicRoughnessAtlas.resolveRect(mesh.metallicRoughnessTexture)
1626
+ );
1627
+ writeVec4(
1628
+ meshFloats,
1629
+ floatOffset * 4 + 176,
1630
+ gpuMaterialSource.normalAtlas.resolveRect(mesh.normalTexture)
1631
+ );
1632
+ writeVec4(
1633
+ meshFloats,
1634
+ floatOffset * 4 + 192,
1635
+ gpuMaterialSource.occlusionAtlas.resolveRect(mesh.occlusionTexture)
1636
+ );
1637
+ writeVec4(
1638
+ meshFloats,
1639
+ floatOffset * 4 + 208,
1640
+ gpuMaterialSource.emissiveAtlas.resolveRect(mesh.emissiveTexture)
1641
+ );
1642
+ writeVec4(meshFloats, floatOffset * 4 + 224, [
1643
+ clampUnit(mesh.normalTexture?.scale ?? mesh.normalTexture?.strength ?? 1),
1644
+ clampUnit(mesh.occlusionTexture?.strength ?? 1),
1645
+ clampUnit(mesh.emissiveTexture?.strength ?? 1),
1646
+ 0
1647
+ ]);
812
1648
  vertexCursor += meshVertexCount;
813
1649
  indexCursor += mesh.indices.length;
814
1650
  triangleCursor += mesh.indices.length / 3;
@@ -871,12 +1707,16 @@ function normalizeSceneObjects(sceneObjects, useDefaultScene = true) {
871
1707
  const source = Array.isArray(sceneObjects) && sceneObjects.length > 0 ? sceneObjects : useDefaultScene ? createDefaultWavefrontSceneObjects() : [];
872
1708
  return source.map((object, index) => normalizeWavefrontSceneObject(object, index));
873
1709
  }
1710
+ function normalizeWavefrontMeshes(meshes) {
1711
+ const source = Array.isArray(meshes) ? meshes : [];
1712
+ return source.map((mesh, index) => normalizeWavefrontMesh(mesh, index));
1713
+ }
874
1714
  function normalizeMeshes(options = {}) {
875
1715
  if (Array.isArray(options.meshes)) {
876
- return options.meshes;
1716
+ return normalizeWavefrontMeshes(options.meshes);
877
1717
  }
878
1718
  if (options.mesh) {
879
- return [options.mesh];
1719
+ return normalizeWavefrontMeshes([options.mesh]);
880
1720
  }
881
1721
  return [];
882
1722
  }
@@ -1111,12 +1951,14 @@ function estimateWavefrontPathTracingMemory(options = {}) {
1111
1951
  options.environmentPortalCapacity,
1112
1952
  0
1113
1953
  );
1954
+ const materialCapacity = readNonNegativeInteger("materialCapacity", options.materialCapacity, 0);
1114
1955
  const queueBytes = tilePixelCapacity * RAY_RECORD_BYTES;
1115
1956
  const hitBytes = tilePixelCapacity * HIT_RECORD_BYTES;
1116
1957
  const accumulationBytes = tilePixelCapacity * ACCUMULATION_RECORD_BYTES;
1117
1958
  const pathVertexBytes = tilePixelCapacity * (maxDepth + 1) * PATH_VERTEX_RECORD_BYTES;
1118
1959
  const sceneObjectBytes = sceneObjectCapacity * SCENE_OBJECT_RECORD_BYTES;
1119
1960
  const triangleBytes = triangleCapacity * TRIANGLE_RECORD_BYTES;
1961
+ const materialTableBytes = materialCapacity * GPU_MATERIAL_RECORD_BYTES;
1120
1962
  const bvhNodeBytes = bvhNodeCapacity * BVH_NODE_RECORD_BYTES;
1121
1963
  const bvhLeafReferenceBytes = bvhLeafSortCapacity * BVH_LEAF_REF_RECORD_BYTES;
1122
1964
  const emissiveTriangleMetadataBytes = emissiveTriangleCapacity * BVH_NODE_RECORD_BYTES;
@@ -1129,6 +1971,7 @@ function estimateWavefrontPathTracingMemory(options = {}) {
1129
1971
  pathVertexBytes,
1130
1972
  sceneObjectBytes,
1131
1973
  triangleBytes,
1974
+ materialTableBytes,
1132
1975
  bvhNodeBytes,
1133
1976
  bvhLeafReferenceBytes,
1134
1977
  emissiveTriangleMetadataBytes,
@@ -1136,7 +1979,7 @@ function estimateWavefrontPathTracingMemory(options = {}) {
1136
1979
  configBytes: CONFIG_BUFFER_BYTES,
1137
1980
  counterBytes: COUNTER_BUFFER_BYTES,
1138
1981
  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
1982
+ totalHotBufferBytes: queueBytes * 2 + hitBytes + accumulationBytes + pathVertexBytes + sceneObjectBytes + triangleBytes + materialTableBytes + bvhNodeBytes + bvhLeafReferenceBytes + emissiveTriangleMetadataBytes + environmentPortalBytes + CONFIG_BUFFER_BYTES + COUNTER_BUFFER_BYTES + INDIRECT_DISPATCH_ARGS_BYTES
1140
1983
  });
1141
1984
  }
1142
1985
  function createWavefrontPathTracingComputeConfig(options = {}) {
@@ -1150,7 +1993,7 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1150
1993
  const samplesPerPixel = clamp(
1151
1994
  readPositiveInteger("samplesPerPixel", options.samplesPerPixel, DEFAULT_SAMPLES_PER_PIXEL),
1152
1995
  1,
1153
- 64
1996
+ MAX_SAMPLES_PER_PIXEL
1154
1997
  );
1155
1998
  const maxFramePassesPerSubmission = clamp(
1156
1999
  readPositiveInteger(
@@ -1168,7 +2011,8 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1168
2011
  );
1169
2012
  const meshes = normalizeMeshes(options);
1170
2013
  const meshSourceShape = estimateMeshSourceShape(meshes);
1171
- const gpuMeshSource = meshes.length > 0 ? createWavefrontGpuMeshSource(meshes) : createWavefrontGpuMeshSource([]);
2014
+ const gpuMaterialSource = meshes.length > 0 ? createWavefrontGpuMaterialSource(meshes) : createWavefrontGpuMaterialSource([]);
2015
+ const gpuMeshSource = meshes.length > 0 ? createWavefrontGpuMeshSource(meshes, gpuMaterialSource) : createWavefrontGpuMeshSource([]);
1172
2016
  const meshAcceleration = accelerationBuildMode === "cpu-debug" ? createWavefrontMeshAcceleration(meshes) : Object.freeze({ nodes: Object.freeze([]), triangles: Object.freeze([]) });
1173
2017
  const emissiveTriangleIndices = createWavefrontEmissiveTriangleIndexSource(
1174
2018
  meshes,
@@ -1179,6 +2023,7 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1179
2023
  const sceneObjects = Object.freeze(
1180
2024
  normalizeSceneObjects(options.sceneObjects, meshes.length === 0)
1181
2025
  );
2026
+ const mediums = collectWavefrontMediums(options, meshes, sceneObjects);
1182
2027
  const sceneObjectCapacity = Math.max(
1183
2028
  sceneObjects.length,
1184
2029
  readPositiveInteger("sceneObjectCapacity", options.sceneObjectCapacity, DEFAULT_SCENE_OBJECT_CAPACITY)
@@ -1237,9 +2082,12 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1237
2082
  sceneObjects,
1238
2083
  sceneObjectCount: sceneObjects.length,
1239
2084
  sceneObjectCapacity,
2085
+ mediums,
2086
+ mediumCount: mediums.length,
1240
2087
  accelerationBuildMode,
1241
2088
  gpuAccelerationBuildRequired: accelerationBuildMode === "gpu" && triangleCount > 0,
1242
2089
  gpuMeshSource,
2090
+ gpuMaterialSource,
1243
2091
  meshAcceleration,
1244
2092
  emissiveTriangleIndices,
1245
2093
  emissiveTriangleCount: emissiveTriangleIndices.count,
@@ -1270,6 +2118,7 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1270
2118
  maxDepth,
1271
2119
  sceneObjectCapacity,
1272
2120
  triangleCapacity,
2121
+ materialCapacity: gpuMaterialSource.count,
1273
2122
  bvhNodeCapacity,
1274
2123
  bvhLeafSortCapacity,
1275
2124
  emissiveTriangleCapacity: emissiveTriangleIndices.capacity,
@@ -1336,16 +2185,35 @@ function packWavefrontSceneObjects(sceneObjects, capacity = sceneObjects.length)
1336
2185
  uintView[u32 + 1] = object.id;
1337
2186
  uintView[u32 + 2] = object.materialKind;
1338
2187
  uintView[u32 + 3] = object.flags;
1339
- writeVec4(floatView, byteOffset + 16, [...object.center, 0]);
1340
- writeVec4(floatView, byteOffset + 32, [...object.halfExtent, 0]);
1341
- writeVec4(floatView, byteOffset + 48, object.color);
1342
- writeVec4(floatView, byteOffset + 64, object.emission);
1343
- writeVec4(floatView, byteOffset + 80, [
2188
+ uintView[u32 + 4] = object.mediumRefId;
2189
+ writeVec4(floatView, byteOffset + 32, [...object.center, 0]);
2190
+ writeVec4(floatView, byteOffset + 48, [...object.halfExtent, 0]);
2191
+ writeVec4(floatView, byteOffset + 64, object.color);
2192
+ writeVec4(floatView, byteOffset + 80, object.emission);
2193
+ writeVec4(floatView, byteOffset + 96, [
1344
2194
  object.roughness,
1345
2195
  object.metallic,
1346
2196
  object.opacity,
1347
2197
  object.ior
1348
2198
  ]);
2199
+ writeVec4(floatView, byteOffset + 112, [
2200
+ object.sheenColor[0] ?? 0,
2201
+ object.sheenColor[1] ?? 0,
2202
+ object.sheenColor[2] ?? 0,
2203
+ object.clearcoat
2204
+ ]);
2205
+ writeVec4(floatView, byteOffset + 128, [
2206
+ object.clearcoatRoughness,
2207
+ object.specular,
2208
+ object.transmission,
2209
+ object.thickness
2210
+ ]);
2211
+ writeVec4(floatView, byteOffset + 144, [
2212
+ object.specularColor[0] ?? 1,
2213
+ object.specularColor[1] ?? 1,
2214
+ object.specularColor[2] ?? 1,
2215
+ 1
2216
+ ]);
1349
2217
  });
1350
2218
  return Object.freeze({
1351
2219
  buffer: bytes,
@@ -1370,7 +2238,7 @@ function packWavefrontTriangles(triangles, capacity = triangles.length) {
1370
2238
  uintView[u32 + 3] = triangle.flags;
1371
2239
  uintView[u32 + 4] = triangle.materialRefId;
1372
2240
  uintView[u32 + 5] = triangle.mediumRefId;
1373
- uintView[u32 + 6] = 0;
2241
+ uintView[u32 + 6] = triangle.materialSlot ?? 0;
1374
2242
  uintView[u32 + 7] = 0;
1375
2243
  writeVec4(floatView, byteOffset + 32, [...triangle.v0, 0]);
1376
2244
  writeVec4(floatView, byteOffset + 48, [...triangle.v1, 0]);
@@ -1383,6 +2251,15 @@ function packWavefrontTriangles(triangles, capacity = triangles.length) {
1383
2251
  writeVec4(floatView, byteOffset + 160, triangle.color);
1384
2252
  writeVec4(floatView, byteOffset + 176, triangle.emission);
1385
2253
  writeVec4(floatView, byteOffset + 192, triangle.material);
2254
+ writeVec4(floatView, byteOffset + 208, triangle.materialResponse);
2255
+ writeVec4(floatView, byteOffset + 224, triangle.materialExtension ?? [0.08, 1, 0, 0]);
2256
+ writeVec4(floatView, byteOffset + 240, triangle.specularColor ?? [1, 1, 1, 1]);
2257
+ writeVec4(floatView, byteOffset + 256, triangle.baseColorAtlas ?? [0, 0, 1, 1]);
2258
+ writeVec4(floatView, byteOffset + 272, triangle.metallicRoughnessAtlas ?? [0, 0, 1, 1]);
2259
+ writeVec4(floatView, byteOffset + 288, triangle.normalAtlas ?? [0, 0, 1, 1]);
2260
+ writeVec4(floatView, byteOffset + 304, triangle.occlusionAtlas ?? [0, 0, 1, 1]);
2261
+ writeVec4(floatView, byteOffset + 320, triangle.emissiveAtlas ?? [0, 0, 1, 1]);
2262
+ writeVec4(floatView, byteOffset + 336, triangle.textureSettings ?? [1, 1, 1, 0]);
1386
2263
  });
1387
2264
  return Object.freeze({
1388
2265
  buffer: bytes,
@@ -1476,6 +2353,12 @@ function createConfigPayload(config, tile, frameIndex, buildRange = {}) {
1476
2353
  0,
1477
2354
  0
1478
2355
  ]);
2356
+ writeVec4(floatView, 304, [
2357
+ config.environmentMap.width ?? 1,
2358
+ config.environmentMap.height ?? 1,
2359
+ config.environmentMap.mipLevelCount ?? 1,
2360
+ config.environmentMap.hasImportanceData ? 1 : 0
2361
+ ]);
1479
2362
  return bytes;
1480
2363
  }
1481
2364
  function createTiles(width, height, tileSize) {
@@ -1649,7 +2532,8 @@ function intersectWavefrontReferenceTriangle(ray, triangle, options = {}) {
1649
2532
  position: Object.freeze(position),
1650
2533
  color: triangle.color,
1651
2534
  emission: triangle.emission,
1652
- material: triangle.material
2535
+ material: triangle.material,
2536
+ materialResponse: triangle.materialResponse
1653
2537
  });
1654
2538
  }
1655
2539
  function createWavefrontReferenceEnvironmentHit(config, ray) {
@@ -1675,7 +2559,8 @@ function createWavefrontReferenceEnvironmentHit(config, ray) {
1675
2559
  position: Object.freeze(add(ray.origin, scale(ray.direction, 1e3))),
1676
2560
  color: Object.freeze([0, 0, 0, 0]),
1677
2561
  emission: radiance,
1678
- material: Object.freeze([1, 0, 1, 1])
2562
+ material: Object.freeze([1, 0, 1, 1]),
2563
+ materialResponse: Object.freeze([0, 0, 0, 0])
1679
2564
  });
1680
2565
  }
1681
2566
  function traceWavefrontReferenceTriangles(config, ray, triangles, options = {}) {
@@ -1754,6 +2639,32 @@ function environmentMapIntegerScale(data) {
1754
2639
  }
1755
2640
  return 1;
1756
2641
  }
2642
+ function environmentMapHasSamplingData(environmentMap) {
2643
+ if (!environmentMap || !environmentMap.data) {
2644
+ return false;
2645
+ }
2646
+ const width = Math.max(1, environmentMap.width ?? 1);
2647
+ const height = Math.max(1, environmentMap.height ?? 1);
2648
+ return environmentMap.data.length >= width * height * 4;
2649
+ }
2650
+ function createRgba8TextureUpload(source) {
2651
+ const width = Math.max(1, Math.trunc(source.width));
2652
+ const height = Math.max(1, Math.trunc(source.height));
2653
+ const bytesPerRow = alignTo(width * 4, 256);
2654
+ const bytes = new Uint8Array(bytesPerRow * height);
2655
+ const data = source.data instanceof Uint8Array ? source.data : new Uint8Array(source.data);
2656
+ for (let y = 0; y < height; y += 1) {
2657
+ const sourceOffset = y * width * 4;
2658
+ const targetOffset = y * bytesPerRow;
2659
+ bytes.set(data.subarray(sourceOffset, sourceOffset + width * 4), targetOffset);
2660
+ }
2661
+ return Object.freeze({
2662
+ bytes,
2663
+ bytesPerRow,
2664
+ width,
2665
+ height
2666
+ });
2667
+ }
1757
2668
  function readEnvironmentMapComponent(data, index, fallback, integerScale = 1) {
1758
2669
  if (!data || index >= data.length) {
1759
2670
  return fallback;
@@ -1761,39 +2672,311 @@ function readEnvironmentMapComponent(data, index, fallback, integerScale = 1) {
1761
2672
  const value = Number(data[index]);
1762
2673
  return Number.isFinite(value) ? Math.max(0, value) * integerScale : fallback;
1763
2674
  }
1764
- function createEnvironmentMapUploadBytes(environmentMap, fallbackColor) {
1765
- const width = Math.max(1, environmentMap.width);
1766
- const height = Math.max(1, environmentMap.height);
2675
+ function buildOrthonormalBasis(normal) {
2676
+ const tangentFallback = Math.abs(normal[1]) < 0.999 ? [0, 1, 0] : [1, 0, 0];
2677
+ const tangent = normalize(cross(tangentFallback, normal), [1, 0, 0]);
2678
+ const bitangent = normalize(cross(normal, tangent), [0, 0, 1]);
2679
+ return { tangent, bitangent };
2680
+ }
2681
+ function localToWorld(local, normal) {
2682
+ const basis = buildOrthonormalBasis(normal);
2683
+ return normalize(
2684
+ add(
2685
+ add(scale(basis.tangent, local[0]), scale(basis.bitangent, local[1])),
2686
+ scale(normal, local[2])
2687
+ ),
2688
+ normal
2689
+ );
2690
+ }
2691
+ function radicalInverseVdc(bits) {
2692
+ let value = bits >>> 0;
2693
+ value = (value << 16 | value >>> 16) >>> 0;
2694
+ value = ((value & 1431655765) << 1 | (value & 2863311530) >>> 1) >>> 0;
2695
+ value = ((value & 858993459) << 2 | (value & 3435973836) >>> 2) >>> 0;
2696
+ value = ((value & 252645135) << 4 | (value & 4042322160) >>> 4) >>> 0;
2697
+ value = ((value & 16711935) << 8 | (value & 4278255360) >>> 8) >>> 0;
2698
+ return value * 23283064365386963e-26;
2699
+ }
2700
+ function hammersley(index, count) {
2701
+ return [index / Math.max(count, 1), radicalInverseVdc(index)];
2702
+ }
2703
+ function importanceSampleGgx(sample, roughness, normal) {
2704
+ const alpha = Math.max(roughness * roughness, 1e-4);
2705
+ const phi = 2 * Math.PI * sample[0];
2706
+ const cosTheta = Math.sqrt((1 - sample[1]) / (1 + (alpha * alpha - 1) * sample[1]));
2707
+ const sinTheta = Math.sqrt(Math.max(0, 1 - cosTheta * cosTheta));
2708
+ const halfVector = localToWorld(
2709
+ [Math.cos(phi) * sinTheta, Math.sin(phi) * sinTheta, cosTheta],
2710
+ normal
2711
+ );
2712
+ return normalize(halfVector, normal);
2713
+ }
2714
+ function geometrySchlickGgx(nDotV, roughness) {
2715
+ const k = (roughness + 1) * (roughness + 1) / 8;
2716
+ return nDotV / Math.max(nDotV * (1 - k) + k, 1e-6);
2717
+ }
2718
+ function geometrySmith(nDotV, nDotL, roughness) {
2719
+ return geometrySchlickGgx(nDotV, roughness) * geometrySchlickGgx(nDotL, roughness);
2720
+ }
2721
+ function integrateBrdfSample(nDotV, roughness, sampleCount) {
2722
+ const viewDirection = [Math.sqrt(Math.max(0, 1 - nDotV * nDotV)), 0, nDotV];
2723
+ const normal = [0, 0, 1];
2724
+ let scaleTerm = 0;
2725
+ let biasTerm = 0;
2726
+ for (let index = 0; index < sampleCount; index += 1) {
2727
+ const xi = hammersley(index, sampleCount);
2728
+ const halfVector = importanceSampleGgx(xi, roughness, normal);
2729
+ const vDotH = Math.max(dot(viewDirection, halfVector), 0);
2730
+ const lightDirection = normalize(
2731
+ subtract(scale(halfVector, 2 * vDotH), viewDirection),
2732
+ normal
2733
+ );
2734
+ const nDotL = Math.max(lightDirection[2], 0);
2735
+ const nDotH = Math.max(halfVector[2], 0);
2736
+ if (nDotL <= 0 || nDotH <= 0 || vDotH <= 0) {
2737
+ continue;
2738
+ }
2739
+ const geometry = geometrySmith(nDotV, nDotL, roughness);
2740
+ const visibility = geometry * vDotH / Math.max(nDotH * nDotV, 1e-6);
2741
+ const fresnel = (1 - vDotH) ** 5;
2742
+ scaleTerm += (1 - fresnel) * visibility;
2743
+ biasTerm += fresnel * visibility;
2744
+ }
2745
+ return [scaleTerm / sampleCount, biasTerm / sampleCount];
2746
+ }
2747
+ function createBrdfLutUploadBytes(size = DEFAULT_BRDF_LUT_SIZE, sampleCount = DEFAULT_BRDF_LUT_SAMPLE_COUNT) {
2748
+ const cacheKey = `${Math.max(1, Math.trunc(size))}:${Math.max(1, Math.trunc(sampleCount))}`;
2749
+ const cached = BRDF_LUT_UPLOAD_CACHE.get(cacheKey);
2750
+ if (cached) {
2751
+ return cached;
2752
+ }
2753
+ const width = Math.max(1, Math.trunc(size));
2754
+ const height = Math.max(1, Math.trunc(size));
1767
2755
  const rowBytes = width * 8;
1768
2756
  const bytesPerRow = alignTo(rowBytes, 256);
1769
2757
  const bytes = new Uint8Array(bytesPerRow * height);
2758
+ const view = new DataView(bytes.buffer);
2759
+ for (let y = 0; y < height; y += 1) {
2760
+ const roughness = (y + 0.5) / height;
2761
+ for (let x = 0; x < width; x += 1) {
2762
+ const nDotV = Math.max((x + 0.5) / width, 1e-4);
2763
+ const [scaleTerm, biasTerm] = integrateBrdfSample(nDotV, roughness, sampleCount);
2764
+ const offset = y * bytesPerRow + x * 8;
2765
+ view.setUint16(offset, float32ToFloat16Bits(scaleTerm), true);
2766
+ view.setUint16(offset + 2, float32ToFloat16Bits(biasTerm), true);
2767
+ view.setUint16(offset + 4, float32ToFloat16Bits(0), true);
2768
+ view.setUint16(offset + 6, float32ToFloat16Bits(1), true);
2769
+ }
2770
+ }
2771
+ const upload = Object.freeze({ bytes, bytesPerRow, width, height });
2772
+ BRDF_LUT_UPLOAD_CACHE.set(cacheKey, upload);
2773
+ return upload;
2774
+ }
2775
+ function createLinearEnvironmentPixels(environmentMap, fallbackColor) {
2776
+ const width = Math.max(1, environmentMap.width);
2777
+ const height = Math.max(1, environmentMap.height);
2778
+ const pixels = new Float32Array(width * height * 4);
1770
2779
  const data = environmentMap.data;
1771
2780
  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
- );
2781
+ for (let index = 0; index < width * height; index += 1) {
2782
+ const sourceOffset = index * 4;
2783
+ const targetOffset = index * 4;
2784
+ pixels[targetOffset] = readEnvironmentMapComponent(data, sourceOffset, fallbackColor[0], integerScale);
2785
+ pixels[targetOffset + 1] = readEnvironmentMapComponent(data, sourceOffset + 1, fallbackColor[1], integerScale);
2786
+ pixels[targetOffset + 2] = readEnvironmentMapComponent(data, sourceOffset + 2, fallbackColor[2], integerScale);
2787
+ pixels[targetOffset + 3] = readEnvironmentMapComponent(data, sourceOffset + 3, fallbackColor[3] ?? 1, integerScale);
2788
+ }
2789
+ return pixels;
2790
+ }
2791
+ function environmentUvToDirection(u, v, rotationRadians = 0) {
2792
+ const angle = (u - rotationRadians / (2 * Math.PI) - 0.5) * 2 * Math.PI;
2793
+ const theta = v * Math.PI;
2794
+ const sinTheta = Math.sin(theta);
2795
+ return [
2796
+ Math.cos(angle) * sinTheta,
2797
+ Math.cos(theta),
2798
+ Math.sin(angle) * sinTheta
2799
+ ];
2800
+ }
2801
+ function sampleEnvironmentPixelsBilinear(pixels, width, height, u, v) {
2802
+ const wrappedU = (u % 1 + 1) % 1;
2803
+ const clampedV = clamp(v, 0, 1);
2804
+ const x = wrappedU * width - 0.5;
2805
+ const y = clampedV * height - 0.5;
2806
+ const x0 = (Math.floor(x) % width + width) % width;
2807
+ const y0 = clamp(Math.floor(y), 0, height - 1);
2808
+ const x1 = (x0 + 1) % width;
2809
+ const y1 = clamp(y0 + 1, 0, height - 1);
2810
+ const tx = x - Math.floor(x);
2811
+ const ty = y - Math.floor(y);
2812
+ const read = (px, py) => {
2813
+ const offset = (py * width + px) * 4;
2814
+ return [pixels[offset], pixels[offset + 1], pixels[offset + 2], pixels[offset + 3]];
1781
2815
  };
2816
+ const a = read(x0, y0);
2817
+ const b = read(x1, y0);
2818
+ const c = read(x0, y1);
2819
+ const d = read(x1, y1);
2820
+ const mixPair = (first, second, factor) => first * (1 - factor) + second * factor;
2821
+ return [
2822
+ mixPair(mixPair(a[0], b[0], tx), mixPair(c[0], d[0], tx), ty),
2823
+ mixPair(mixPair(a[1], b[1], tx), mixPair(c[1], d[1], tx), ty),
2824
+ mixPair(mixPair(a[2], b[2], tx), mixPair(c[2], d[2], tx), ty),
2825
+ mixPair(mixPair(a[3], b[3], tx), mixPair(c[3], d[3], tx), ty)
2826
+ ];
2827
+ }
2828
+ function directionToEnvironmentUv(direction, rotationRadians = 0) {
2829
+ const unitDirection = normalize(direction, [0, 1, 0]);
2830
+ const rotationTurns = rotationRadians / (2 * Math.PI);
2831
+ const u = ((Math.atan2(unitDirection[2], unitDirection[0]) / (2 * Math.PI) + 0.5 + rotationTurns) % 1 + 1) % 1;
2832
+ const v = Math.acos(clamp(unitDirection[1], -1, 1)) / Math.PI;
2833
+ return [u, clamp(v, 0, 1)];
2834
+ }
2835
+ function sampleEnvironmentRadiance(pixels, width, height, direction, rotationRadians = 0) {
2836
+ const [u, v] = directionToEnvironmentUv(direction, rotationRadians);
2837
+ return sampleEnvironmentPixelsBilinear(pixels, width, height, u, v);
2838
+ }
2839
+ function createFloat16RgbaUploadFromLevels(levels) {
2840
+ return levels.map((level) => {
2841
+ const rowBytes = level.width * 8;
2842
+ const bytesPerRow = alignTo(rowBytes, 256);
2843
+ const bytes = new Uint8Array(bytesPerRow * level.height);
2844
+ const view = new DataView(bytes.buffer);
2845
+ for (let y = 0; y < level.height; y += 1) {
2846
+ for (let x = 0; x < level.width; x += 1) {
2847
+ const sourceOffset = (y * level.width + x) * 4;
2848
+ const targetOffset = y * bytesPerRow + x * 8;
2849
+ view.setUint16(targetOffset, float32ToFloat16Bits(level.data[sourceOffset]), true);
2850
+ view.setUint16(targetOffset + 2, float32ToFloat16Bits(level.data[sourceOffset + 1]), true);
2851
+ view.setUint16(targetOffset + 4, float32ToFloat16Bits(level.data[sourceOffset + 2]), true);
2852
+ view.setUint16(targetOffset + 6, float32ToFloat16Bits(level.data[sourceOffset + 3]), true);
2853
+ }
2854
+ }
2855
+ return Object.freeze({ bytes, bytesPerRow, width: level.width, height: level.height });
2856
+ });
2857
+ }
2858
+ function createPrefilteredEnvironmentLevels(environmentMap, fallbackColor) {
2859
+ const sourcePixels = createLinearEnvironmentPixels(environmentMap, fallbackColor);
2860
+ const sourceWidth = Math.max(1, environmentMap.width);
2861
+ const sourceHeight = Math.max(1, environmentMap.height);
2862
+ const mipLevelCount = Math.max(1, Math.floor(Math.log2(Math.max(sourceWidth, sourceHeight))) + 1);
2863
+ const levels = [
2864
+ Object.freeze({
2865
+ width: sourceWidth,
2866
+ height: sourceHeight,
2867
+ data: sourcePixels
2868
+ })
2869
+ ];
2870
+ for (let mipLevel = 1; mipLevel < mipLevelCount; mipLevel += 1) {
2871
+ const width = Math.max(1, sourceWidth >> mipLevel);
2872
+ const height = Math.max(1, sourceHeight >> mipLevel);
2873
+ const roughness = mipLevelCount <= 1 ? 0 : mipLevel / (mipLevelCount - 1);
2874
+ const data = new Float32Array(width * height * 4);
2875
+ const sampleCount = roughness < 0.25 ? 64 : roughness < 0.6 ? 96 : 128;
2876
+ for (let y = 0; y < height; y += 1) {
2877
+ for (let x = 0; x < width; x += 1) {
2878
+ const direction = environmentUvToDirection((x + 0.5) / width, (y + 0.5) / height, environmentMap.rotationRadians);
2879
+ const normal = normalize(direction, [0, 1, 0]);
2880
+ const viewDirection = normal;
2881
+ let totalWeight = 0;
2882
+ const accum = [0, 0, 0];
2883
+ for (let sampleIndex = 0; sampleIndex < sampleCount; sampleIndex += 1) {
2884
+ const xi = hammersley(sampleIndex, sampleCount);
2885
+ const halfVector = importanceSampleGgx(xi, roughness, normal);
2886
+ const viewDotHalf = Math.max(dot(viewDirection, halfVector), 0);
2887
+ const lightDirection = normalize(
2888
+ subtract(scale(halfVector, 2 * viewDotHalf), viewDirection),
2889
+ normal
2890
+ );
2891
+ const nDotL = Math.max(dot(normal, lightDirection), 0);
2892
+ if (nDotL <= 1e-6) {
2893
+ continue;
2894
+ }
2895
+ const radiance = sampleEnvironmentRadiance(
2896
+ sourcePixels,
2897
+ sourceWidth,
2898
+ sourceHeight,
2899
+ lightDirection,
2900
+ environmentMap.rotationRadians
2901
+ );
2902
+ accum[0] += radiance[0] * nDotL;
2903
+ accum[1] += radiance[1] * nDotL;
2904
+ accum[2] += radiance[2] * nDotL;
2905
+ totalWeight += nDotL;
2906
+ }
2907
+ const offset = (y * width + x) * 4;
2908
+ data[offset] = accum[0] / Math.max(totalWeight, 1e-6);
2909
+ data[offset + 1] = accum[1] / Math.max(totalWeight, 1e-6);
2910
+ data[offset + 2] = accum[2] / Math.max(totalWeight, 1e-6);
2911
+ data[offset + 3] = 1;
2912
+ }
2913
+ }
2914
+ levels.push(Object.freeze({ width, height, data }));
2915
+ }
2916
+ return Object.freeze({
2917
+ levels,
2918
+ mipLevelCount,
2919
+ width: sourceWidth,
2920
+ height: sourceHeight
2921
+ });
2922
+ }
2923
+ function createEnvironmentSamplingTables(environmentMap, fallbackColor) {
2924
+ if (!environmentMapHasSamplingData(environmentMap)) {
2925
+ return Object.freeze({
2926
+ width: 1,
2927
+ height: 1,
2928
+ pdf: new Float32Array([1]),
2929
+ marginalCdf: new Float32Array([1]),
2930
+ conditionalCdf: new Float32Array([1]),
2931
+ hasImportanceData: false
2932
+ });
2933
+ }
2934
+ const pixels = createLinearEnvironmentPixels(environmentMap, fallbackColor);
2935
+ const width = Math.max(1, environmentMap.width);
2936
+ const height = Math.max(1, environmentMap.height);
2937
+ const pdf = new Float32Array(width * height);
2938
+ const marginalCdf = new Float32Array(height);
2939
+ const conditionalCdf = new Float32Array(width * height);
2940
+ const rowSums = new Float32Array(height);
2941
+ let totalWeight = 0;
1782
2942
  for (let y = 0; y < height; y += 1) {
2943
+ const theta = (y + 0.5) / height * Math.PI;
2944
+ const sinTheta = Math.max(Math.sin(theta), 1e-4);
2945
+ let rowWeight = 0;
1783
2946
  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);
2947
+ const offset = (y * width + x) * 4;
2948
+ const luminance = pixels[offset] * 0.2126 + pixels[offset + 1] * 0.7152 + pixels[offset + 2] * 0.0722;
2949
+ const weight = Math.max(luminance * sinTheta, 1e-6);
2950
+ pdf[y * width + x] = weight;
2951
+ rowWeight += weight;
2952
+ conditionalCdf[y * width + x] = rowWeight;
2953
+ }
2954
+ rowSums[y] = rowWeight;
2955
+ totalWeight += rowWeight;
2956
+ if (rowWeight > 0) {
2957
+ for (let x = 0; x < width; x += 1) {
2958
+ conditionalCdf[y * width + x] /= rowWeight;
2959
+ }
2960
+ } else {
2961
+ for (let x = 0; x < width; x += 1) {
2962
+ conditionalCdf[y * width + x] = (x + 1) / width;
2963
+ }
1790
2964
  }
2965
+ marginalCdf[y] = totalWeight;
2966
+ }
2967
+ for (let y = 0; y < height; y += 1) {
2968
+ marginalCdf[y] /= Math.max(totalWeight, 1e-6);
2969
+ }
2970
+ for (let index = 0; index < pdf.length; index += 1) {
2971
+ pdf[index] /= Math.max(totalWeight, 1e-6);
1791
2972
  }
1792
2973
  return Object.freeze({
1793
- bytes,
1794
- bytesPerRow,
1795
2974
  width,
1796
- height
2975
+ height,
2976
+ pdf,
2977
+ marginalCdf,
2978
+ conditionalCdf,
2979
+ hasImportanceData: true
1797
2980
  });
1798
2981
  }
1799
2982
  function createEnvironmentMapResource(device, constants, environmentMap, fallbackColor) {
@@ -1805,10 +2988,14 @@ function createEnvironmentMapResource(device, constants, environmentMap, fallbac
1805
2988
  addressModeU: "repeat",
1806
2989
  addressModeV: "clamp-to-edge",
1807
2990
  magFilter: "linear",
1808
- minFilter: "linear"
2991
+ minFilter: "linear",
2992
+ mipmapFilter: "linear"
1809
2993
  }),
1810
2994
  texture: null,
1811
- ownsTexture: false
2995
+ ownsTexture: false,
2996
+ width: Math.max(1, environmentMap.width),
2997
+ height: Math.max(1, environmentMap.height),
2998
+ mipLevelCount: Math.max(1, environmentMap.mipLevelCount ?? 1)
1812
2999
  });
1813
3000
  }
1814
3001
  if (environmentMap.texture && typeof environmentMap.texture.createView === "function") {
@@ -1819,15 +3006,91 @@ function createEnvironmentMapResource(device, constants, environmentMap, fallbac
1819
3006
  addressModeU: "repeat",
1820
3007
  addressModeV: "clamp-to-edge",
1821
3008
  magFilter: "linear",
1822
- minFilter: "linear"
3009
+ minFilter: "linear",
3010
+ mipmapFilter: "linear"
1823
3011
  }),
1824
3012
  texture: environmentMap.texture,
1825
- ownsTexture: false
3013
+ ownsTexture: false,
3014
+ width: Math.max(1, environmentMap.width),
3015
+ height: Math.max(1, environmentMap.height),
3016
+ mipLevelCount: Math.max(1, environmentMap.mipLevelCount ?? 1)
1826
3017
  });
1827
3018
  }
1828
- const upload = createEnvironmentMapUploadBytes(environmentMap, fallbackColor);
3019
+ const prefiltered = createPrefilteredEnvironmentLevels(environmentMap, fallbackColor);
3020
+ const uploads = createFloat16RgbaUploadFromLevels(prefiltered.levels);
1829
3021
  const texture = device.createTexture({
1830
3022
  label: environmentMap.enabled ? "plasius.wavefront.environmentMap" : "plasius.wavefront.environmentMapFallback",
3023
+ size: { width: prefiltered.width, height: prefiltered.height },
3024
+ format: "rgba16float",
3025
+ mipLevelCount: prefiltered.mipLevelCount,
3026
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
3027
+ });
3028
+ uploads.forEach((upload, mipLevel) => {
3029
+ device.queue.writeTexture(
3030
+ { texture, mipLevel },
3031
+ upload.bytes,
3032
+ { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
3033
+ { width: upload.width, height: upload.height, depthOrArrayLayers: 1 }
3034
+ );
3035
+ });
3036
+ return Object.freeze({
3037
+ view: texture.createView(),
3038
+ sampler: environmentMap.sampler ?? device.createSampler({
3039
+ label: "plasius.wavefront.environmentMapSampler",
3040
+ addressModeU: "repeat",
3041
+ addressModeV: "clamp-to-edge",
3042
+ magFilter: "linear",
3043
+ minFilter: "linear",
3044
+ mipmapFilter: "linear"
3045
+ }),
3046
+ texture,
3047
+ ownsTexture: true,
3048
+ width: prefiltered.width,
3049
+ height: prefiltered.height,
3050
+ mipLevelCount: prefiltered.mipLevelCount
3051
+ });
3052
+ }
3053
+ function createEnvironmentSamplingTextureResource(device, constants, environmentMap, fallbackColor) {
3054
+ const tables = createEnvironmentSamplingTables(environmentMap, fallbackColor);
3055
+ const rowBytes = tables.width * 8;
3056
+ const bytesPerRow = alignTo(rowBytes, 256);
3057
+ const bytes = new Uint8Array(bytesPerRow * tables.height);
3058
+ const view = new DataView(bytes.buffer);
3059
+ for (let y = 0; y < tables.height; y += 1) {
3060
+ for (let x = 0; x < tables.width; x += 1) {
3061
+ const probability = tables.pdf[y * tables.width + x];
3062
+ const conditional = tables.conditionalCdf[y * tables.width + x];
3063
+ const marginal = tables.marginalCdf[y];
3064
+ const offset = y * bytesPerRow + x * 8;
3065
+ view.setUint16(offset, float32ToFloat16Bits(probability), true);
3066
+ view.setUint16(offset + 2, float32ToFloat16Bits(conditional), true);
3067
+ view.setUint16(offset + 4, float32ToFloat16Bits(marginal), true);
3068
+ view.setUint16(offset + 6, float32ToFloat16Bits(1), true);
3069
+ }
3070
+ }
3071
+ const texture = device.createTexture({
3072
+ label: "plasius.wavefront.environmentSampling",
3073
+ size: { width: tables.width, height: tables.height },
3074
+ format: "rgba16float",
3075
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
3076
+ });
3077
+ device.queue.writeTexture(
3078
+ { texture },
3079
+ bytes,
3080
+ { bytesPerRow, rowsPerImage: tables.height },
3081
+ { width: tables.width, height: tables.height, depthOrArrayLayers: 1 }
3082
+ );
3083
+ return Object.freeze({
3084
+ view: texture.createView(),
3085
+ texture,
3086
+ ownsTexture: true,
3087
+ hasImportanceData: tables.hasImportanceData
3088
+ });
3089
+ }
3090
+ function createBrdfLutResource(device, constants, size = DEFAULT_BRDF_LUT_SIZE) {
3091
+ const upload = createBrdfLutUploadBytes(size);
3092
+ const texture = device.createTexture({
3093
+ label: "plasius.wavefront.brdfLut",
1831
3094
  size: { width: upload.width, height: upload.height },
1832
3095
  format: "rgba16float",
1833
3096
  usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
@@ -1840,14 +3103,110 @@ function createEnvironmentMapResource(device, constants, environmentMap, fallbac
1840
3103
  );
1841
3104
  return Object.freeze({
1842
3105
  view: texture.createView(),
1843
- sampler: environmentMap.sampler ?? device.createSampler({
1844
- label: "plasius.wavefront.environmentMapSampler",
1845
- addressModeU: "repeat",
3106
+ sampler: device.createSampler({
3107
+ label: "plasius.wavefront.brdfLutSampler",
3108
+ addressModeU: "clamp-to-edge",
1846
3109
  addressModeV: "clamp-to-edge",
1847
3110
  magFilter: "linear",
1848
3111
  minFilter: "linear"
1849
3112
  }),
1850
3113
  texture,
3114
+ ownsTexture: true,
3115
+ width: upload.width,
3116
+ height: upload.height
3117
+ });
3118
+ }
3119
+ function createMediumTextureResource(device, constants, mediums) {
3120
+ const normalized = Array.isArray(mediums) && mediums.length > 0 ? mediums : [{ id: 0 }];
3121
+ const width = Math.max(
3122
+ 1,
3123
+ normalized.reduce((maximum, medium) => Math.max(maximum, medium.id ?? 0), 0) + 1
3124
+ );
3125
+ const level = {
3126
+ width,
3127
+ height: MEDIUM_TABLE_ROWS,
3128
+ data: new Float32Array(width * MEDIUM_TABLE_ROWS * 4)
3129
+ };
3130
+ for (const medium of normalized) {
3131
+ const mediumId = Math.max(0, Math.trunc(Number(medium.id) || 0));
3132
+ const absorptionOffset = mediumId * 4;
3133
+ level.data[absorptionOffset] = Math.max(0, medium.absorption?.[0] ?? 0);
3134
+ level.data[absorptionOffset + 1] = Math.max(0, medium.absorption?.[1] ?? 0);
3135
+ level.data[absorptionOffset + 2] = Math.max(0, medium.absorption?.[2] ?? 0);
3136
+ level.data[absorptionOffset + 3] = Math.max(0, medium.phaseModel ?? 0);
3137
+ const scatteringOffset = (width + mediumId) * 4;
3138
+ level.data[scatteringOffset] = Math.max(0, medium.scattering?.[0] ?? 0);
3139
+ level.data[scatteringOffset + 1] = Math.max(0, medium.scattering?.[1] ?? 0);
3140
+ level.data[scatteringOffset + 2] = Math.max(0, medium.scattering?.[2] ?? 0);
3141
+ level.data[scatteringOffset + 3] = Math.max(0, medium.density ?? 0);
3142
+ }
3143
+ const upload = createFloat16RgbaUploadFromLevels([level])[0];
3144
+ const texture = device.createTexture({
3145
+ label: "plasius.wavefront.mediumTable",
3146
+ size: { width, height: MEDIUM_TABLE_ROWS },
3147
+ format: "rgba16float",
3148
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
3149
+ });
3150
+ device.queue.writeTexture(
3151
+ { texture },
3152
+ upload.bytes,
3153
+ { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
3154
+ { width, height: MEDIUM_TABLE_ROWS, depthOrArrayLayers: 1 }
3155
+ );
3156
+ return Object.freeze({
3157
+ texture,
3158
+ view: texture.createView(),
3159
+ ownsTexture: true,
3160
+ count: normalized.length,
3161
+ width
3162
+ });
3163
+ }
3164
+ function mediumTablesEqual(left, right) {
3165
+ const leftMediums = Array.isArray(left) ? left : [];
3166
+ const rightMediums = Array.isArray(right) ? right : [];
3167
+ if (leftMediums.length !== rightMediums.length) {
3168
+ return false;
3169
+ }
3170
+ for (let index = 0; index < leftMediums.length; index += 1) {
3171
+ const leftMedium = leftMediums[index];
3172
+ const rightMedium = rightMediums[index];
3173
+ if ((leftMedium?.id ?? 0) !== (rightMedium?.id ?? 0)) {
3174
+ return false;
3175
+ }
3176
+ if ((leftMedium?.phaseModel ?? 0) !== (rightMedium?.phaseModel ?? 0)) {
3177
+ return false;
3178
+ }
3179
+ if ((leftMedium?.density ?? 0) !== (rightMedium?.density ?? 0)) {
3180
+ return false;
3181
+ }
3182
+ for (let component = 0; component < 3; component += 1) {
3183
+ if ((leftMedium?.absorption?.[component] ?? 0) !== (rightMedium?.absorption?.[component] ?? 0)) {
3184
+ return false;
3185
+ }
3186
+ if ((leftMedium?.scattering?.[component] ?? 0) !== (rightMedium?.scattering?.[component] ?? 0)) {
3187
+ return false;
3188
+ }
3189
+ }
3190
+ }
3191
+ return true;
3192
+ }
3193
+ function createAtlasTextureResource(device, constants, atlas, label) {
3194
+ const upload = createRgba8TextureUpload(atlas);
3195
+ const texture = device.createTexture({
3196
+ label,
3197
+ size: { width: upload.width, height: upload.height },
3198
+ format: "rgba8unorm",
3199
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
3200
+ });
3201
+ device.queue.writeTexture(
3202
+ { texture },
3203
+ upload.bytes,
3204
+ { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
3205
+ { width: upload.width, height: upload.height, depthOrArrayLayers: 1 }
3206
+ );
3207
+ return Object.freeze({
3208
+ texture,
3209
+ view: texture.createView(),
1851
3210
  ownsTexture: true
1852
3211
  });
1853
3212
  }
@@ -1893,6 +3252,24 @@ ${diagnostics}` : "";
1893
3252
  });
1894
3253
  }
1895
3254
  }
3255
+ async function assertShaderModuleCompiles(shaderModule, label) {
3256
+ if (typeof shaderModule?.compilationInfo !== "function") {
3257
+ return;
3258
+ }
3259
+ const info = await shaderModule.compilationInfo();
3260
+ const messages = Array.isArray(info?.messages) ? info.messages : [];
3261
+ const errors = messages.filter((message) => message?.type === "error");
3262
+ if (errors.length <= 0) {
3263
+ return;
3264
+ }
3265
+ const diagnostics = errors.map((message) => {
3266
+ const line = Number.isFinite(message.lineNum) ? message.lineNum : "?";
3267
+ const column = Number.isFinite(message.linePos) ? message.linePos : "?";
3268
+ return `line ${line}:${column} ${message.message}`;
3269
+ }).join("\n");
3270
+ throw new Error(`WGSL compilation preflight failed for ${label}:
3271
+ ${diagnostics}`);
3272
+ }
1896
3273
  async function createRenderPipeline(device, descriptor) {
1897
3274
  if (typeof device.createRenderPipelineAsync === "function") {
1898
3275
  return device.createRenderPipelineAsync(descriptor);
@@ -1901,6 +3278,7 @@ async function createRenderPipeline(device, descriptor) {
1901
3278
  }
1902
3279
  var WAVEFRONT_COMPUTE_WGSL = `
1903
3280
  const RAY_FLAG_GUIDED_EMISSIVE: u32 = 1u;
3281
+ const RAY_FLAG_DELTA_SAMPLE: u32 = 2u;
1904
3282
 
1905
3283
  struct RayRecord {
1906
3284
  rayId: u32,
@@ -1926,11 +3304,12 @@ struct HitRecord {
1926
3304
  primitiveId: u32,
1927
3305
  materialRefId: u32,
1928
3306
  mediumRefId: u32,
3307
+ materialSlot: u32,
1929
3308
  pad0: u32,
1930
3309
  pad1: u32,
1931
- pad2: u32,
1932
3310
  distance: f32,
1933
- pad3: vec3<f32>,
3311
+ occlusion: f32,
3312
+ pad2: vec2<f32>,
1934
3313
  position: vec4<f32>,
1935
3314
  geometricNormal: vec4<f32>,
1936
3315
  shadingNormal: vec4<f32>,
@@ -1939,6 +3318,9 @@ struct HitRecord {
1939
3318
  color: vec4<f32>,
1940
3319
  emission: vec4<f32>,
1941
3320
  material: vec4<f32>,
3321
+ materialResponse: vec4<f32>,
3322
+ materialExtension: vec4<f32>,
3323
+ specularColor: vec4<f32>,
1942
3324
  };
1943
3325
 
1944
3326
  struct SceneObject {
@@ -1946,11 +3328,18 @@ struct SceneObject {
1946
3328
  objectId: u32,
1947
3329
  materialKind: u32,
1948
3330
  flags: u32,
3331
+ mediumRefId: u32,
3332
+ pad0: u32,
3333
+ pad1: u32,
3334
+ pad2: u32,
1949
3335
  center: vec4<f32>,
1950
3336
  halfExtent: vec4<f32>,
1951
3337
  color: vec4<f32>,
1952
3338
  emission: vec4<f32>,
1953
3339
  material: vec4<f32>,
3340
+ materialResponse: vec4<f32>,
3341
+ materialExtension: vec4<f32>,
3342
+ specularColor: vec4<f32>,
1954
3343
  };
1955
3344
 
1956
3345
  struct TriangleRecord {
@@ -1960,7 +3349,7 @@ struct TriangleRecord {
1960
3349
  flags: u32,
1961
3350
  materialRefId: u32,
1962
3351
  mediumRefId: u32,
1963
- pad0: u32,
3352
+ materialSlot: u32,
1964
3353
  pad1: u32,
1965
3354
  v0: vec4<f32>,
1966
3355
  v1: vec4<f32>,
@@ -1973,6 +3362,15 @@ struct TriangleRecord {
1973
3362
  color: vec4<f32>,
1974
3363
  emission: vec4<f32>,
1975
3364
  material: vec4<f32>,
3365
+ materialResponse: vec4<f32>,
3366
+ materialExtension: vec4<f32>,
3367
+ specularColor: vec4<f32>,
3368
+ baseColorAtlas: vec4<f32>,
3369
+ metallicRoughnessAtlas: vec4<f32>,
3370
+ normalAtlas: vec4<f32>,
3371
+ occlusionAtlas: vec4<f32>,
3372
+ emissiveAtlas: vec4<f32>,
3373
+ textureSettings: vec4<f32>,
1976
3374
  };
1977
3375
 
1978
3376
  struct BvhNode {
@@ -1993,10 +3391,10 @@ struct BvhLeafRef {
1993
3391
 
1994
3392
  struct ScatterResult {
1995
3393
  direction: vec4<f32>,
3394
+ pdf: f32,
3395
+ mediumRefId: u32,
1996
3396
  flags: u32,
1997
3397
  pad0: u32,
1998
- pad1: u32,
1999
- pad2: u32,
2000
3398
  };
2001
3399
 
2002
3400
  struct MeshVertex {
@@ -2017,10 +3415,19 @@ struct MeshRange {
2017
3415
  triangleCount: u32,
2018
3416
  firstVertex: u32,
2019
3417
  vertexCount: u32,
2020
- pad0: u32,
3418
+ materialSlot: u32,
2021
3419
  color: vec4<f32>,
2022
3420
  emission: vec4<f32>,
2023
3421
  material: vec4<f32>,
3422
+ materialResponse: vec4<f32>,
3423
+ materialExtension: vec4<f32>,
3424
+ specularColor: vec4<f32>,
3425
+ baseColorAtlas: vec4<f32>,
3426
+ metallicRoughnessAtlas: vec4<f32>,
3427
+ normalAtlas: vec4<f32>,
3428
+ occlusionAtlas: vec4<f32>,
3429
+ emissiveAtlas: vec4<f32>,
3430
+ textureSettings: vec4<f32>,
2024
3431
  };
2025
3432
 
2026
3433
  struct FrameConfig {
@@ -2061,6 +3468,7 @@ struct FrameConfig {
2061
3468
  _portalPad1: u32,
2062
3469
  environmentMapSettings: vec4<f32>,
2063
3470
  pathResolveSettings: vec4<f32>,
3471
+ environmentMapMeta: vec4<f32>,
2064
3472
  };
2065
3473
 
2066
3474
  struct Counters {
@@ -2123,6 +3531,16 @@ struct EnvironmentPortal {
2123
3531
  @group(0) @binding(20) var environmentMapTexture: texture_2d<f32>;
2124
3532
  @group(0) @binding(21) var environmentMapSampler: sampler;
2125
3533
  @group(0) @binding(22) var<storage, read_write> pathVertices: array<vec4<f32>>;
3534
+ @group(0) @binding(23) var baseColorAtlasTexture: texture_2d<f32>;
3535
+ @group(0) @binding(24) var metallicRoughnessAtlasTexture: texture_2d<f32>;
3536
+ @group(0) @binding(25) var normalAtlasTexture: texture_2d<f32>;
3537
+ @group(0) @binding(26) var occlusionAtlasTexture: texture_2d<f32>;
3538
+ @group(0) @binding(27) var emissiveAtlasTexture: texture_2d<f32>;
3539
+ @group(0) @binding(28) var materialAtlasSampler: sampler;
3540
+ @group(0) @binding(29) var brdfLutTexture: texture_2d<f32>;
3541
+ @group(0) @binding(30) var brdfLutSampler: sampler;
3542
+ @group(0) @binding(31) var environmentSamplingTexture: texture_2d<f32>;
3543
+ @group(0) @binding(32) var mediumTableTexture: texture_2d<f32>;
2126
3544
 
2127
3545
  fn hash_u32(value: u32) -> u32 {
2128
3546
  var x = value;
@@ -2159,6 +3577,146 @@ fn safe_normalize(value: vec3<f32>, fallback: vec3<f32>) -> vec3<f32> {
2159
3577
  return value / len;
2160
3578
  }
2161
3579
 
3580
+ struct TangentBasis {
3581
+ tangent: vec3<f32>,
3582
+ bitangent: vec3<f32>,
3583
+ };
3584
+
3585
+ struct SurfaceMaterialSample {
3586
+ color: vec4<f32>,
3587
+ emission: vec4<f32>,
3588
+ material: vec4<f32>,
3589
+ materialResponse: vec4<f32>,
3590
+ materialExtension: vec4<f32>,
3591
+ specularColor: vec4<f32>,
3592
+ shadingNormal: vec3<f32>,
3593
+ occlusion: f32,
3594
+ };
3595
+
3596
+ fn srgb_to_linear_channel(value: f32) -> f32 {
3597
+ if (value <= 0.04045) {
3598
+ return value / 12.92;
3599
+ }
3600
+ return pow((value + 0.055) / 1.055, 2.4);
3601
+ }
3602
+
3603
+ fn srgb_to_linear_vec3(value: vec3<f32>) -> vec3<f32> {
3604
+ return vec3<f32>(
3605
+ srgb_to_linear_channel(value.x),
3606
+ srgb_to_linear_channel(value.y),
3607
+ srgb_to_linear_channel(value.z)
3608
+ );
3609
+ }
3610
+
3611
+ fn wrap_uv(uv: vec2<f32>) -> vec2<f32> {
3612
+ return fract(fract(uv) + vec2<f32>(1.0));
3613
+ }
3614
+
3615
+ fn atlas_sample_uv(rect: vec4<f32>, uv: vec2<f32>) -> vec2<f32> {
3616
+ let local = wrap_uv(uv);
3617
+ let clamped = clamp(local, vec2<f32>(0.001), vec2<f32>(0.999));
3618
+ return rect.xy + clamped * rect.zw;
3619
+ }
3620
+
3621
+ fn sample_atlas(textureRef: texture_2d<f32>, rect: vec4<f32>, uv: vec2<f32>) -> vec4<f32> {
3622
+ return textureSampleLevel(textureRef, materialAtlasSampler, atlas_sample_uv(rect, uv), 0.0);
3623
+ }
3624
+
3625
+ fn build_triangle_tangent_basis(
3626
+ triangle: TriangleRecord,
3627
+ fallbackNormal: vec3<f32>
3628
+ ) -> TangentBasis {
3629
+ let edge1 = triangle.v1.xyz - triangle.v0.xyz;
3630
+ let edge2 = triangle.v2.xyz - triangle.v0.xyz;
3631
+ let uv0 = triangle.uv0uv1.xy;
3632
+ let uv1 = triangle.uv0uv1.zw;
3633
+ let uv2 = triangle.uv2Pad.xy;
3634
+ let deltaUv1 = uv1 - uv0;
3635
+ let deltaUv2 = uv2 - uv0;
3636
+ let determinant = deltaUv1.x * deltaUv2.y - deltaUv1.y * deltaUv2.x;
3637
+ if (abs(determinant) <= 0.000001) {
3638
+ let tangentFallback = select(vec3<f32>(0.0, 1.0, 0.0), vec3<f32>(1.0, 0.0, 0.0), abs(fallbackNormal.y) >= 0.999);
3639
+ let tangent = safe_normalize(cross(tangentFallback, fallbackNormal), vec3<f32>(1.0, 0.0, 0.0));
3640
+ let bitangent = safe_normalize(cross(fallbackNormal, tangent), vec3<f32>(0.0, 0.0, 1.0));
3641
+ return TangentBasis(tangent, bitangent);
3642
+ }
3643
+ let inverse = 1.0 / determinant;
3644
+ let tangent = safe_normalize(
3645
+ inverse * (edge1 * deltaUv2.y - edge2 * deltaUv1.y),
3646
+ vec3<f32>(1.0, 0.0, 0.0)
3647
+ );
3648
+ let bitangent = safe_normalize(
3649
+ inverse * (-edge1 * deltaUv2.x + edge2 * deltaUv1.x),
3650
+ vec3<f32>(0.0, 0.0, 1.0)
3651
+ );
3652
+ return TangentBasis(tangent, bitangent);
3653
+ }
3654
+
3655
+ fn sample_surface_material(
3656
+ triangle: TriangleRecord,
3657
+ uv: vec2<f32>,
3658
+ geometricNormal: vec3<f32>,
3659
+ shadingNormal: vec3<f32>
3660
+ ) -> SurfaceMaterialSample {
3661
+ let baseColorTexel = sample_atlas(baseColorAtlasTexture, triangle.baseColorAtlas, uv);
3662
+ let baseColor = vec4<f32>(
3663
+ clamp(triangle.color.rgb * srgb_to_linear_vec3(baseColorTexel.rgb), vec3<f32>(0.0), vec3<f32>(1.0)),
3664
+ clamp(triangle.color.a * baseColorTexel.a, 0.0, 1.0)
3665
+ );
3666
+ let metallicRoughnessTexel = sample_atlas(
3667
+ metallicRoughnessAtlasTexture,
3668
+ triangle.metallicRoughnessAtlas,
3669
+ uv
3670
+ );
3671
+ let normalTexel = sample_atlas(normalAtlasTexture, triangle.normalAtlas, uv);
3672
+ let occlusionTexel = sample_atlas(occlusionAtlasTexture, triangle.occlusionAtlas, uv);
3673
+ let emissiveTexel = sample_atlas(emissiveAtlasTexture, triangle.emissiveAtlas, uv);
3674
+ let normalScale = clamp(triangle.textureSettings.x, 0.0, 1.0);
3675
+ let tangentBasis = build_triangle_tangent_basis(triangle, geometricNormal);
3676
+ let tangentNormal = safe_normalize(
3677
+ vec3<f32>(
3678
+ (normalTexel.x * 2.0 - 1.0) * normalScale,
3679
+ (normalTexel.y * 2.0 - 1.0) * normalScale,
3680
+ 1.0 + ((normalTexel.z * 2.0 - 1.0) - 1.0) * normalScale
3681
+ ),
3682
+ vec3<f32>(0.0, 0.0, 1.0)
3683
+ );
3684
+ let mappedNormal = safe_normalize(
3685
+ tangentBasis.tangent * tangentNormal.x +
3686
+ tangentBasis.bitangent * tangentNormal.y +
3687
+ shadingNormal * tangentNormal.z,
3688
+ shadingNormal
3689
+ );
3690
+ let emission = vec4<f32>(
3691
+ max(
3692
+ triangle.emission.rgb *
3693
+ srgb_to_linear_vec3(emissiveTexel.rgb) *
3694
+ max(triangle.textureSettings.z, 0.0),
3695
+ vec3<f32>(0.0)
3696
+ ),
3697
+ clamp(triangle.emission.a * emissiveTexel.a, 0.0, 1.0)
3698
+ );
3699
+ return SurfaceMaterialSample(
3700
+ baseColor,
3701
+ emission,
3702
+ vec4<f32>(
3703
+ clamp(triangle.material.x * metallicRoughnessTexel.y, 0.0, 1.0),
3704
+ clamp(triangle.material.y * metallicRoughnessTexel.z, 0.0, 1.0),
3705
+ clamp(triangle.material.z * baseColor.a, 0.0, 1.0),
3706
+ clamp(triangle.material.w, 1.0, 3.0)
3707
+ ),
3708
+ triangle.materialResponse,
3709
+ triangle.materialExtension,
3710
+ triangle.specularColor,
3711
+ repair_shading_normal(geometricNormal, mappedNormal),
3712
+ clamp(
3713
+ mix(1.0, occlusionTexel.x, clamp(triangle.textureSettings.y, 0.0, 1.0)),
3714
+ 0.0,
3715
+ 1.0
3716
+ )
3717
+ );
3718
+ }
3719
+
2162
3720
  fn saturate(value: f32) -> f32 {
2163
3721
  return clamp(value, 0.0, 1.0);
2164
3722
  }
@@ -2167,6 +3725,10 @@ fn max_component(value: vec3<f32>) -> f32 {
2167
3725
  return max(max(value.x, value.y), value.z);
2168
3726
  }
2169
3727
 
3728
+ fn radiance_luminance(value: vec3<f32>) -> f32 {
3729
+ return dot(value, vec3<f32>(0.2126, 0.7152, 0.0722));
3730
+ }
3731
+
2170
3732
  fn environment_map_enabled() -> bool {
2171
3733
  return config.environmentMapSettings.x > 0.5;
2172
3734
  }
@@ -2287,12 +3849,349 @@ fn environment_portal_radiance_scale(origin: vec3<f32>, direction: vec3<f32>) ->
2287
3849
  return scale;
2288
3850
  }
2289
3851
 
2290
- fn environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
2291
- let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
2292
- let portalScale = environment_portal_radiance_scale(origin, rayDirection);
2293
- let portalHit = max_component(portalScale) > 0.0001;
2294
- return base_environment_radiance(rayDirection) *
2295
- select(vec3<f32>(1.0), portalScale, portalHit);
3852
+ fn environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
3853
+ let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
3854
+ let portalScale = environment_portal_radiance_scale(origin, rayDirection);
3855
+ let portalHit = max_component(portalScale) > 0.0001;
3856
+ return base_environment_radiance(rayDirection) *
3857
+ select(vec3<f32>(1.0), portalScale, portalHit);
3858
+ }
3859
+
3860
+ fn direct_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
3861
+ let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
3862
+ let portalScale = environment_portal_radiance_scale(origin, rayDirection);
3863
+ let portalHit = max_component(portalScale) > 0.0001;
3864
+ if (
3865
+ config.environmentPortalCount > 0u &&
3866
+ config.environmentPortalMode == 2u &&
3867
+ !portalHit
3868
+ ) {
3869
+ return vec3<f32>(0.0);
3870
+ }
3871
+ return base_environment_radiance(rayDirection) *
3872
+ select(vec3<f32>(1.0), portalScale, portalHit);
3873
+ }
3874
+
3875
+ fn radical_inverse_vdc(bitsValue: u32) -> f32 {
3876
+ var bits = bitsValue;
3877
+ bits = (bits << 16u) | (bits >> 16u);
3878
+ bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xaaaaaaaau) >> 1u);
3879
+ bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xccccccccu) >> 2u);
3880
+ bits = ((bits & 0x0f0f0f0fu) << 4u) | ((bits & 0xf0f0f0f0u) >> 4u);
3881
+ bits = ((bits & 0x00ff00ffu) << 8u) | ((bits & 0xff00ff00u) >> 8u);
3882
+ return f32(bits) * 2.3283064365386963e-10;
3883
+ }
3884
+
3885
+ fn hammersley_2d(index: u32, count: u32) -> vec2<f32> {
3886
+ return vec2<f32>(f32(index) / max(f32(count), 1.0), radical_inverse_vdc(index));
3887
+ }
3888
+
3889
+ fn build_basis_tangent(normal: vec3<f32>) -> vec3<f32> {
3890
+ let tangentFallback = select(vec3<f32>(0.0, 1.0, 0.0), vec3<f32>(1.0, 0.0, 0.0), abs(normal.y) >= 0.999);
3891
+ return safe_normalize(cross(tangentFallback, normal), vec3<f32>(1.0, 0.0, 0.0));
3892
+ }
3893
+
3894
+ fn local_to_world(local: vec3<f32>, normal: vec3<f32>) -> vec3<f32> {
3895
+ let tangent = build_basis_tangent(normal);
3896
+ let bitangent = safe_normalize(cross(normal, tangent), vec3<f32>(0.0, 0.0, 1.0));
3897
+ return safe_normalize(tangent * local.x + bitangent * local.y + normal * local.z, normal);
3898
+ }
3899
+
3900
+ fn cosine_sample_hemisphere(sample: vec2<f32>, normal: vec3<f32>) -> vec3<f32> {
3901
+ let phi = 6.28318530718 * sample.x;
3902
+ let radius = sqrt(sample.y);
3903
+ let x = cos(phi) * radius;
3904
+ let y = sin(phi) * radius;
3905
+ let z = sqrt(max(0.0, 1.0 - sample.y));
3906
+ return local_to_world(vec3<f32>(x, y, z), normal);
3907
+ }
3908
+
3909
+ fn importance_sample_ggx(sample: vec2<f32>, roughness: f32, normal: vec3<f32>) -> vec3<f32> {
3910
+ let alpha = max(roughness * roughness, 0.0001);
3911
+ let phi = 6.28318530718 * sample.x;
3912
+ let cosTheta = sqrt((1.0 - sample.y) / max(1.0 + (alpha * alpha - 1.0) * sample.y, 0.0001));
3913
+ let sinTheta = sqrt(max(0.0, 1.0 - cosTheta * cosTheta));
3914
+ let localHalf = vec3<f32>(cos(phi) * sinTheta, sin(phi) * sinTheta, cosTheta);
3915
+ return local_to_world(localHalf, normal);
3916
+ }
3917
+
3918
+ fn distribution_ggx(normal: vec3<f32>, halfVector: vec3<f32>, roughness: f32) -> f32 {
3919
+ let alpha = max(roughness * roughness, 0.0001);
3920
+ let alpha2 = alpha * alpha;
3921
+ let nDotH = saturate(dot(normal, halfVector));
3922
+ let denominator = nDotH * nDotH * (alpha2 - 1.0) + 1.0;
3923
+ return alpha2 / max(3.14159265359 * denominator * denominator, 0.000001);
3924
+ }
3925
+
3926
+ fn geometry_schlick_ggx(nDotValue: f32, roughness: f32) -> f32 {
3927
+ let k = ((roughness + 1.0) * (roughness + 1.0)) / 8.0;
3928
+ return nDotValue / max(nDotValue * (1.0 - k) + k, 0.000001);
3929
+ }
3930
+
3931
+ fn geometry_smith(normal: vec3<f32>, viewDirection: vec3<f32>, lightDirection: vec3<f32>, roughness: f32) -> f32 {
3932
+ let nDotV = saturate(dot(normal, viewDirection));
3933
+ let nDotL = saturate(dot(normal, lightDirection));
3934
+ return geometry_schlick_ggx(nDotV, roughness) * geometry_schlick_ggx(nDotL, roughness);
3935
+ }
3936
+
3937
+ fn fresnel_schlick(cosine: f32, f0: vec3<f32>) -> vec3<f32> {
3938
+ return f0 + (vec3<f32>(1.0) - f0) * pow(1.0 - cosine, 5.0);
3939
+ }
3940
+
3941
+ fn sample_brdf_lut(nDotV: f32, roughness: f32) -> vec2<f32> {
3942
+ let uv = vec2<f32>(clamp(nDotV, 0.0, 1.0), clamp(roughness, 0.0, 1.0));
3943
+ return textureSampleLevel(brdfLutTexture, brdfLutSampler, uv, 0.0).xy;
3944
+ }
3945
+
3946
+ fn prefiltered_environment_radiance(direction: vec3<f32>, roughness: f32) -> vec3<f32> {
3947
+ let uv = environment_map_uv(direction);
3948
+ let maxLevel = max(config.environmentMapMeta.z - 1.0, 0.0);
3949
+ let lod = clamp(roughness, 0.0, 1.0) * maxLevel;
3950
+ let texel = max(textureSampleLevel(environmentMapTexture, environmentMapSampler, uv, lod).rgb, vec3<f32>(0.0));
3951
+ return texel * max(config.environmentMapSettings.y, 0.0);
3952
+ }
3953
+
3954
+ fn environment_pdf_dimensions() -> vec2<u32> {
3955
+ return vec2<u32>(
3956
+ max(u32(config.environmentMapMeta.x), 1u),
3957
+ max(u32(config.environmentMapMeta.y), 1u)
3958
+ );
3959
+ }
3960
+
3961
+ fn environment_importance_sampling_enabled() -> bool {
3962
+ return config.environmentMapMeta.w > 0.5;
3963
+ }
3964
+
3965
+ fn uniform_sphere_pdf() -> f32 {
3966
+ return 1.0 / (4.0 * 3.14159265359);
3967
+ }
3968
+
3969
+ fn sample_uniform_sphere_direction(sample: vec2<f32>) -> vec3<f32> {
3970
+ let z = 1.0 - 2.0 * sample.y;
3971
+ let radial = sqrt(max(1.0 - z * z, 0.0));
3972
+ let phi = sample.x * 6.28318530718;
3973
+ return vec3<f32>(cos(phi) * radial, z, sin(phi) * radial);
3974
+ }
3975
+
3976
+ fn environment_sampling_texel(x: u32, y: u32) -> vec4<f32> {
3977
+ return textureLoad(environmentSamplingTexture, vec2<i32>(i32(x), i32(y)), 0);
3978
+ }
3979
+
3980
+ fn environment_pdf_texel(x: u32, y: u32) -> f32 {
3981
+ return environment_sampling_texel(x, y).x;
3982
+ }
3983
+
3984
+ fn environment_row_cdf_texel(y: u32) -> f32 {
3985
+ return environment_sampling_texel(0u, y).z;
3986
+ }
3987
+
3988
+ fn environment_column_cdf_texel(x: u32, y: u32) -> f32 {
3989
+ return environment_sampling_texel(x, y).y;
3990
+ }
3991
+
3992
+ fn environment_direction_pdf(direction: vec3<f32>) -> f32 {
3993
+ if (!environment_importance_sampling_enabled()) {
3994
+ return uniform_sphere_pdf();
3995
+ }
3996
+ let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
3997
+ let uv = environment_map_uv(rayDirection);
3998
+ let dimensions = environment_pdf_dimensions();
3999
+ let width = max(f32(dimensions.x), 1.0);
4000
+ let height = max(f32(dimensions.y), 1.0);
4001
+ let x = min(u32(uv.x * width), dimensions.x - 1u);
4002
+ let y = min(u32(uv.y * height), dimensions.y - 1u);
4003
+ let discretePdf = max(environment_pdf_texel(x, y), 0.0);
4004
+ let sinTheta = sqrt(max(1.0 - rayDirection.y * rayDirection.y, 0.0));
4005
+ let solidAngle = max((2.0 * 3.14159265359 * 3.14159265359 * sinTheta) / (width * height), 0.000001);
4006
+ return discretePdf / solidAngle;
4007
+ }
4008
+
4009
+ fn sample_row_cdf(count: u32, sampleValue: f32) -> u32 {
4010
+ if (count == 0u) {
4011
+ return 0u;
4012
+ }
4013
+ var low = 0u;
4014
+ var high = count - 1u;
4015
+ loop {
4016
+ if (low >= high) {
4017
+ break;
4018
+ }
4019
+ let mid = (low + high) / 2u;
4020
+ let cdfValue = environment_row_cdf_texel(mid);
4021
+ if (sampleValue <= cdfValue) {
4022
+ high = mid;
4023
+ } else {
4024
+ low = mid + 1u;
4025
+ }
4026
+ }
4027
+ return min(low, count - 1u);
4028
+ }
4029
+
4030
+ fn sample_column_cdf(row: u32, count: u32, sampleValue: f32) -> u32 {
4031
+ if (count == 0u) {
4032
+ return 0u;
4033
+ }
4034
+ var low = 0u;
4035
+ var high = count - 1u;
4036
+ loop {
4037
+ if (low >= high) {
4038
+ break;
4039
+ }
4040
+ let mid = (low + high) / 2u;
4041
+ let cdfValue = environment_column_cdf_texel(mid, row);
4042
+ if (sampleValue <= cdfValue) {
4043
+ high = mid;
4044
+ } else {
4045
+ low = mid + 1u;
4046
+ }
4047
+ }
4048
+ return min(low, count - 1u);
4049
+ }
4050
+
4051
+ struct EnvironmentSample {
4052
+ direction: vec3<f32>,
4053
+ radiance: vec3<f32>,
4054
+ pdf: f32,
4055
+ };
4056
+
4057
+ fn sample_environment_importance(sample: vec2<f32>) -> EnvironmentSample {
4058
+ if (!environment_importance_sampling_enabled()) {
4059
+ let direction = sample_uniform_sphere_direction(sample);
4060
+ return EnvironmentSample(direction, base_environment_radiance(direction), uniform_sphere_pdf());
4061
+ }
4062
+ let dimensions = environment_pdf_dimensions();
4063
+ let row = sample_row_cdf(dimensions.y, sample.y);
4064
+ let column = sample_column_cdf(row, dimensions.x, sample.x);
4065
+ let uv = vec2<f32>(
4066
+ (f32(column) + 0.5) / max(f32(dimensions.x), 1.0),
4067
+ (f32(row) + 0.5) / max(f32(dimensions.y), 1.0)
4068
+ );
4069
+ let theta = uv.y * 3.14159265359;
4070
+ let phi = (uv.x - 0.5 - config.environmentMapSettings.z / 6.28318530718) * 6.28318530718;
4071
+ let sinTheta = sin(theta);
4072
+ let direction = vec3<f32>(cos(phi) * sinTheta, cos(theta), sin(phi) * sinTheta);
4073
+ let pdf = environment_direction_pdf(direction);
4074
+ return EnvironmentSample(direction, base_environment_radiance(direction), pdf);
4075
+ }
4076
+
4077
+ fn power_heuristic(pdfA: f32, pdfB: f32) -> f32 {
4078
+ let a2 = pdfA * pdfA;
4079
+ let b2 = pdfB * pdfB;
4080
+ return a2 / max(a2 + b2, 0.000001);
4081
+ }
4082
+
4083
+ fn visible_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
4084
+ let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
4085
+ let visible = !scene_visibility_blocked(origin, rayDirection, 1000000.0);
4086
+ return select(vec3<f32>(0.0), direct_environment_radiance(origin, rayDirection), visible);
4087
+ }
4088
+
4089
+ fn glossy_environment_direction(
4090
+ incidentDirection: vec3<f32>,
4091
+ normal: vec3<f32>,
4092
+ roughness: f32,
4093
+ normalBlendScale: f32
4094
+ ) -> vec3<f32> {
4095
+ let reflectionDirection = reflect(incidentDirection, normal);
4096
+ let blend = clamp(roughness * roughness * normalBlendScale, 0.0, 0.92);
4097
+ return safe_normalize(mix(reflectionDirection, normal, blend), normal);
4098
+ }
4099
+
4100
+ fn surface_glossiness(hit: HitRecord) -> f32 {
4101
+ let roughness = clamp(hit.material.x, 0.0, 1.0);
4102
+ let metallic = clamp(hit.material.y, 0.0, 1.0);
4103
+ let sheen = clamp(max_component(hit.materialResponse.xyz), 0.0, 1.0);
4104
+ let clearcoat = clamp(hit.materialResponse.w, 0.0, 1.0);
4105
+ let specularWeight = clamp(hit.materialExtension.y, 0.0, 1.0);
4106
+ let transmission = clamp(hit.materialExtension.z, 0.0, 1.0);
4107
+ let baseGloss =
4108
+ max(
4109
+ clearcoat,
4110
+ max(sheen * 0.72, max(specularWeight * (0.38 + metallic * 0.62), transmission))
4111
+ );
4112
+ return clamp(baseGloss * (1.0 - roughness * 0.72) + metallic * (1.0 - roughness) * 0.35, 0.0, 1.0);
4113
+ }
4114
+
4115
+ fn surface_specular_f0(hit: HitRecord, surfaceColor: vec3<f32>) -> vec3<f32> {
4116
+ let metallic = clamp(hit.material.y, 0.0, 1.0);
4117
+ let specularWeight = clamp(hit.materialExtension.y, 0.0, 1.0);
4118
+ let specularColor = clamp(hit.specularColor.xyz, vec3<f32>(0.0), vec3<f32>(1.0));
4119
+ let dielectricF0 = vec3<f32>(0.04) * specularWeight * specularColor;
4120
+ return mix(dielectricF0, surfaceColor, metallic);
4121
+ }
4122
+
4123
+ fn surface_bsdf_sampling_weights(hit: HitRecord) -> vec3<f32> {
4124
+ let metallic = clamp(hit.material.y, 0.0, 1.0);
4125
+ let clearcoat = clamp(hit.materialResponse.w, 0.0, 1.0);
4126
+ let specularWeight = clamp(hit.materialExtension.y, 0.0, 1.0);
4127
+ let diffuseWeight = clamp(
4128
+ (1.0 - metallic) * max(1.0 - specularWeight * 0.5 - clearcoat * 0.25, 0.15),
4129
+ 0.0,
4130
+ 1.0
4131
+ );
4132
+ let specWeight = clamp(max(metallic, specularWeight * 0.75) * (1.0 - clearcoat * 0.5), 0.0, 1.0);
4133
+ let clearcoatWeight = clamp(clearcoat, 0.0, 1.0);
4134
+ let totalWeight = max(diffuseWeight + specWeight + clearcoatWeight, 0.000001);
4135
+ return vec3<f32>(
4136
+ diffuseWeight / totalWeight,
4137
+ specWeight / totalWeight,
4138
+ clearcoatWeight / totalWeight
4139
+ );
4140
+ }
4141
+
4142
+ fn evaluate_surface_bsdf(hit: HitRecord, viewDirection: vec3<f32>, lightDirection: vec3<f32>) -> vec3<f32> {
4143
+ let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
4144
+ let surfaceColor = clamp(max(hit.color.xyz, config.ambientColor.xyz * 0.35), vec3<f32>(0.0), vec3<f32>(1.0));
4145
+ let roughness = clamp(hit.material.x, 0.0, 1.0);
4146
+ let metallic = clamp(hit.material.y, 0.0, 1.0);
4147
+ let clearcoat = clamp(hit.materialResponse.w, 0.0, 1.0);
4148
+ let clearcoatRoughness = clamp(hit.materialExtension.x, 0.0, 1.0);
4149
+ let occlusion = clamp(hit.occlusion, 0.0, 1.0);
4150
+ let nDotV = saturate(dot(normal, viewDirection));
4151
+ let nDotL = saturate(dot(normal, lightDirection));
4152
+ if (nDotV <= 0.0 || nDotL <= 0.0) {
4153
+ return vec3<f32>(0.0);
4154
+ }
4155
+ let halfVector = safe_normalize(viewDirection + lightDirection, normal);
4156
+ let vDotH = saturate(dot(viewDirection, halfVector));
4157
+ let f0 = surface_specular_f0(hit, surfaceColor);
4158
+ let fresnel = fresnel_schlick(vDotH, f0);
4159
+ let distribution = distribution_ggx(normal, halfVector, roughness);
4160
+ let geometry = geometry_smith(normal, viewDirection, lightDirection, roughness);
4161
+ let specular = (distribution * geometry * fresnel) / max(4.0 * nDotV * nDotL, 0.000001);
4162
+ let diffuseWeight = (1.0 - metallic) * (1.0 - clearcoat * 0.24) * (1.0 - clamp(max_component(fresnel), 0.0, 0.98));
4163
+ let diffuse = surfaceColor * diffuseWeight / 3.14159265359;
4164
+ let clearcoatHalf = safe_normalize(viewDirection + lightDirection, normal);
4165
+ let clearcoatDistribution = distribution_ggx(normal, clearcoatHalf, max(clearcoatRoughness, 0.02));
4166
+ let clearcoatGeometry = geometry_smith(normal, viewDirection, lightDirection, max(clearcoatRoughness, 0.02));
4167
+ let clearcoatFresnel = fresnel_schlick(saturate(dot(viewDirection, clearcoatHalf)), vec3<f32>(0.04));
4168
+ let clearcoatTerm =
4169
+ (clearcoatDistribution * clearcoatGeometry * clearcoatFresnel) /
4170
+ max(4.0 * nDotV * nDotL, 0.000001) *
4171
+ clearcoat;
4172
+ return (diffuse + specular + clearcoatTerm) * mix(0.42, 1.0, occlusion);
4173
+ }
4174
+
4175
+ fn diffuse_pdf(normal: vec3<f32>, lightDirection: vec3<f32>) -> f32 {
4176
+ return saturate(dot(normal, lightDirection)) / 3.14159265359;
4177
+ }
4178
+
4179
+ fn ggx_pdf(normal: vec3<f32>, viewDirection: vec3<f32>, lightDirection: vec3<f32>, roughness: f32) -> f32 {
4180
+ let halfVector = safe_normalize(viewDirection + lightDirection, normal);
4181
+ let nDotH = saturate(dot(normal, halfVector));
4182
+ let vDotH = saturate(dot(viewDirection, halfVector));
4183
+ let distribution = distribution_ggx(normal, halfVector, roughness);
4184
+ return (distribution * nDotH) / max(4.0 * vDotH, 0.000001);
4185
+ }
4186
+
4187
+ fn evaluate_surface_bsdf_pdf(hit: HitRecord, viewDirection: vec3<f32>, lightDirection: vec3<f32>) -> f32 {
4188
+ let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
4189
+ let roughness = clamp(hit.material.x, 0.0, 1.0);
4190
+ let weights = surface_bsdf_sampling_weights(hit);
4191
+ let diffuseTerm = diffuse_pdf(normal, lightDirection);
4192
+ let specTerm = ggx_pdf(normal, viewDirection, lightDirection, max(roughness, 0.02));
4193
+ let clearcoatTerm = ggx_pdf(normal, viewDirection, lightDirection, max(clamp(hit.materialExtension.x, 0.0, 1.0), 0.02));
4194
+ return weights.x * diffuseTerm + weights.y * specTerm + weights.z * clearcoatTerm;
2296
4195
  }
2297
4196
 
2298
4197
  fn gated_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
@@ -2307,12 +4206,102 @@ fn gated_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f
2307
4206
  return environment_radiance(origin, direction);
2308
4207
  }
2309
4208
 
4209
+ fn medium_dimensions() -> vec2<u32> {
4210
+ return textureDimensions(mediumTableTexture);
4211
+ }
4212
+
4213
+ fn medium_valid(mediumRefId: u32) -> bool {
4214
+ let dimensions = medium_dimensions();
4215
+ return mediumRefId > 0u && mediumRefId < dimensions.x;
4216
+ }
4217
+
4218
+ fn medium_absorption(mediumRefId: u32) -> vec3<f32> {
4219
+ if (!medium_valid(mediumRefId)) {
4220
+ return vec3<f32>(0.0);
4221
+ }
4222
+ return max(
4223
+ textureLoad(mediumTableTexture, vec2<i32>(i32(mediumRefId), 0), 0).xyz,
4224
+ vec3<f32>(0.0)
4225
+ );
4226
+ }
4227
+
4228
+ fn medium_scattering(mediumRefId: u32) -> vec3<f32> {
4229
+ if (!medium_valid(mediumRefId)) {
4230
+ return vec3<f32>(0.0);
4231
+ }
4232
+ return max(
4233
+ textureLoad(mediumTableTexture, vec2<i32>(i32(mediumRefId), 1), 0).xyz,
4234
+ vec3<f32>(0.0)
4235
+ );
4236
+ }
4237
+
4238
+ fn medium_transmittance(mediumRefId: u32, distance: f32) -> vec3<f32> {
4239
+ if (!medium_valid(mediumRefId) || distance <= 0.000001) {
4240
+ return vec3<f32>(1.0);
4241
+ }
4242
+ let extinction = medium_absorption(mediumRefId) + medium_scattering(mediumRefId);
4243
+ return vec3<f32>(
4244
+ exp(-extinction.x * distance),
4245
+ exp(-extinction.y * distance),
4246
+ exp(-extinction.z * distance)
4247
+ );
4248
+ }
4249
+
4250
+ fn transmitted_medium_ref_id(ray: RayRecord, hit: HitRecord) -> u32 {
4251
+ if (hit.mediumRefId == 0u) {
4252
+ return ray.mediumRefId;
4253
+ }
4254
+ if (hit.frontFace == 1u) {
4255
+ return hit.mediumRefId;
4256
+ }
4257
+ if (ray.mediumRefId == hit.mediumRefId) {
4258
+ return 0u;
4259
+ }
4260
+ return ray.mediumRefId;
4261
+ }
4262
+
2310
4263
  fn surface_path_response(hit: HitRecord) -> vec3<f32> {
2311
4264
  let color = clamp(hit.color.xyz, vec3<f32>(0.0), vec3<f32>(1.0));
2312
4265
  let opacity = clamp(hit.material.z, 0.0, 1.0);
4266
+ let occlusion = clamp(hit.occlusion, 0.0, 1.0);
2313
4267
  let materialEnergy = select(0.68, 0.92, hit.materialKind == 1u || hit.materialKind == 2u);
2314
4268
  let transparentEnergy = select(materialEnergy, 0.9, hit.hitType == 3u);
2315
- return mix(vec3<f32>(1.0), color, max(opacity, 0.18)) * transparentEnergy;
4269
+ return mix(vec3<f32>(1.0), color, max(opacity, 0.18)) * transparentEnergy * mix(0.55, 1.0, occlusion);
4270
+ }
4271
+
4272
+ fn bounded_path_response_luminance(ray: RayRecord, hit: HitRecord) -> f32 {
4273
+ let daylightFloor = max(config.pathResolveSettings.y, 0.0) * 0.08;
4274
+ let hdriFloor = max(config.environmentMapSettings.w, 0.0) * 0.02;
4275
+ let sceneFloor = max(daylightFloor, hdriFloor);
4276
+ if (sceneFloor <= 0.000001) {
4277
+ return 0.0;
4278
+ }
4279
+ let bounceRatio = select(
4280
+ 0.0,
4281
+ f32(ray.bounce) / max(f32(config.maxDepth - 1u), 1.0),
4282
+ config.maxDepth > 1u
4283
+ );
4284
+ let bounceScale = 1.0 - bounceRatio * 0.55;
4285
+ let materialScale = select(1.0, 0.34, hit.materialKind == 1u || hit.materialKind == 2u);
4286
+ let transparentScale = select(materialScale, 0.58, hit.hitType == 3u);
4287
+ let opacityScale = mix(0.55, 1.0, clamp(hit.material.z, 0.0, 1.0));
4288
+ return sceneFloor * bounceScale * transparentScale * opacityScale;
4289
+ }
4290
+
4291
+ fn stabilize_surface_path_response(ray: RayRecord, hit: HitRecord, response: vec3<f32>) -> vec3<f32> {
4292
+ let minimumLuminance = bounded_path_response_luminance(ray, hit);
4293
+ let responseLuminance = radiance_luminance(response);
4294
+ if (minimumLuminance <= 0.000001 || responseLuminance >= minimumLuminance) {
4295
+ return response;
4296
+ }
4297
+ let tintBase = max(response, max(hit.color.xyz * 0.65, config.ambientColor.xyz * 0.35));
4298
+ let tint = tintBase / max(max_component(tintBase), 0.0001);
4299
+ let lifted = select(
4300
+ tint * minimumLuminance,
4301
+ response * (minimumLuminance / max(responseLuminance, 0.0001)),
4302
+ responseLuminance > 0.0001
4303
+ );
4304
+ return clamp(lifted, vec3<f32>(0.0), vec3<f32>(0.98));
2316
4305
  }
2317
4306
 
2318
4307
  fn sunlit_baseline_radiance(normal: vec3<f32>) -> vec3<f32> {
@@ -2331,12 +4320,24 @@ fn sunlit_baseline_radiance(normal: vec3<f32>) -> vec3<f32> {
2331
4320
  return clamp_sample_radiance(sunTint * baseline * skyFacing * directionalWeight * 0.04);
2332
4321
  }
2333
4322
 
2334
- fn terminal_surface_environment_source(hit: HitRecord) -> vec3<f32> {
4323
+ fn terminal_surface_environment_source(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
2335
4324
  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
4325
+ let origin = hit.position.xyz + normal * 0.003;
4326
+ let roughness = clamp(hit.material.x, 0.0, 1.0);
4327
+ let glossiness = surface_glossiness(hit);
4328
+ let normalEnvironment = gated_environment_radiance(origin, normal);
4329
+ let viewDirection = safe_normalize(-ray.direction.xyz, normal);
4330
+ let reflectionDirection = glossy_environment_direction(
4331
+ ray.direction.xyz,
4332
+ normal,
4333
+ roughness,
4334
+ mix(0.88, 0.38, glossiness)
2339
4335
  );
4336
+ let reflectionEnvironment = prefiltered_environment_radiance(reflectionDirection, roughness);
4337
+ let surfaceColor = clamp(max(hit.color.xyz, config.ambientColor.xyz * 0.35), vec3<f32>(0.0), vec3<f32>(1.0));
4338
+ let f0 = surface_specular_f0(hit, surfaceColor);
4339
+ let brdfTerm = sample_brdf_lut(saturate(dot(normal, viewDirection)), roughness);
4340
+ let specularEnvironment = reflectionEnvironment * (f0 * brdfTerm.x + vec3<f32>(brdfTerm.y));
2340
4341
  let sunlitFloor = sunlit_baseline_radiance(normal);
2341
4342
  let ambientFloor = select(
2342
4343
  max(config.ambientColor.xyz, sunlitFloor * 0.82),
@@ -2348,17 +4349,27 @@ fn terminal_surface_environment_source(hit: HitRecord) -> vec3<f32> {
2348
4349
  max(config.environmentMapSettings.w, max(0.12, config.pathResolveSettings.y * 0.42)),
2349
4350
  environment_map_enabled()
2350
4351
  );
2351
- let environmentFloor = max(ambientFloor, max(sunlitFloor, normalEnvironment * environmentInfluence));
4352
+ let glossyEnvironment = max(
4353
+ normalEnvironment,
4354
+ max(reflectionEnvironment * mix(0.24, 0.92, glossiness), specularEnvironment)
4355
+ );
4356
+ let environmentFloor = max(ambientFloor, max(sunlitFloor, glossyEnvironment * environmentInfluence));
2352
4357
  let materialFloor = select(0.7, 1.0, hit.materialKind == 0u || hit.materialKind == 3u);
2353
4358
  return clamp_sample_radiance(environmentFloor * materialFloor);
2354
4359
  }
2355
4360
 
2356
- fn terminal_surface_environment_contribution(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
4361
+ fn terminal_surface_environment_contribution(
4362
+ ray: RayRecord,
4363
+ throughput: vec3<f32>,
4364
+ hit: HitRecord
4365
+ ) -> vec3<f32> {
2357
4366
  let surfaceColor = max(hit.color.xyz, config.ambientColor.xyz);
4367
+ let occlusion = mix(0.75, 1.0, clamp(hit.occlusion, 0.0, 1.0));
2358
4368
  return clamp_sample_radiance(
2359
- ray.throughput.xyz *
4369
+ throughput *
2360
4370
  surfaceColor *
2361
- terminal_surface_environment_source(hit)
4371
+ terminal_surface_environment_source(ray, hit) *
4372
+ occlusion
2362
4373
  );
2363
4374
  }
2364
4375
 
@@ -2391,6 +4402,10 @@ fn direct_environment_portal_irradiance(origin: vec3<f32>, normal: vec3<f32>) ->
2391
4402
  );
2392
4403
  let area = max(portal.position.w, 0.0001);
2393
4404
  let distanceFalloff = clamp(area / max(distanceSquared, area * 0.25), 0.0, 2.5);
4405
+ let traceDistance = max(sqrt(distanceSquared) - 0.01, 0.01);
4406
+ if (scene_visibility_blocked(origin, direction, traceDistance)) {
4407
+ continue;
4408
+ }
2394
4409
  irradiance = irradiance +
2395
4410
  portal.color.rgb *
2396
4411
  portal.normal.w *
@@ -2402,48 +4417,79 @@ fn direct_environment_portal_irradiance(origin: vec3<f32>, normal: vec3<f32>) ->
2402
4417
  return irradiance;
2403
4418
  }
2404
4419
 
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()
4420
+ fn visibility_test_ray(origin: vec3<f32>, direction: vec3<f32>) -> RayRecord {
4421
+ let rayDirection = safe_normalize(direction, vec3<f32>(0.0, 1.0, 0.0));
4422
+ return RayRecord(
4423
+ 0u,
4424
+ 0u,
4425
+ 0u,
4426
+ 0u,
4427
+ 0u,
4428
+ 0u,
4429
+ 0u,
4430
+ 0u,
4431
+ vec4<f32>(origin, 1.0),
4432
+ vec4<f32>(rayDirection, 0.0),
4433
+ vec4<f32>(1.0, 1.0, 1.0, 1.0)
2424
4434
  );
2425
- let skyIrradiance = max(ambientIrradiance, normalEnvironment * skyVisibility * environmentIrradianceScale);
4435
+ }
2426
4436
 
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);
4437
+ fn scene_visibility_blocked(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> bool {
4438
+ let testRay = visibility_test_ray(origin, direction);
4439
+ let nearest = max(maxDistance, 0.001);
2435
4440
 
2436
- let diffuseWeight = select(1.0 - metallic * 0.65, 0.22, hit.materialKind == 1u);
2437
- let diffuse = surfaceColor * (skyIrradiance + sunIrradiance + portalIrradiance) * diffuseWeight;
4441
+ for (var objectIndex = 0u; objectIndex < config.sceneObjectCount; objectIndex = objectIndex + 1u) {
4442
+ let object = sceneObjects[objectIndex];
4443
+ var current = no_candidate();
4444
+ if (object.kind == 1u) {
4445
+ current = intersect_sphere(testRay, object);
4446
+ } else if (object.kind == 2u) {
4447
+ current = intersect_box(testRay, object);
4448
+ }
4449
+ if (current.hit == 1u && current.distance < nearest) {
4450
+ return true;
4451
+ }
4452
+ }
2438
4453
 
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);
4454
+ let meshCandidate = intersect_bvh(testRay, nearest);
4455
+ return meshCandidate.hit == 1u && meshCandidate.distance < nearest;
4456
+ }
2444
4457
 
2445
- let bounceWeight = select(1.0, 0.38, ray.bounce > 0u);
2446
- return clamp_sample_radiance(ray.throughput.xyz * (diffuse + specular) * bounceWeight);
4458
+ fn surface_direct_environment_contribution(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
4459
+ let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
4460
+ let origin = hit.position.xyz + normal * 0.003;
4461
+ let viewDirection = safe_normalize(-ray.direction.xyz, normal);
4462
+ let lightSample = sample_environment_importance(vec2<f32>(
4463
+ random01(mix_seed(ray.sourcePixelId, ray.sampleId, ray.bounce, config.frameIndex, 41u)),
4464
+ random01(mix_seed(ray.sourcePixelId, ray.sampleId, ray.bounce, config.frameIndex, 43u))
4465
+ ));
4466
+ if (lightSample.pdf <= 0.000001) {
4467
+ return vec3<f32>(0.0);
4468
+ }
4469
+ let lightDirection = safe_normalize(lightSample.direction, normal);
4470
+ let nDotL = saturate(dot(normal, lightDirection));
4471
+ if (nDotL <= 0.000001) {
4472
+ return vec3<f32>(0.0);
4473
+ }
4474
+ if (scene_visibility_blocked(origin, lightDirection, 1000000.0)) {
4475
+ return vec3<f32>(0.0);
4476
+ }
4477
+ let incidentRadiance = direct_environment_radiance(origin, lightDirection);
4478
+ if (max_component(incidentRadiance) <= 0.000001) {
4479
+ return vec3<f32>(0.0);
4480
+ }
4481
+ let bsdf = evaluate_surface_bsdf(hit, viewDirection, lightDirection);
4482
+ if (max_component(bsdf) <= 0.000001) {
4483
+ return vec3<f32>(0.0);
4484
+ }
4485
+ let bsdfPdf = evaluate_surface_bsdf_pdf(hit, viewDirection, lightDirection);
4486
+ let misWeight = power_heuristic(lightSample.pdf, bsdfPdf);
4487
+ let contribution =
4488
+ ray.throughput.xyz *
4489
+ bsdf *
4490
+ incidentRadiance *
4491
+ (nDotL * misWeight / max(lightSample.pdf, 0.000001));
4492
+ return clamp_sample_radiance(contribution);
2447
4493
  }
2448
4494
 
2449
4495
  fn default_mesh_range() -> MeshRange {
@@ -2462,7 +4508,16 @@ fn default_mesh_range() -> MeshRange {
2462
4508
  0u,
2463
4509
  vec4<f32>(0.72, 0.72, 0.68, 1.0),
2464
4510
  vec4<f32>(0.0),
2465
- vec4<f32>(0.72, 0.0, 1.0, 1.45)
4511
+ vec4<f32>(0.72, 0.0, 1.0, 1.45),
4512
+ vec4<f32>(0.0, 0.0, 0.0, 0.08),
4513
+ vec4<f32>(1.0, 1.0, 1.0, 1.0),
4514
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4515
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4516
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4517
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4518
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4519
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
4520
+ vec4<f32>(1.0, 1.0, 1.0, 0.0)
2466
4521
  );
2467
4522
  }
2468
4523
 
@@ -2558,7 +4613,7 @@ fn prepareMeshTrianglesAndLeaves(@builtin(global_invocation_id) globalId: vec3<u
2558
4613
  mesh.flags,
2559
4614
  mesh.materialRefId,
2560
4615
  mesh.mediumRefId,
2561
- 0u,
4616
+ mesh.materialSlot,
2562
4617
  0u,
2563
4618
  vec4<f32>(vertex0.position.xyz, 0.0),
2564
4619
  vec4<f32>(vertex1.position.xyz, 0.0),
@@ -2570,7 +4625,16 @@ fn prepareMeshTrianglesAndLeaves(@builtin(global_invocation_id) globalId: vec3<u
2570
4625
  vec4<f32>(uv2, 0.0, 0.0),
2571
4626
  mesh.color,
2572
4627
  mesh.emission,
2573
- mesh.material
4628
+ mesh.material,
4629
+ mesh.materialResponse,
4630
+ mesh.materialExtension,
4631
+ mesh.specularColor,
4632
+ mesh.baseColorAtlas,
4633
+ mesh.metallicRoughnessAtlas,
4634
+ mesh.normalAtlas,
4635
+ mesh.occlusionAtlas,
4636
+ mesh.emissiveAtlas,
4637
+ mesh.textureSettings
2574
4638
  );
2575
4639
 
2576
4640
  let leafBase = config.triangleCount - 1u;
@@ -2729,7 +4793,8 @@ fn make_miss(ray: RayRecord) -> HitRecord {
2729
4793
  0u,
2730
4794
  0u,
2731
4795
  -1.0,
2732
- vec3<f32>(0.0),
4796
+ 1.0,
4797
+ vec2<f32>(0.0),
2733
4798
  vec4<f32>(ray.origin.xyz + ray.direction.xyz * 1000.0, 1.0),
2734
4799
  vec4<f32>(-ray.direction.xyz, 0.0),
2735
4800
  vec4<f32>(-ray.direction.xyz, 0.0),
@@ -2737,7 +4802,10 @@ fn make_miss(ray: RayRecord) -> HitRecord {
2737
4802
  vec4<f32>(0.0),
2738
4803
  vec4<f32>(radiance, 1.0),
2739
4804
  vec4<f32>(0.0),
2740
- vec4<f32>(1.0, 0.0, 1.0, 1.0)
4805
+ vec4<f32>(1.0, 0.0, 1.0, 1.0),
4806
+ vec4<f32>(0.0, 0.0, 0.0, 0.08),
4807
+ vec4<f32>(0.08, 1.0, 0.0, 0.0),
4808
+ vec4<f32>(1.0, 1.0, 1.0, 1.0)
2741
4809
  );
2742
4810
  }
2743
4811
 
@@ -2772,7 +4840,7 @@ fn intersect_sphere(ray: RayRecord, object: SceneObject) -> Candidate {
2772
4840
  0xffffffffu,
2773
4841
  object.objectId,
2774
4842
  object.objectId,
2775
- 0u
4843
+ object.mediumRefId
2776
4844
  );
2777
4845
  }
2778
4846
 
@@ -2824,7 +4892,7 @@ fn intersect_box(ray: RayRecord, object: SceneObject) -> Candidate {
2824
4892
  0xffffffffu,
2825
4893
  object.objectId,
2826
4894
  object.objectId,
2827
- 0u
4895
+ object.mediumRefId
2828
4896
  );
2829
4897
  }
2830
4898
 
@@ -3032,6 +5100,19 @@ fn denoise_range_space(value: vec3<f32>) -> vec3<f32> {
3032
5100
  return value / (vec3<f32>(1.0) + value);
3033
5101
  }
3034
5102
 
5103
+ fn denoise_sample_count() -> f32 {
5104
+ return clamp(1.0 / max(config.projectionAndSampling.z, 0.000001), 1.0, 256.0);
5105
+ }
5106
+
5107
+ fn denoise_strength() -> f32 {
5108
+ let spp = denoise_sample_count();
5109
+ return clamp(0.44 / sqrt(spp), 0.08, 0.44);
5110
+ }
5111
+
5112
+ fn denoise_kernel_radius() -> i32 {
5113
+ return select(1i, 2i, denoise_sample_count() < 2.5);
5114
+ }
5115
+
3035
5116
  @compute @workgroup_size(64)
3036
5117
  fn generatePrimaryRays(@builtin(global_invocation_id) globalId: vec3<u32>) {
3037
5118
  let index = globalId.x;
@@ -3062,6 +5143,10 @@ fn intersectActiveQueue(@builtin(global_invocation_id) globalId: vec3<u32>) {
3062
5143
  let ray = activeQueue[index];
3063
5144
  var nearest = 1000000.0;
3064
5145
  var hitObject = SceneObject(
5146
+ 0u,
5147
+ 0u,
5148
+ 0u,
5149
+ 0u,
3065
5150
  0u,
3066
5151
  0u,
3067
5152
  0u,
@@ -3070,7 +5155,10 @@ fn intersectActiveQueue(@builtin(global_invocation_id) globalId: vec3<u32>) {
3070
5155
  vec4<f32>(0.0),
3071
5156
  vec4<f32>(0.0),
3072
5157
  vec4<f32>(0.0),
3073
- vec4<f32>(1.0, 0.0, 1.0, 1.0)
5158
+ vec4<f32>(1.0, 0.0, 1.0, 1.0),
5159
+ vec4<f32>(0.0, 0.0, 0.0, 0.08),
5160
+ vec4<f32>(0.08, 1.0, 0.0, 0.0),
5161
+ vec4<f32>(1.0, 1.0, 1.0, 1.0)
3074
5162
  );
3075
5163
  var candidate = no_candidate();
3076
5164
  var hitTriangle = TriangleRecord(
@@ -3092,7 +5180,16 @@ fn intersectActiveQueue(@builtin(global_invocation_id) globalId: vec3<u32>) {
3092
5180
  vec4<f32>(0.0),
3093
5181
  vec4<f32>(0.0),
3094
5182
  vec4<f32>(0.0),
3095
- vec4<f32>(1.0, 0.0, 1.0, 1.0)
5183
+ vec4<f32>(1.0, 0.0, 1.0, 1.0),
5184
+ vec4<f32>(0.0, 0.0, 0.0, 0.08),
5185
+ vec4<f32>(0.08, 1.0, 0.0, 0.0),
5186
+ vec4<f32>(1.0, 1.0, 1.0, 1.0),
5187
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
5188
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
5189
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
5190
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
5191
+ vec4<f32>(0.0, 0.0, 1.0, 1.0),
5192
+ vec4<f32>(1.0, 1.0, 1.0, 0.0)
3096
5193
  );
3097
5194
 
3098
5195
  for (var objectIndex = 0u; objectIndex < config.sceneObjectCount; objectIndex = objectIndex + 1u) {
@@ -3125,16 +5222,28 @@ fn intersectActiveQueue(@builtin(global_invocation_id) globalId: vec3<u32>) {
3125
5222
  let position = ray.origin.xyz + ray.direction.xyz * candidate.distance;
3126
5223
  let hitMaterialKind = select(hitObject.materialKind, hitTriangle.materialKind, candidate.triangleIndex != 0xffffffffu);
3127
5224
  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);
5225
+ let meshSurface = sample_surface_material(
5226
+ hitTriangle,
5227
+ candidate.uv,
5228
+ candidate.geometricNormal,
5229
+ candidate.shadingNormal
5230
+ );
5231
+ let hitColor = select(hitObject.color, meshSurface.color, candidate.triangleIndex != 0xffffffffu);
5232
+ let hitEmission = select(hitObject.emission, meshSurface.emission, candidate.triangleIndex != 0xffffffffu);
5233
+ let hitMaterial = select(hitObject.material, meshSurface.material, candidate.triangleIndex != 0xffffffffu);
5234
+ let hitMaterialResponse = select(hitObject.materialResponse, meshSurface.materialResponse, candidate.triangleIndex != 0xffffffffu);
5235
+ let hitMaterialExtension = select(hitObject.materialExtension, meshSurface.materialExtension, candidate.triangleIndex != 0xffffffffu);
5236
+ let hitSpecularColor = select(hitObject.specularColor, meshSurface.specularColor, candidate.triangleIndex != 0xffffffffu);
5237
+ let hitShadingNormal = select(candidate.shadingNormal, meshSurface.shadingNormal, candidate.triangleIndex != 0xffffffffu);
3131
5238
  let hitPrimitiveId = select(candidate.primitiveId, hitTriangle.triangleId, candidate.triangleIndex != 0xffffffffu);
3132
5239
  let hitMaterialRefId = select(candidate.materialRefId, hitTriangle.materialRefId, candidate.triangleIndex != 0xffffffffu);
3133
5240
  let hitMediumRefId = select(candidate.mediumRefId, hitTriangle.mediumRefId, candidate.triangleIndex != 0xffffffffu);
5241
+ let hitMaterialSlot = select(0u, hitTriangle.materialSlot, candidate.triangleIndex != 0xffffffffu);
5242
+ let hitOcclusion = select(1.0, meshSurface.occlusion, candidate.triangleIndex != 0xffffffffu);
3134
5243
  var hitType = 0u;
3135
5244
  if (hitMaterialKind == 4u || emission_power(hitEmission) > 0.0001) {
3136
5245
  hitType = 1u;
3137
- } else if (hitMaterialKind == 3u || hitMaterial.z < 0.999) {
5246
+ } else if (hitMaterialKind == 3u || hitMaterial.z < 0.999 || hitMaterialExtension.z > 0.001) {
3138
5247
  hitType = 3u;
3139
5248
  }
3140
5249
  atomicAdd(&counters.hitCount, 1u);
@@ -3148,19 +5257,23 @@ fn intersectActiveQueue(@builtin(global_invocation_id) globalId: vec3<u32>) {
3148
5257
  hitPrimitiveId,
3149
5258
  hitMaterialRefId,
3150
5259
  hitMediumRefId,
3151
- 0u,
5260
+ hitMaterialSlot,
3152
5261
  0u,
3153
5262
  0u,
3154
5263
  candidate.distance,
3155
- vec3<f32>(0.0),
5264
+ hitOcclusion,
5265
+ vec2<f32>(0.0),
3156
5266
  vec4<f32>(position, 1.0),
3157
5267
  vec4<f32>(candidate.geometricNormal, 0.0),
3158
- vec4<f32>(candidate.shadingNormal, 0.0),
5268
+ vec4<f32>(hitShadingNormal, 0.0),
3159
5269
  vec4<f32>(candidate.barycentric, 0.0),
3160
5270
  vec4<f32>(candidate.uv, 0.0, 0.0),
3161
5271
  hitColor,
3162
5272
  hitEmission,
3163
- hitMaterial
5273
+ hitMaterial,
5274
+ hitMaterialResponse,
5275
+ hitMaterialExtension,
5276
+ hitSpecularColor
3164
5277
  );
3165
5278
  }
3166
5279
 
@@ -3229,60 +5342,106 @@ fn sample_environment_portal_direction(hit: HitRecord, seed: u32, fallback: vec3
3229
5342
  }
3230
5343
 
3231
5344
  fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult {
5345
+ let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
5346
+ let viewDirection = safe_normalize(-ray.direction.xyz, normal);
3232
5347
  let roughness = clamp(hit.material.x, 0.0, 1.0);
3233
- if (hit.materialKind == 1u) {
5348
+ let transmission = clamp(hit.materialExtension.z, 0.0, 1.0);
5349
+ if (hit.materialKind == 1u && roughness <= 0.02) {
3234
5350
  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,
5351
+ vec4<f32>(reflect(ray.direction.xyz, normal), 0.0),
5352
+ 1.0,
5353
+ ray.mediumRefId,
5354
+ RAY_FLAG_DELTA_SAMPLE,
3244
5355
  0u,
3245
- 0u
3246
5356
  );
3247
5357
  }
3248
5358
 
3249
- if (hit.materialKind == 2u || hit.materialKind == 3u) {
5359
+ if (hit.materialKind == 2u || hit.materialKind == 3u || transmission > 0.001) {
3250
5360
  let ior = max(hit.material.w, 1.01);
3251
5361
  let etaRatio = select(ior, 1.0 / ior, hit.frontFace == 1u);
3252
- let cosTheta = min(dot(-ray.direction.xyz, hit.shadingNormal.xyz), 1.0);
5362
+ let cosTheta = min(dot(-ray.direction.xyz, normal), 1.0);
3253
5363
  let sinTheta = sqrt(max(0.0, 1.0 - cosTheta * cosTheta));
3254
5364
  let cannotRefract = etaRatio * sinTheta > 1.0;
3255
5365
  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);
5366
+ let transmissionReflectChance = select(
5367
+ reflectChance,
5368
+ max(reflectChance, 1.0 - transmission),
5369
+ transmission > 0.001
5370
+ );
5371
+ if (cannotRefract || random01(seed + 23u) < transmissionReflectChance) {
5372
+ return ScatterResult(
5373
+ vec4<f32>(reflect(ray.direction.xyz, normal), 0.0),
5374
+ 1.0,
5375
+ ray.mediumRefId,
5376
+ RAY_FLAG_DELTA_SAMPLE,
5377
+ 0u,
5378
+ );
3258
5379
  }
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
- );
5380
+ return ScatterResult(
5381
+ vec4<f32>(refract_direction(ray.direction.xyz, normal, etaRatio), 0.0),
5382
+ 1.0,
5383
+ transmitted_medium_ref_id(ray, hit),
5384
+ RAY_FLAG_DELTA_SAMPLE,
5385
+ 0u,
5386
+ );
5387
+ }
5388
+
5389
+ let guidedEmissiveAvailable = config.emissiveTriangleCount > 0u;
5390
+ let guidedPortalAvailable =
5391
+ config.environmentPortalCount > 0u && config.environmentPortalMode != 0u;
5392
+ let guidedSelector = random01(seed + 17u);
5393
+ if (guidedEmissiveAvailable && guidedSelector < 0.18) {
5394
+ let guidedDirection = sample_emissive_triangle_direction(hit, seed + 101u, normal);
5395
+ if (dot(normal, guidedDirection) > 0.000001) {
5396
+ let guidedPdf = max(evaluate_surface_bsdf_pdf(hit, viewDirection, guidedDirection), 0.000001);
5397
+ return ScatterResult(
5398
+ vec4<f32>(guidedDirection, 0.0),
5399
+ guidedPdf,
5400
+ ray.mediumRefId,
5401
+ RAY_FLAG_GUIDED_EMISSIVE,
5402
+ 0u,
5403
+ );
5404
+ }
5405
+ }
5406
+ if (guidedPortalAvailable && guidedSelector < 0.32) {
5407
+ let guidedDirection = sample_environment_portal_direction(hit, seed + 131u, normal);
5408
+ if (dot(normal, guidedDirection) > 0.000001) {
5409
+ let guidedPdf = max(evaluate_surface_bsdf_pdf(hit, viewDirection, guidedDirection), 0.000001);
5410
+ return ScatterResult(vec4<f32>(guidedDirection, 0.0), guidedPdf, ray.mediumRefId, 0u, 0u);
5411
+ }
5412
+ }
5413
+
5414
+ let weights = surface_bsdf_sampling_weights(hit);
5415
+ let selector = random01(seed + 31u);
5416
+ var lightDirection = normal;
5417
+ if (selector < weights.x) {
5418
+ lightDirection = cosine_sample_hemisphere(
5419
+ vec2<f32>(random01(seed + 37u), random01(seed + 41u)),
5420
+ normal
5421
+ );
5422
+ } else if (selector < weights.x + weights.y) {
5423
+ let halfVector = importance_sample_ggx(
5424
+ vec2<f32>(random01(seed + 47u), random01(seed + 53u)),
5425
+ max(roughness, 0.02),
5426
+ normal
5427
+ );
5428
+ lightDirection = safe_normalize(reflect(-viewDirection, halfVector), normal);
5429
+ } else {
5430
+ let halfVector = importance_sample_ggx(
5431
+ vec2<f32>(random01(seed + 59u), random01(seed + 61u)),
5432
+ max(clamp(hit.materialExtension.x, 0.0, 1.0), 0.02),
5433
+ normal
5434
+ );
5435
+ lightDirection = safe_normalize(reflect(-viewDirection, halfVector), normal);
5436
+ }
5437
+ if (dot(normal, lightDirection) <= 0.000001) {
5438
+ lightDirection = cosine_sample_hemisphere(
5439
+ vec2<f32>(random01(seed + 67u), random01(seed + 71u)),
5440
+ normal
5441
+ );
5442
+ }
5443
+ let pdf = max(evaluate_surface_bsdf_pdf(hit, viewDirection, lightDirection), 0.000001);
5444
+ return ScatterResult(vec4<f32>(lightDirection, 0.0), pdf, ray.mediumRefId, 0u, 0u);
3286
5445
  }
3287
5446
 
3288
5447
  @compute @workgroup_size(64)
@@ -3295,15 +5454,17 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
3295
5454
 
3296
5455
  let ray = activeQueue[index];
3297
5456
  let hit = hits[index];
5457
+ let segmentTransmittance = medium_transmittance(ray.mediumRefId, hit.distance);
5458
+ let arrivingThroughput = ray.throughput.xyz * segmentTransmittance;
3298
5459
  var contribution = vec3<f32>(0.0);
3299
5460
 
3300
5461
  if (hit.hitType == 1u) {
3301
5462
  let guidedLightWeight = select(1.0, 0.24, (ray.flags & RAY_FLAG_GUIDED_EMISSIVE) != 0u);
3302
5463
  let sourceRadiance = max(hit.emission.xyz, hit.color.xyz) * guidedLightWeight;
3303
5464
  if (deferred_path_resolve_enabled()) {
3304
- record_deferred_terminal_source(ray, sourceRadiance);
5465
+ record_deferred_terminal_source(ray, sourceRadiance * segmentTransmittance);
3305
5466
  } else {
3306
- contribution = clamp_sample_radiance(ray.throughput.xyz * sourceRadiance);
5467
+ contribution = clamp_sample_radiance(arrivingThroughput * sourceRadiance);
3307
5468
  accumulation[ray.rayId] =
3308
5469
  accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
3309
5470
  }
@@ -3312,10 +5473,17 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
3312
5473
  }
3313
5474
 
3314
5475
  if (hit.hitType == 2u) {
5476
+ var sourceRadiance = hit.color.xyz;
5477
+ if ((ray.flags & RAY_FLAG_DELTA_SAMPLE) == 0u) {
5478
+ let bsdfPdf = max(ray.throughput.w, 0.000001);
5479
+ let lightPdf = environment_direction_pdf(ray.direction.xyz);
5480
+ let misWeight = power_heuristic(bsdfPdf, lightPdf);
5481
+ sourceRadiance = sourceRadiance * misWeight;
5482
+ }
3315
5483
  if (deferred_path_resolve_enabled()) {
3316
- record_deferred_terminal_source(ray, hit.color.xyz);
5484
+ record_deferred_terminal_source(ray, sourceRadiance * segmentTransmittance);
3317
5485
  } else {
3318
- contribution = clamp_sample_radiance(ray.throughput.xyz * max(hit.color.xyz, config.ambientColor.xyz));
5486
+ contribution = clamp_sample_radiance(arrivingThroughput * sourceRadiance);
3319
5487
  accumulation[ray.rayId] =
3320
5488
  accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
3321
5489
  }
@@ -3323,24 +5491,47 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
3323
5491
  return;
3324
5492
  }
3325
5493
 
3326
- let response = surface_path_response(hit);
5494
+ let response = stabilize_surface_path_response(
5495
+ ray,
5496
+ hit,
5497
+ surface_path_response(hit) * segmentTransmittance
5498
+ );
3327
5499
  record_deferred_path_response(ray, response);
3328
5500
 
3329
5501
  let shouldEstimateDirectEnvironment =
3330
- !deferred_path_resolve_enabled() &&
3331
5502
  (hit.materialKind == 0u || hit.materialKind == 1u) &&
3332
- hit.material.z >= 0.95;
5503
+ hit.material.z >= 0.95 &&
5504
+ ray.bounce < 2u;
3333
5505
  if (shouldEstimateDirectEnvironment) {
3334
- let directEnvironment = surface_direct_environment_contribution(ray, hit);
5506
+ let directEnvironment = surface_direct_environment_contribution(
5507
+ RayRecord(
5508
+ ray.rayId,
5509
+ ray.parentRayId,
5510
+ ray.sourcePixelId,
5511
+ ray.sampleId,
5512
+ ray.bounce,
5513
+ ray.mediumRefId,
5514
+ ray.flags,
5515
+ 0u,
5516
+ ray.origin,
5517
+ ray.direction,
5518
+ vec4<f32>(arrivingThroughput, ray.throughput.w)
5519
+ ),
5520
+ hit
5521
+ );
3335
5522
  accumulation[ray.rayId] =
3336
5523
  accumulation[ray.rayId] + vec4<f32>(directEnvironment * sample_weight(), 0.0);
3337
5524
  }
3338
5525
 
3339
5526
  if (ray.bounce + 1u >= config.maxDepth) {
3340
5527
  if (deferred_path_resolve_enabled()) {
3341
- record_deferred_terminal_source(ray, terminal_surface_environment_source(hit));
5528
+ record_deferred_terminal_source(ray, terminal_surface_environment_source(ray, hit));
3342
5529
  } else {
3343
- let terminalEnvironment = terminal_surface_environment_contribution(ray, hit);
5530
+ let terminalEnvironment = terminal_surface_environment_contribution(
5531
+ ray,
5532
+ arrivingThroughput,
5533
+ hit
5534
+ );
3344
5535
  accumulation[ray.rayId] =
3345
5536
  accumulation[ray.rayId] + vec4<f32>(terminalEnvironment * sample_weight(), 1.0);
3346
5537
  }
@@ -3353,9 +5544,13 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
3353
5544
  let nextIndex = atomicAdd(&counters.nextCount, 1u);
3354
5545
  if (nextIndex >= config.tilePixelCount) {
3355
5546
  if (deferred_path_resolve_enabled()) {
3356
- record_deferred_terminal_source(ray, terminal_surface_environment_source(hit));
5547
+ record_deferred_terminal_source(ray, terminal_surface_environment_source(ray, hit));
3357
5548
  } else {
3358
- let overflowEnvironment = terminal_surface_environment_contribution(ray, hit);
5549
+ let overflowEnvironment = terminal_surface_environment_contribution(
5550
+ ray,
5551
+ arrivingThroughput,
5552
+ hit
5553
+ );
3359
5554
  accumulation[ray.rayId] =
3360
5555
  accumulation[ray.rayId] + vec4<f32>(overflowEnvironment * sample_weight(), 1.0);
3361
5556
  }
@@ -3369,12 +5564,12 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
3369
5564
  ray.sourcePixelId,
3370
5565
  ray.sampleId,
3371
5566
  ray.bounce + 1u,
3372
- ray.mediumRefId,
5567
+ scatter.mediumRefId,
3373
5568
  scatter.flags,
3374
5569
  0u,
3375
5570
  vec4<f32>(offset_origin(hit.position.xyz, hit.shadingNormal.xyz), 1.0),
3376
5571
  scatter.direction,
3377
- vec4<f32>(throughput, ray.throughput.w)
5572
+ vec4<f32>(throughput, scatter.pdf)
3378
5573
  );
3379
5574
  }
3380
5575
 
@@ -3443,8 +5638,11 @@ fn denoiseLinearRadiance(@builtin(global_invocation_id) globalId: vec3<u32>) {
3443
5638
 
3444
5639
  let pixel = vec2<i32>(i32(x), i32(y));
3445
5640
  let center = textureLoad(denoiseInputRadiance, pixel, 0).xyz;
3446
- var sum = center * 1.4;
3447
- var totalWeight = 1.4;
5641
+ let strength = denoise_strength();
5642
+ let kernelRadius = denoise_kernel_radius();
5643
+ let centerWeight = 1.7 - strength * 0.35;
5644
+ var sum = center * centerWeight;
5645
+ var totalWeight = centerWeight;
3448
5646
  let centerRange = denoise_range_space(center);
3449
5647
 
3450
5648
  for (var oy = -2i; oy <= 2i; oy = oy + 1i) {
@@ -3452,13 +5650,16 @@ fn denoiseLinearRadiance(@builtin(global_invocation_id) globalId: vec3<u32>) {
3452
5650
  if (ox == 0i && oy == 0i) {
3453
5651
  continue;
3454
5652
  }
5653
+ if (abs(ox) > kernelRadius || abs(oy) > kernelRadius) {
5654
+ continue;
5655
+ }
3455
5656
  let sx = clamp(i32(x) + ox, 0i, i32(config.canvasWidth) - 1i);
3456
5657
  let sy = clamp(i32(y) + oy, 0i, i32(config.canvasHeight) - 1i);
3457
5658
  let sampleColor = textureLoad(denoiseInputRadiance, vec2<i32>(sx, sy), 0).xyz;
3458
5659
  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);
5660
+ let rangeWeight = 1.0 / (1.0 + colorDistance * (11.0 + strength * 6.0));
5661
+ let distanceWeight = 1.0 / (1.0 + f32(ox * ox + oy * oy) * (0.62 + strength * 0.24));
5662
+ let diagonalWeight = select(1.0, 0.92, abs(ox) + abs(oy) > 1i);
3462
5663
  let weight = rangeWeight * diagonalWeight * distanceWeight;
3463
5664
  sum = sum + sampleColor * weight;
3464
5665
  totalWeight = totalWeight + weight;
@@ -3466,8 +5667,9 @@ fn denoiseLinearRadiance(@builtin(global_invocation_id) globalId: vec3<u32>) {
3466
5667
  }
3467
5668
 
3468
5669
  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));
5670
+ let outlier = saturate(length(denoise_range_space(center) - denoise_range_space(filtered)) * 2.1);
5671
+ let blend = min(0.3, strength * (0.62 + outlier * 0.12));
5672
+ let color = min(mix(center, filtered, blend), vec3<f32>(16.0));
3471
5673
  textureStore(denoisedRadianceImage, pixel, vec4<f32>(color, 1.0));
3472
5674
  }
3473
5675
 
@@ -3481,8 +5683,10 @@ fn resolveDenoisedOutputImage(@builtin(global_invocation_id) globalId: vec3<u32>
3481
5683
 
3482
5684
  let pixel = vec2<i32>(i32(x), i32(y));
3483
5685
  let center = textureLoad(finalDenoiseInputRadiance, pixel, 0).xyz;
3484
- var sum = center * 1.25;
3485
- var totalWeight = 1.25;
5686
+ let strength = denoise_strength();
5687
+ let centerWeight = 1.35 - strength * 0.25;
5688
+ var sum = center * centerWeight;
5689
+ var totalWeight = centerWeight;
3486
5690
  let centerRange = denoise_range_space(center);
3487
5691
 
3488
5692
  for (var oy = -1i; oy <= 1i; oy = oy + 1i) {
@@ -3494,8 +5698,8 @@ fn resolveDenoisedOutputImage(@builtin(global_invocation_id) globalId: vec3<u32>
3494
5698
  let sy = clamp(i32(y) + oy, 0i, i32(config.canvasHeight) - 1i);
3495
5699
  let sampleColor = textureLoad(finalDenoiseInputRadiance, vec2<i32>(sx, sy), 0).xyz;
3496
5700
  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);
5701
+ let rangeWeight = 1.0 / (1.0 + colorDistance * (12.0 + strength * 8.0));
5702
+ let distanceWeight = 1.0 / (1.0 + f32(ox * ox + oy * oy) * (0.82 + strength * 0.28));
3499
5703
  let weight = rangeWeight * distanceWeight;
3500
5704
  sum = sum + sampleColor * weight;
3501
5705
  totalWeight = totalWeight + weight;
@@ -3503,8 +5707,9 @@ fn resolveDenoisedOutputImage(@builtin(global_invocation_id) globalId: vec3<u32>
3503
5707
  }
3504
5708
 
3505
5709
  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));
5710
+ let outlier = saturate(length(denoise_range_space(center) - denoise_range_space(filtered)) * 2.2);
5711
+ let blend = min(0.18, strength * (0.42 + outlier * 0.08));
5712
+ let radiance = min(mix(center, filtered, blend), vec3<f32>(16.0));
3508
5713
  textureStore(denoisedOutputImage, pixel, vec4<f32>(tone_map_radiance(radiance), 1.0));
3509
5714
  }
3510
5715
  `;
@@ -3580,95 +5785,25 @@ function createAdapterInfoSnapshot(adapter) {
3580
5785
  vendor: typeof info.vendor === "string" ? info.vendor : "",
3581
5786
  architecture: typeof info.architecture === "string" ? info.architecture : "",
3582
5787
  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"
5788
+ description: typeof info.description === "string" ? info.description : ""
5789
+ });
5790
+ }
5791
+ function createGpuAdapterParallelismDiagnostics(adapter, device) {
5792
+ return Object.freeze({
5793
+ physicalCoreCount: null,
5794
+ physicalCoreCountAvailable: false,
5795
+ physicalCoreCountUnavailableReason: "WebGPU does not expose physical GPU core counts.",
5796
+ adapterInfo: createAdapterInfoSnapshot(adapter),
5797
+ adapterLimits: Object.freeze({
5798
+ maxComputeInvocationsPerWorkgroup: readGpuLimit(adapter, device, "maxComputeInvocationsPerWorkgroup"),
5799
+ maxComputeWorkgroupSizeX: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeX"),
5800
+ maxComputeWorkgroupSizeY: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeY"),
5801
+ maxComputeWorkgroupSizeZ: readGpuLimit(adapter, device, "maxComputeWorkgroupSizeZ"),
5802
+ maxComputeWorkgroupsPerDimension: readGpuLimit(adapter, device, "maxComputeWorkgroupsPerDimension"),
5803
+ maxStorageBuffersPerShaderStage: readGpuLimit(adapter, device, "maxStorageBuffersPerShaderStage"),
5804
+ maxStorageBufferBindingSize: readGpuLimit(adapter, device, "maxStorageBufferBindingSize")
5805
+ }),
5806
+ configuredWorkgroupSize: WORKGROUP_SIZE
3672
5807
  });
3673
5808
  }
3674
5809
  function createEnvironmentMapSnapshot(environmentMap) {
@@ -3676,12 +5811,38 @@ function createEnvironmentMapSnapshot(environmentMap) {
3676
5811
  enabled: environmentMap.enabled,
3677
5812
  width: environmentMap.width,
3678
5813
  height: environmentMap.height,
5814
+ mipLevelCount: environmentMap.mipLevelCount ?? 1,
3679
5815
  projection: environmentMap.projection,
3680
5816
  intensity: environmentMap.intensity,
3681
5817
  rotationRadians: environmentMap.rotationRadians,
3682
- ambientStrength: environmentMap.ambientStrength
5818
+ ambientStrength: environmentMap.ambientStrength,
5819
+ hasImportanceData: environmentMap.hasImportanceData === true
3683
5820
  });
3684
5821
  }
5822
+ function nowMs() {
5823
+ if (typeof performance !== "undefined" && typeof performance.now === "function") {
5824
+ return performance.now();
5825
+ }
5826
+ return Date.now();
5827
+ }
5828
+ function estimateSubmittedGpuWorkTimeoutMs(config, tileCount, overrideTimeoutMs = null) {
5829
+ if (Number.isFinite(overrideTimeoutMs)) {
5830
+ return Math.max(1, Math.trunc(Number(overrideTimeoutMs)));
5831
+ }
5832
+ const samplesPerPixel = Math.max(
5833
+ 1,
5834
+ Number(config?.renderedSamplesPerPixel ?? config?.samplesPerPixel ?? 1)
5835
+ );
5836
+ const maxDepth = Math.max(1, Number(config?.maxDepth ?? 1));
5837
+ const deferredResolvePasses = config?.deferredPathResolve ? 1 : 0;
5838
+ const denoisePasses = config?.denoise ? samplesPerPixel < 4 ? 2 : 1 : 0;
5839
+ const tiles = Math.max(1, Number(tileCount ?? 1));
5840
+ const estimatedPasses = tiles * (samplesPerPixel * (maxDepth + 1 + deferredResolvePasses) + denoisePasses + 1);
5841
+ return Math.min(
5842
+ GPU_MAX_SUBMITTED_WORK_TIMEOUT_MS,
5843
+ GPU_SUBMITTED_WORK_TIMEOUT_MS + estimatedPasses * 5
5844
+ );
5845
+ }
3685
5846
  async function createWavefrontPathTracingComputeRenderer(options = {}) {
3686
5847
  assertAnalyticDisplayQualityPolicy(options);
3687
5848
  const constants = getGpuUsageConstants();
@@ -3886,6 +6047,65 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3886
6047
  config.environmentMap,
3887
6048
  config.environmentColor
3888
6049
  );
6050
+ const environmentSamplingResource = createEnvironmentSamplingTextureResource(
6051
+ device,
6052
+ constants,
6053
+ config.environmentMap,
6054
+ config.environmentColor
6055
+ );
6056
+ let mediumTextureResource = createMediumTextureResource(
6057
+ device,
6058
+ constants,
6059
+ config.mediums
6060
+ );
6061
+ config = Object.freeze({
6062
+ ...config,
6063
+ environmentMap: Object.freeze({
6064
+ ...config.environmentMap,
6065
+ width: environmentMapResource.width,
6066
+ height: environmentMapResource.height,
6067
+ mipLevelCount: environmentMapResource.mipLevelCount,
6068
+ hasImportanceData: environmentSamplingResource.hasImportanceData
6069
+ })
6070
+ });
6071
+ const brdfLutResource = createBrdfLutResource(device, constants);
6072
+ const baseColorAtlasResource = createAtlasTextureResource(
6073
+ device,
6074
+ constants,
6075
+ config.gpuMaterialSource.baseColorAtlas,
6076
+ "plasius.wavefront.materialAtlas.baseColor"
6077
+ );
6078
+ const metallicRoughnessAtlasResource = createAtlasTextureResource(
6079
+ device,
6080
+ constants,
6081
+ config.gpuMaterialSource.metallicRoughnessAtlas,
6082
+ "plasius.wavefront.materialAtlas.metallicRoughness"
6083
+ );
6084
+ const normalAtlasResource = createAtlasTextureResource(
6085
+ device,
6086
+ constants,
6087
+ config.gpuMaterialSource.normalAtlas,
6088
+ "plasius.wavefront.materialAtlas.normal"
6089
+ );
6090
+ const occlusionAtlasResource = createAtlasTextureResource(
6091
+ device,
6092
+ constants,
6093
+ config.gpuMaterialSource.occlusionAtlas,
6094
+ "plasius.wavefront.materialAtlas.occlusion"
6095
+ );
6096
+ const emissiveAtlasResource = createAtlasTextureResource(
6097
+ device,
6098
+ constants,
6099
+ config.gpuMaterialSource.emissiveAtlas,
6100
+ "plasius.wavefront.materialAtlas.emissive"
6101
+ );
6102
+ const materialAtlasSampler = device.createSampler({
6103
+ label: "plasius.wavefront.materialAtlasSampler",
6104
+ addressModeU: "clamp-to-edge",
6105
+ addressModeV: "clamp-to-edge",
6106
+ magFilter: "linear",
6107
+ minFilter: "linear"
6108
+ });
3889
6109
  const traceBindGroupLayout = device.createBindGroupLayout({
3890
6110
  label: "plasius.wavefront.traceBindGroupLayout",
3891
6111
  entries: [
@@ -3915,7 +6135,17 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3915
6135
  { binding: 19, visibility: constants.shader.COMPUTE, buffer: { type: "read-only-storage" } },
3916
6136
  { binding: 20, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
3917
6137
  { binding: 21, visibility: constants.shader.COMPUTE, sampler: { type: "filtering" } },
3918
- { binding: 22, visibility: constants.shader.COMPUTE, buffer: { type: "storage" } }
6138
+ { binding: 22, visibility: constants.shader.COMPUTE, buffer: { type: "storage" } },
6139
+ { binding: 23, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
6140
+ { binding: 24, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
6141
+ { binding: 25, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
6142
+ { binding: 26, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
6143
+ { binding: 27, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
6144
+ { binding: 28, visibility: constants.shader.COMPUTE, sampler: { type: "filtering" } },
6145
+ { binding: 29, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
6146
+ { binding: 30, visibility: constants.shader.COMPUTE, sampler: { type: "filtering" } },
6147
+ { binding: 31, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
6148
+ { binding: 32, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } }
3919
6149
  ]
3920
6150
  });
3921
6151
  const accelerationBindGroupLayout = device.createBindGroupLayout({
@@ -3994,6 +6224,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
3994
6224
  label: "plasius.wavefront.computeShader",
3995
6225
  code: WAVEFRONT_COMPUTE_WGSL
3996
6226
  });
6227
+ await assertShaderModuleCompiles(computeShader, "plasius.wavefront.computeShader");
3997
6228
  const pipelines = {
3998
6229
  prepareMeshTrianglesAndLeaves: await createComputePipeline(
3999
6230
  device,
@@ -4092,14 +6323,27 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4092
6323
  { binding: 19, resource: { buffer: environmentPortalBuffer } },
4093
6324
  { binding: 20, resource: environmentMapResource.view },
4094
6325
  { binding: 21, resource: environmentMapResource.sampler },
4095
- { binding: 22, resource: { buffer: pathVertexBuffer } }
6326
+ { binding: 22, resource: { buffer: pathVertexBuffer } },
6327
+ { binding: 23, resource: baseColorAtlasResource.view },
6328
+ { binding: 24, resource: metallicRoughnessAtlasResource.view },
6329
+ { binding: 25, resource: normalAtlasResource.view },
6330
+ { binding: 26, resource: occlusionAtlasResource.view },
6331
+ { binding: 27, resource: emissiveAtlasResource.view },
6332
+ { binding: 28, resource: materialAtlasSampler },
6333
+ { binding: 29, resource: brdfLutResource.view },
6334
+ { binding: 30, resource: brdfLutResource.sampler },
6335
+ { binding: 31, resource: environmentSamplingResource.view },
6336
+ { binding: 32, resource: mediumTextureResource.view }
4096
6337
  ]
4097
6338
  });
4098
6339
  }
4099
- const bindGroups = [
4100
- createTraceBindGroup(activeQueue, nextQueue, "plasius.wavefront.bind.activeNext"),
4101
- createTraceBindGroup(nextQueue, activeQueue, "plasius.wavefront.bind.nextActive")
4102
- ];
6340
+ function createTraceBindGroups() {
6341
+ return [
6342
+ createTraceBindGroup(activeQueue, nextQueue, "plasius.wavefront.bind.activeNext"),
6343
+ createTraceBindGroup(nextQueue, activeQueue, "plasius.wavefront.bind.nextActive")
6344
+ ];
6345
+ }
6346
+ let bindGroups = createTraceBindGroups();
4103
6347
  const bvhBuildBindGroup = device.createBindGroup({
4104
6348
  label: "plasius.wavefront.bind.bvhBuild",
4105
6349
  layout: accelerationBindGroupLayout,
@@ -4145,6 +6389,11 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4145
6389
  outputView,
4146
6390
  "plasius.wavefront.bind.denoise.scratchToOutput"
4147
6391
  );
6392
+ const denoiseDirectResolveBindGroup = createDenoiseResolveBindGroup(
6393
+ radianceView,
6394
+ outputView,
6395
+ "plasius.wavefront.bind.denoise.radianceToOutput"
6396
+ );
4148
6397
  const presentBindGroupLayout = device.createBindGroupLayout({
4149
6398
  label: "plasius.wavefront.presentBindGroupLayout",
4150
6399
  entries: [
@@ -4182,23 +6431,129 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4182
6431
  let accelerationBuilt = !config.gpuAccelerationBuildRequired;
4183
6432
  let accelerationBuildCount = 0;
4184
6433
  let activeCameraOptions = options.camera ?? DEFAULT_CAMERA;
6434
+ let lastCompletedFrameTimeMs = null;
6435
+ let lastCompletedSamplesPerPixel = Math.max(1, config.samplesPerPixel);
4185
6436
  let lastGpuParallelism = createGpuParallelismDiagnostics(
4186
6437
  gpuAdapterParallelism,
4187
6438
  createGpuParallelismCounters()
4188
6439
  );
6440
+ function resolveRenderedSamplesPerPixel(renderOptions = {}, awaitGPUCompletion = true) {
6441
+ const targetSamplesPerPixel = clamp(
6442
+ readPositiveInteger(
6443
+ "samplesPerPixel",
6444
+ renderOptions.samplesPerPixel,
6445
+ config.samplesPerPixel
6446
+ ),
6447
+ 1,
6448
+ config.samplesPerPixel
6449
+ );
6450
+ const frameTimeBudgetMs = Number.isFinite(renderOptions.frameTimeBudgetMs) ? Math.max(0, Number(renderOptions.frameTimeBudgetMs)) : null;
6451
+ const minimumSamplesPerPixel = clamp(
6452
+ readPositiveInteger(
6453
+ "minimumSamplesPerPixel",
6454
+ renderOptions.minimumSamplesPerPixel,
6455
+ frameTimeBudgetMs !== null && targetSamplesPerPixel > 1 ? 1 : targetSamplesPerPixel
6456
+ ),
6457
+ 1,
6458
+ targetSamplesPerPixel
6459
+ );
6460
+ if (frameTimeBudgetMs === null || !awaitGPUCompletion || targetSamplesPerPixel <= minimumSamplesPerPixel) {
6461
+ return Object.freeze({
6462
+ renderedSamplesPerPixel: targetSamplesPerPixel,
6463
+ targetSamplesPerPixel,
6464
+ minimumSamplesPerPixel,
6465
+ frameTimeBudgetMs,
6466
+ budgetConstrained: false
6467
+ });
6468
+ }
6469
+ const estimatedSampleTimeMs = Number.isFinite(lastCompletedFrameTimeMs) && lastCompletedFrameTimeMs > 0 ? lastCompletedFrameTimeMs / Math.max(1, lastCompletedSamplesPerPixel) : null;
6470
+ if (!Number.isFinite(estimatedSampleTimeMs) || estimatedSampleTimeMs <= 0) {
6471
+ return Object.freeze({
6472
+ renderedSamplesPerPixel: minimumSamplesPerPixel,
6473
+ targetSamplesPerPixel,
6474
+ minimumSamplesPerPixel,
6475
+ frameTimeBudgetMs,
6476
+ budgetConstrained: minimumSamplesPerPixel < targetSamplesPerPixel
6477
+ });
6478
+ }
6479
+ const budgetLimitedSamples = clamp(
6480
+ Math.floor(frameTimeBudgetMs / estimatedSampleTimeMs),
6481
+ minimumSamplesPerPixel,
6482
+ targetSamplesPerPixel
6483
+ );
6484
+ return Object.freeze({
6485
+ renderedSamplesPerPixel: budgetLimitedSamples,
6486
+ targetSamplesPerPixel,
6487
+ minimumSamplesPerPixel,
6488
+ frameTimeBudgetMs,
6489
+ budgetConstrained: budgetLimitedSamples < targetSamplesPerPixel
6490
+ });
6491
+ }
6492
+ function createFrameStats({
6493
+ frameIndex,
6494
+ accelerationBuildSubmitted,
6495
+ frameSubmissionCount,
6496
+ parallelismCounters,
6497
+ renderedSamplesPerPixel,
6498
+ targetSamplesPerPixel,
6499
+ frameTimeBudgetMs,
6500
+ budgetConstrained
6501
+ }) {
6502
+ lastGpuParallelism = createGpuParallelismDiagnostics(gpuAdapterParallelism, parallelismCounters);
6503
+ const commandSubmissions = frameSubmissionCount + (accelerationBuildSubmitted ? 1 : 0);
6504
+ return Object.freeze({
6505
+ frame,
6506
+ frameIndex,
6507
+ width: config.width,
6508
+ height: config.height,
6509
+ maxDepth: config.maxDepth,
6510
+ tiles: tiles.length,
6511
+ tileSize: config.tileSize,
6512
+ samplesPerPixel: targetSamplesPerPixel,
6513
+ renderedSamplesPerPixel,
6514
+ frameTimeBudgetMs,
6515
+ budgetConstrained,
6516
+ maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
6517
+ screenRays: config.width * config.height,
6518
+ primaryRays: config.width * config.height * renderedSamplesPerPixel,
6519
+ sceneObjectCount: config.sceneObjectCount,
6520
+ triangleCount: config.triangleCount,
6521
+ emissiveTriangleCount: config.emissiveTriangleCount,
6522
+ environmentPortalCount: config.environmentPortalCount,
6523
+ environmentPortalMode: config.environmentPortalMode,
6524
+ mediumCount: config.mediumCount,
6525
+ environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
6526
+ deferredPathResolve: config.deferredPathResolve,
6527
+ bvhNodeCount: config.bvhNodeCount,
6528
+ displayQuality: config.displayQuality,
6529
+ accelerationBuildMode: config.accelerationBuildMode,
6530
+ gpuAccelerationBuildRequired: config.gpuAccelerationBuildRequired,
6531
+ accelerationBuildSubmitted,
6532
+ accelerationBuilt,
6533
+ accelerationBuildCount,
6534
+ commandSubmissions,
6535
+ frameConfigSlots: frameConfigSlotCount,
6536
+ gpuParallelism: lastGpuParallelism,
6537
+ memory: config.memory
6538
+ });
6539
+ }
6540
+ function writeFrameConfigSlot(slot, tile, frameIndex, buildRange = {}) {
6541
+ if (slot >= frameConfigSlotCount) {
6542
+ throw new Error("Wavefront frame config slot capacity exceeded.");
6543
+ }
6544
+ const offset = slot * configBufferStride;
6545
+ device.queue.writeBuffer(
6546
+ configBuffer,
6547
+ offset,
6548
+ createConfigPayload(config, tile, frameIndex, buildRange)
6549
+ );
6550
+ return offset;
6551
+ }
4189
6552
  function createFrameConfigWriter(frameIndex) {
4190
6553
  let slot = 0;
4191
6554
  return (tile, buildRange = {}) => {
4192
- if (slot >= frameConfigSlotCount) {
4193
- throw new Error("Wavefront frame config slot capacity exceeded.");
4194
- }
4195
- const offset = slot * configBufferStride;
6555
+ const offset = writeFrameConfigSlot(slot, tile, frameIndex, buildRange);
4196
6556
  slot += 1;
4197
- device.queue.writeBuffer(
4198
- configBuffer,
4199
- offset,
4200
- createConfigPayload(config, tile, frameIndex, buildRange)
4201
- );
4202
6557
  return offset;
4203
6558
  };
4204
6559
  }
@@ -4243,7 +6598,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4243
6598
  passEncoder.setPipeline(pipelines.prepareMeshTrianglesAndLeaves);
4244
6599
  const prepareWorkgroups = Math.ceil(config.bvhLeafSortCapacity / WORKGROUP_SIZE);
4245
6600
  passEncoder.dispatchWorkgroups(prepareWorkgroups);
4246
- recordDirectDispatch(parallelism, [prepareWorkgroups]);
6601
+ recordDirectDispatch(parallelism, [prepareWorkgroups], WORKGROUP_SIZE);
4247
6602
  passEncoder.setPipeline(pipelines.sortBvhLeafRefs);
4248
6603
  for (let stageIndex = 0; stageIndex < config.bvhSortStages.length; stageIndex += 1) {
4249
6604
  passEncoder.setBindGroup(0, bvhBuildBindGroup, [
@@ -4251,13 +6606,13 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4251
6606
  ]);
4252
6607
  const sortWorkgroups = Math.ceil(config.bvhLeafSortCapacity / WORKGROUP_SIZE);
4253
6608
  passEncoder.dispatchWorkgroups(sortWorkgroups);
4254
- recordDirectDispatch(parallelism, [sortWorkgroups]);
6609
+ recordDirectDispatch(parallelism, [sortWorkgroups], WORKGROUP_SIZE);
4255
6610
  }
4256
6611
  passEncoder.setBindGroup(0, bvhBuildBindGroup, [0]);
4257
6612
  passEncoder.setPipeline(pipelines.writeSortedBvhLeaves);
4258
6613
  const leafWriteWorkgroups = Math.ceil(config.triangleCount / WORKGROUP_SIZE);
4259
6614
  passEncoder.dispatchWorkgroups(leafWriteWorkgroups);
4260
- recordDirectDispatch(parallelism, [leafWriteWorkgroups]);
6615
+ recordDirectDispatch(parallelism, [leafWriteWorkgroups], WORKGROUP_SIZE);
4261
6616
  passEncoder.setPipeline(pipelines.buildBvhInternalLevel);
4262
6617
  for (let levelIndex = 0; levelIndex < config.bvhBuildLevels.length; levelIndex += 1) {
4263
6618
  const buildLevel = config.bvhBuildLevels[levelIndex];
@@ -4266,7 +6621,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4266
6621
  ]);
4267
6622
  const levelWorkgroups = Math.ceil(buildLevel.count / WORKGROUP_SIZE);
4268
6623
  passEncoder.dispatchWorkgroups(levelWorkgroups);
4269
- recordDirectDispatch(parallelism, [levelWorkgroups]);
6624
+ recordDirectDispatch(parallelism, [levelWorkgroups], WORKGROUP_SIZE);
4270
6625
  }
4271
6626
  passEncoder.end();
4272
6627
  device.queue.submit([encoder.finish()]);
@@ -4282,7 +6637,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4282
6637
  generatePass.setBindGroup(0, bindGroups[0], [configOffset]);
4283
6638
  generatePass.setPipeline(pipelines.generatePrimaryRays);
4284
6639
  generatePass.dispatchWorkgroups(tileWorkgroups);
4285
- recordDirectDispatch(parallelism, [tileWorkgroups]);
6640
+ recordDirectDispatch(parallelism, [tileWorkgroups], WORKGROUP_SIZE);
4286
6641
  generatePass.end();
4287
6642
  for (let bounceIndex = 0; bounceIndex < config.maxDepth; bounceIndex += 1) {
4288
6643
  encoder.copyBufferToBuffer(
@@ -4298,10 +6653,10 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4298
6653
  passEncoder.setBindGroup(0, bindGroups[bounceIndex % 2], [configOffset]);
4299
6654
  passEncoder.setPipeline(pipelines.intersectActiveQueue);
4300
6655
  passEncoder.dispatchWorkgroupsIndirect(activeDispatchBuffer, 0);
4301
- recordIndirectDispatch(parallelism, tileWorkgroups);
6656
+ recordIndirectDispatch(parallelism, tileWorkgroups, WORKGROUP_SIZE);
4302
6657
  passEncoder.setPipeline(pipelines.resolveSurfaceRecords);
4303
6658
  passEncoder.dispatchWorkgroupsIndirect(activeDispatchBuffer, 0);
4304
- recordIndirectDispatch(parallelism, tileWorkgroups);
6659
+ recordIndirectDispatch(parallelism, tileWorkgroups, WORKGROUP_SIZE);
4305
6660
  passEncoder.setPipeline(pipelines.compactAndSwapQueues);
4306
6661
  passEncoder.dispatchWorkgroups(1);
4307
6662
  recordDirectDispatch(parallelism, [1], 1);
@@ -4316,30 +6671,45 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4316
6671
  passEncoder.setBindGroup(0, bindGroups[0], [configOffset]);
4317
6672
  passEncoder.setPipeline(pipelines.accumulateTerminalRadiance);
4318
6673
  passEncoder.dispatchWorkgroups(tileWorkgroups);
4319
- recordDirectDispatch(parallelism, [tileWorkgroups]);
6674
+ recordDirectDispatch(parallelism, [tileWorkgroups], WORKGROUP_SIZE);
4320
6675
  passEncoder.end();
4321
6676
  }
4322
- function encodeDenoise(encoder, configOffset, parallelism) {
6677
+ function encodeDenoise(encoder, configOffset, parallelism, renderedSamplesPerPixel = config.samplesPerPixel) {
4323
6678
  if (!config.denoise) {
4324
6679
  return;
4325
6680
  }
4326
6681
  const denoiseWorkgroupsX = Math.ceil(config.width / 8);
4327
6682
  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();
6683
+ const useTwoPassDenoise = renderedSamplesPerPixel < 4;
6684
+ if (useTwoPassDenoise) {
6685
+ const radiancePass = encoder.beginComputePass({
6686
+ label: "plasius.wavefront.denoiseRadiancePass"
6687
+ });
6688
+ radiancePass.setBindGroup(0, denoiseRadianceBindGroup, [configOffset]);
6689
+ radiancePass.setPipeline(pipelines.denoiseLinearRadiance);
6690
+ radiancePass.dispatchWorkgroups(denoiseWorkgroupsX, denoiseWorkgroupsY);
6691
+ recordDirectDispatch(
6692
+ parallelism,
6693
+ [denoiseWorkgroupsX, denoiseWorkgroupsY],
6694
+ WORKGROUP_SIZE
6695
+ );
6696
+ radiancePass.end();
6697
+ }
4336
6698
  const resolvePass = encoder.beginComputePass({
4337
6699
  label: "plasius.wavefront.denoiseResolvePass"
4338
6700
  });
4339
- resolvePass.setBindGroup(0, denoiseResolveBindGroup, [configOffset]);
6701
+ resolvePass.setBindGroup(
6702
+ 0,
6703
+ useTwoPassDenoise ? denoiseResolveBindGroup : denoiseDirectResolveBindGroup,
6704
+ [configOffset]
6705
+ );
4340
6706
  resolvePass.setPipeline(pipelines.resolveDenoisedOutputImage);
4341
6707
  resolvePass.dispatchWorkgroups(denoiseWorkgroupsX, denoiseWorkgroupsY);
4342
- recordDirectDispatch(parallelism, [denoiseWorkgroupsX, denoiseWorkgroupsY]);
6708
+ recordDirectDispatch(
6709
+ parallelism,
6710
+ [denoiseWorkgroupsX, denoiseWorkgroupsY],
6711
+ WORKGROUP_SIZE
6712
+ );
4343
6713
  resolvePass.end();
4344
6714
  }
4345
6715
  function encodePresent(encoder) {
@@ -4360,98 +6730,213 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4360
6730
  passEncoder.draw(3);
4361
6731
  passEncoder.end();
4362
6732
  }
4363
- function dispatchFrame(frameIndex, parallelism) {
6733
+ function dispatchFrame(frameIndex, parallelism, renderedSamplesPerPixel = config.samplesPerPixel) {
4364
6734
  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}`
6735
+ const batch = createGpuSubmissionBatcher({
6736
+ device,
6737
+ frameIndex,
6738
+ maxFramePassesPerSubmission: config.maxFramePassesPerSubmission
4369
6739
  });
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
6740
  for (const tile of tiles) {
4389
- for (let sampleIndex = 0; sampleIndex < config.samplesPerPixel; sampleIndex += 1) {
6741
+ for (let sampleIndex = 0; sampleIndex < renderedSamplesPerPixel; sampleIndex += 1) {
4390
6742
  const configOffset = writeFrameConfig(tile, {
4391
6743
  sampleIndex,
4392
- sampleWeight: 1 / config.samplesPerPixel
6744
+ sampleWeight: 1 / renderedSamplesPerPixel
4393
6745
  });
4394
- encodeTileSample(reserveEncoder(), tile, configOffset, parallelism);
6746
+ encodeTileSample(
6747
+ batch.reserve(config.maxDepth + 1),
6748
+ tile,
6749
+ configOffset,
6750
+ parallelism
6751
+ );
4395
6752
  if (config.deferredPathResolve) {
4396
- encodeTileOutput(reserveEncoder(), tile, configOffset, parallelism);
6753
+ encodeTileOutput(batch.reserve(1), tile, configOffset, parallelism);
4397
6754
  }
4398
6755
  }
4399
6756
  if (!config.deferredPathResolve) {
4400
6757
  const outputConfigOffset = writeFrameConfig(tile, {
4401
6758
  sampleIndex: 0,
4402
- sampleWeight: 1 / config.samplesPerPixel
6759
+ sampleWeight: 1 / renderedSamplesPerPixel
4403
6760
  });
4404
- encodeTileOutput(reserveEncoder(), tile, outputConfigOffset, parallelism);
6761
+ encodeTileOutput(batch.reserve(1), tile, outputConfigOffset, parallelism);
4405
6762
  }
4406
6763
  }
4407
6764
  if (config.denoise) {
4408
6765
  const denoiseConfigOffset = writeFrameConfig(
4409
6766
  { x: 0, y: 0, width: config.width, height: config.height },
4410
- { sampleIndex: 0, sampleWeight: 1 / config.samplesPerPixel }
6767
+ { sampleIndex: 0, sampleWeight: 1 / renderedSamplesPerPixel }
6768
+ );
6769
+ const denoisePassCount = renderedSamplesPerPixel < 4 ? 2 : 1;
6770
+ encodeDenoise(
6771
+ batch.reserve(denoisePassCount),
6772
+ denoiseConfigOffset,
6773
+ parallelism,
6774
+ renderedSamplesPerPixel
4411
6775
  );
4412
- encodeDenoise(reserveEncoder(), denoiseConfigOffset, parallelism);
4413
6776
  }
4414
- encodePresent(reserveEncoder());
4415
- submitCurrentEncoder();
4416
- return submissionCount;
6777
+ encodePresent(batch.reserve(1));
6778
+ return batch.flush();
4417
6779
  }
4418
- function renderOnce() {
6780
+ function renderOnce(renderOptions = {}, resolvedSamplingPlan = null) {
6781
+ const frameStartTimeMs = nowMs();
4419
6782
  frame += 1;
4420
6783
  const frameIndex = frame + config.frameIndex;
6784
+ const samplingPlan = resolvedSamplingPlan ?? resolveRenderedSamplesPerPixel(renderOptions, false);
4421
6785
  const parallelismCounters = createGpuParallelismCounters();
4422
6786
  const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex, parallelismCounters);
4423
- const frameSubmissionCount = dispatchFrame(frameIndex, parallelismCounters);
4424
- lastGpuParallelism = createGpuParallelismDiagnostics(gpuAdapterParallelism, parallelismCounters);
6787
+ const frameSubmissionCount = dispatchFrame(
6788
+ frameIndex,
6789
+ parallelismCounters,
6790
+ samplingPlan.renderedSamplesPerPixel
6791
+ );
6792
+ const frameTimeMs = Math.max(0, nowMs() - frameStartTimeMs);
4425
6793
  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,
6794
+ ...createFrameStats({
6795
+ frameIndex,
6796
+ accelerationBuildSubmitted,
6797
+ frameSubmissionCount,
6798
+ parallelismCounters,
6799
+ renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel,
6800
+ targetSamplesPerPixel: samplingPlan.targetSamplesPerPixel,
6801
+ frameTimeBudgetMs: samplingPlan.frameTimeBudgetMs,
6802
+ budgetConstrained: samplingPlan.budgetConstrained
6803
+ }),
6804
+ gpuWorkerJobs: createGpuWorkerJobDiagnostics(
6805
+ lastGpuParallelism,
6806
+ frameSubmissionCount + (accelerationBuildSubmitted ? 1 : 0),
6807
+ frameTimeMs,
6808
+ false
6809
+ )
6810
+ });
6811
+ }
6812
+ async function waitForSubmittedGpuWork(options2 = {}) {
6813
+ if (typeof device.queue.onSubmittedWorkDone !== "function") {
6814
+ return true;
6815
+ }
6816
+ const timeoutMs = Math.max(
6817
+ 1,
6818
+ Number.isFinite(options2.timeoutMs) ? Number(options2.timeoutMs) : GPU_SUBMITTED_WORK_TIMEOUT_MS
6819
+ );
6820
+ const allowTimeout = options2.allowTimeout !== false;
6821
+ const completionPromise = device.queue.onSubmittedWorkDone().then(
6822
+ () => ({ status: "done" }),
6823
+ (error) => {
6824
+ throw error;
6825
+ }
6826
+ );
6827
+ const lossPromise = typeof device.lost?.then === "function" ? device.lost.then((info) => {
6828
+ throw new Error(
6829
+ `WebGPU device lost while waiting for submitted work (${info?.reason ?? "unknown"}).`
6830
+ );
6831
+ }) : null;
6832
+ let timeoutHandle = null;
6833
+ let resolveTimeoutPromise = null;
6834
+ let timeoutSettled = false;
6835
+ const settleTimeoutPromise = (value) => {
6836
+ if (timeoutSettled) {
6837
+ return;
6838
+ }
6839
+ timeoutSettled = true;
6840
+ resolveTimeoutPromise?.(value);
6841
+ };
6842
+ const timeoutPromise = new Promise((resolve) => {
6843
+ resolveTimeoutPromise = resolve;
6844
+ timeoutHandle = setTimeout(() => settleTimeoutPromise({ status: "timeout" }), timeoutMs);
6845
+ });
6846
+ let result;
6847
+ try {
6848
+ result = await Promise.race(
6849
+ [completionPromise, timeoutPromise, lossPromise].filter(Boolean)
6850
+ );
6851
+ } finally {
6852
+ if (timeoutHandle !== null) {
6853
+ clearTimeout(timeoutHandle);
6854
+ settleTimeoutPromise({ status: "cancelled" });
6855
+ }
6856
+ }
6857
+ if (result?.status === "timeout") {
6858
+ if (!allowTimeout) {
6859
+ throw new Error(`Timed out after ${timeoutMs} ms waiting for submitted GPU work.`);
6860
+ }
6861
+ console.warn(
6862
+ `[plasius.wavefront] Submitted GPU work did not report completion within ${timeoutMs} ms; continuing.`
6863
+ );
6864
+ return false;
6865
+ }
6866
+ return true;
6867
+ }
6868
+ function dispatchFrameAwaitingGpu(frameIndex, parallelism, renderedSamplesPerPixel = config.samplesPerPixel) {
6869
+ const samplePassesPerSample = config.maxDepth + 1 + (config.deferredPathResolve ? 1 : 0);
6870
+ const denoisePassCount = config.denoise ? renderedSamplesPerPixel < 4 ? 2 : 1 : 0;
6871
+ const tailPassCount = denoisePassCount + 1;
6872
+ const sampleBatchSize = Math.max(
6873
+ 1,
6874
+ Math.floor(
6875
+ Math.max(config.maxFramePassesPerSubmission - tailPassCount, 1) / Math.max(samplePassesPerSample, 1)
6876
+ )
6877
+ );
6878
+ let submissionCount = 0;
6879
+ for (const tile of tiles) {
6880
+ for (let sampleStart = 0; sampleStart < renderedSamplesPerPixel; sampleStart += sampleBatchSize) {
6881
+ const sampleEnd = Math.min(renderedSamplesPerPixel, sampleStart + sampleBatchSize);
6882
+ const batch = createGpuSubmissionBatcher({
6883
+ device,
6884
+ frameIndex,
6885
+ maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
6886
+ startingSubmissionCount: submissionCount
6887
+ });
6888
+ let slot = 0;
6889
+ for (let sampleIndex = sampleStart; sampleIndex < sampleEnd; sampleIndex += 1) {
6890
+ const configOffset = writeFrameConfigSlot(slot, tile, frameIndex, {
6891
+ sampleIndex,
6892
+ sampleWeight: 1 / renderedSamplesPerPixel
6893
+ });
6894
+ slot += 1;
6895
+ encodeTileSample(
6896
+ batch.reserve(config.maxDepth + 1),
6897
+ tile,
6898
+ configOffset,
6899
+ parallelism
6900
+ );
6901
+ if (config.deferredPathResolve) {
6902
+ encodeTileOutput(batch.reserve(1), tile, configOffset, parallelism);
6903
+ }
6904
+ }
6905
+ if (!config.deferredPathResolve && sampleEnd >= renderedSamplesPerPixel) {
6906
+ const outputConfigOffset = writeFrameConfigSlot(slot, tile, frameIndex, {
6907
+ sampleIndex: 0,
6908
+ sampleWeight: 1 / renderedSamplesPerPixel
6909
+ });
6910
+ encodeTileOutput(batch.reserve(1), tile, outputConfigOffset, parallelism);
6911
+ }
6912
+ batch.flush();
6913
+ submissionCount += batch.getSubmissionCount();
6914
+ }
6915
+ }
6916
+ const tail = createGpuSubmissionBatcher({
6917
+ device,
6918
+ frameIndex,
4433
6919
  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
6920
+ startingSubmissionCount: submissionCount
4454
6921
  });
6922
+ if (config.denoise) {
6923
+ const denoiseConfigOffset = writeFrameConfigSlot(
6924
+ 0,
6925
+ { x: 0, y: 0, width: config.width, height: config.height },
6926
+ frameIndex,
6927
+ { sampleIndex: 0, sampleWeight: 1 / renderedSamplesPerPixel }
6928
+ );
6929
+ encodeDenoise(
6930
+ tail.reserve(denoisePassCount),
6931
+ denoiseConfigOffset,
6932
+ parallelism,
6933
+ renderedSamplesPerPixel
6934
+ );
6935
+ }
6936
+ encodePresent(tail.reserve(1));
6937
+ tail.flush();
6938
+ submissionCount += tail.getSubmissionCount();
6939
+ return submissionCount;
4455
6940
  }
4456
6941
  async function readOutputProbe(optionsForProbe = {}) {
4457
6942
  const mapMode = constants.map;
@@ -4465,6 +6950,10 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4465
6950
  size: 256,
4466
6951
  usage: constants.buffer.COPY_DST | constants.buffer.MAP_READ
4467
6952
  });
6953
+ await waitForSubmittedGpuWork({
6954
+ timeoutMs: GPU_READBACK_COMPLETION_TIMEOUT_MS,
6955
+ allowTimeout: false
6956
+ });
4468
6957
  const encoder = device.createCommandEncoder({
4469
6958
  label: "plasius.wavefront.outputProbe.copy"
4470
6959
  });
@@ -4474,6 +6963,10 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4474
6963
  { width: 1, height: 1, depthOrArrayLayers: 1 }
4475
6964
  );
4476
6965
  device.queue.submit([encoder.finish()]);
6966
+ await waitForSubmittedGpuWork({
6967
+ timeoutMs: GPU_READBACK_COMPLETION_TIMEOUT_MS,
6968
+ allowTimeout: false
6969
+ });
4477
6970
  await readback.mapAsync(mapMode.READ);
4478
6971
  const bytes = new Uint8Array(readback.getMappedRange()).slice(0, 4);
4479
6972
  readback.unmap();
@@ -4486,7 +6979,57 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4486
6979
  });
4487
6980
  }
4488
6981
  async function renderFrame(renderOptions = {}) {
4489
- const frameStats = renderOnce();
6982
+ const awaitGPUCompletion = renderOptions.awaitGPUCompletion !== false;
6983
+ const samplingPlan = resolveRenderedSamplesPerPixel(renderOptions, awaitGPUCompletion);
6984
+ const useThrottledHighSamplePath = awaitGPUCompletion && samplingPlan.renderedSamplesPerPixel >= 8;
6985
+ const submittedWorkTimeoutMs = estimateSubmittedGpuWorkTimeoutMs(
6986
+ { ...config, renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel },
6987
+ tiles.length,
6988
+ renderOptions.submittedWorkTimeoutMs
6989
+ );
6990
+ const frameStartTimeMs = nowMs();
6991
+ const submissionWaitOptions = awaitGPUCompletion ? { timeoutMs: submittedWorkTimeoutMs, allowTimeout: false } : { timeoutMs: submittedWorkTimeoutMs };
6992
+ let frameStats;
6993
+ if (useThrottledHighSamplePath) {
6994
+ frame += 1;
6995
+ const frameIndex = frame + config.frameIndex;
6996
+ const parallelismCounters = createGpuParallelismCounters();
6997
+ const accelerationBuildSubmitted = dispatchGpuAccelerationBuild(frameIndex, parallelismCounters);
6998
+ const frameSubmissionCount = dispatchFrameAwaitingGpu(
6999
+ frameIndex,
7000
+ parallelismCounters,
7001
+ samplingPlan.renderedSamplesPerPixel
7002
+ );
7003
+ frameStats = createFrameStats({
7004
+ frameIndex,
7005
+ accelerationBuildSubmitted,
7006
+ frameSubmissionCount,
7007
+ parallelismCounters,
7008
+ renderedSamplesPerPixel: samplingPlan.renderedSamplesPerPixel,
7009
+ targetSamplesPerPixel: samplingPlan.targetSamplesPerPixel,
7010
+ frameTimeBudgetMs: samplingPlan.frameTimeBudgetMs,
7011
+ budgetConstrained: samplingPlan.budgetConstrained
7012
+ });
7013
+ } else {
7014
+ frameStats = renderOnce(renderOptions, samplingPlan);
7015
+ }
7016
+ if (awaitGPUCompletion) {
7017
+ await waitForSubmittedGpuWork(submissionWaitOptions);
7018
+ }
7019
+ const frameTimeMs = Math.max(0, nowMs() - frameStartTimeMs);
7020
+ if (awaitGPUCompletion) {
7021
+ lastCompletedFrameTimeMs = frameTimeMs;
7022
+ lastCompletedSamplesPerPixel = frameStats.renderedSamplesPerPixel ?? frameStats.samplesPerPixel;
7023
+ }
7024
+ frameStats = Object.freeze({
7025
+ ...frameStats,
7026
+ gpuWorkerJobs: createGpuWorkerJobDiagnostics(
7027
+ frameStats.gpuParallelism,
7028
+ frameStats.commandSubmissions,
7029
+ frameTimeMs,
7030
+ awaitGPUCompletion
7031
+ )
7032
+ });
4490
7033
  const probe = renderOptions.readOutputProbe === false ? null : await readOutputProbe(renderOptions.probe);
4491
7034
  const maxChannel = probe ? Math.max(...probe.rgba.slice(0, 3)) : 0;
4492
7035
  return Object.freeze({
@@ -4507,10 +7050,8 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4507
7050
  queueOverflow: 0
4508
7051
  });
4509
7052
  }
4510
- function updateSceneObjects(sceneObjects) {
4511
- const nextPackedScene = packWavefrontSceneObjects(sceneObjects, config.sceneObjectCapacity);
4512
- packedScene = nextPackedScene;
4513
- config = createWavefrontPathTracingComputeConfig({
7053
+ function rebuildLiveConfig(overrides = {}) {
7054
+ return createWavefrontPathTracingComputeConfig({
4514
7055
  ...options,
4515
7056
  canvas,
4516
7057
  width: config.width,
@@ -4521,26 +7062,35 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4521
7062
  sceneObjectCapacity: config.sceneObjectCapacity,
4522
7063
  sceneObjects: packedScene.objects,
4523
7064
  camera: activeCameraOptions,
4524
- frameIndex: config.frameIndex
7065
+ environmentMap: {
7066
+ ...config.environmentMap
7067
+ },
7068
+ frameIndex: config.frameIndex,
7069
+ ...overrides
4525
7070
  });
7071
+ }
7072
+ function rebuildMediumResources(nextConfig) {
7073
+ const previousMediumTextureResource = mediumTextureResource;
7074
+ mediumTextureResource = createMediumTextureResource(device, constants, nextConfig.mediums);
7075
+ bindGroups = createTraceBindGroups();
7076
+ if (previousMediumTextureResource?.ownsTexture) {
7077
+ previousMediumTextureResource.texture?.destroy?.();
7078
+ }
7079
+ }
7080
+ function updateSceneObjects(sceneObjects) {
7081
+ const nextPackedScene = packWavefrontSceneObjects(sceneObjects, config.sceneObjectCapacity);
7082
+ packedScene = nextPackedScene;
7083
+ const nextConfig = rebuildLiveConfig();
7084
+ if (!mediumTablesEqual(config.mediums, nextConfig.mediums)) {
7085
+ rebuildMediumResources(nextConfig);
7086
+ }
7087
+ config = nextConfig;
4526
7088
  device.queue.writeBuffer(sceneObjectBuffer, 0, packedScene.buffer);
4527
7089
  return config;
4528
7090
  }
4529
7091
  function updateCamera(cameraOptions = {}) {
4530
7092
  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
- });
7093
+ config = rebuildLiveConfig();
4544
7094
  return config;
4545
7095
  }
4546
7096
  function getSnapshot() {
@@ -4558,6 +7108,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4558
7108
  emissiveTriangleCount: config.emissiveTriangleCount,
4559
7109
  environmentPortalCount: config.environmentPortalCount,
4560
7110
  environmentPortalMode: config.environmentPortalMode,
7111
+ mediumCount: config.mediumCount,
4561
7112
  environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
4562
7113
  deferredPathResolve: config.deferredPathResolve,
4563
7114
  bvhNodeCount: config.bvhNodeCount,
@@ -4595,6 +7146,28 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
4595
7146
  if (environmentMapResource.ownsTexture) {
4596
7147
  environmentMapResource.texture?.destroy?.();
4597
7148
  }
7149
+ if (environmentSamplingResource.ownsTexture) {
7150
+ environmentSamplingResource.texture?.destroy?.();
7151
+ }
7152
+ if (mediumTextureResource.ownsTexture) {
7153
+ mediumTextureResource.texture?.destroy?.();
7154
+ }
7155
+ brdfLutResource.texture?.destroy?.();
7156
+ if (baseColorAtlasResource.ownsTexture) {
7157
+ baseColorAtlasResource.texture?.destroy?.();
7158
+ }
7159
+ if (metallicRoughnessAtlasResource.ownsTexture) {
7160
+ metallicRoughnessAtlasResource.texture?.destroy?.();
7161
+ }
7162
+ if (normalAtlasResource.ownsTexture) {
7163
+ normalAtlasResource.texture?.destroy?.();
7164
+ }
7165
+ if (occlusionAtlasResource.ownsTexture) {
7166
+ occlusionAtlasResource.texture?.destroy?.();
7167
+ }
7168
+ if (emissiveAtlasResource.ownsTexture) {
7169
+ emissiveAtlasResource.texture?.destroy?.();
7170
+ }
4598
7171
  context.unconfigure?.();
4599
7172
  }
4600
7173
  return Object.freeze({
@@ -4747,6 +7320,48 @@ var rendererAccelerationStructurePolicies = Object.freeze(
4747
7320
  })
4748
7321
  )
4749
7322
  );
7323
+ function clampWavefrontAdaptiveSamplesPerPixel(value) {
7324
+ if (!Number.isFinite(value)) {
7325
+ return 1;
7326
+ }
7327
+ return Math.max(1, Math.min(256, Math.round(value)));
7328
+ }
7329
+ function createWavefrontAdaptiveSamplingLevels(options = {}) {
7330
+ const requestedSamplesPerPixel = clampWavefrontAdaptiveSamplesPerPixel(
7331
+ options.samplesPerPixel ?? 1
7332
+ );
7333
+ const minimumSamplesPerPixel = Math.min(
7334
+ requestedSamplesPerPixel,
7335
+ clampWavefrontAdaptiveSamplesPerPixel(options.minimumSamplesPerPixel ?? 1)
7336
+ );
7337
+ const frameTimeBudgetMs = Number.isFinite(options.frameTimeBudgetMs) ? Math.max(0, Number(options.frameTimeBudgetMs)) : 0;
7338
+ const levels = /* @__PURE__ */ new Set([minimumSamplesPerPixel, requestedSamplesPerPixel]);
7339
+ let currentSamplesPerPixel = minimumSamplesPerPixel;
7340
+ while (currentSamplesPerPixel < requestedSamplesPerPixel) {
7341
+ levels.add(currentSamplesPerPixel);
7342
+ currentSamplesPerPixel *= 2;
7343
+ }
7344
+ levels.add(Math.min(currentSamplesPerPixel, requestedSamplesPerPixel));
7345
+ return Object.freeze({
7346
+ requestedSamplesPerPixel,
7347
+ minimumSamplesPerPixel,
7348
+ frameTimeBudgetMs,
7349
+ levels: Object.freeze(
7350
+ [...levels].sort((left, right) => left - right).map(
7351
+ (samplesPerPixel) => Object.freeze({
7352
+ id: `${samplesPerPixel}spp`,
7353
+ label: `${samplesPerPixel} spp`,
7354
+ estimatedCostMs: samplesPerPixel,
7355
+ config: Object.freeze({
7356
+ samplesPerPixel,
7357
+ frameTimeBudgetMs,
7358
+ minimumSamplesPerPixel
7359
+ })
7360
+ })
7361
+ )
7362
+ )
7363
+ });
7364
+ }
4750
7365
  function createWavefrontField(name, type, description) {
4751
7366
  return Object.freeze({
4752
7367
  name,
@@ -5924,9 +8539,11 @@ var defaultRendererClearColor = DEFAULT_CLEAR_COLOR;
5924
8539
  createGpuRenderer,
5925
8540
  createRayTracingRenderPlan,
5926
8541
  createRendererDebugHooks,
8542
+ createWavefrontAdaptiveSamplingLevels,
5927
8543
  createWavefrontBvhBuildLevels,
5928
8544
  createWavefrontBvhSortStages,
5929
8545
  createWavefrontEmissiveTriangleIndexSource,
8546
+ createWavefrontGpuMaterialSource,
5930
8547
  createWavefrontGpuMeshSource,
5931
8548
  createWavefrontMeshAcceleration,
5932
8549
  createWavefrontPathTracingComputeConfig,