@plasius/gpu-renderer 0.2.6 → 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
@@ -212,17 +212,19 @@ var DEFAULT_MAX_DEPTH = 6;
212
212
  var DEFAULT_TILE_SIZE = 128;
213
213
  var DEFAULT_SAMPLES_PER_PIXEL = 1;
214
214
  var MAX_SAMPLES_PER_PIXEL = 256;
215
- var DEFAULT_BRDF_LUT_SIZE = 256;
215
+ var DEFAULT_BRDF_LUT_SIZE = 128;
216
+ var DEFAULT_BRDF_LUT_SAMPLE_COUNT = 256;
216
217
  var DEFAULT_MAX_FRAME_PASSES_PER_SUBMISSION = 256;
217
218
  var DEFAULT_SCENE_OBJECT_CAPACITY = 128;
218
219
  var DEFAULT_ENVIRONMENT_PORTAL_CAPACITY = 32;
220
+ var DEFAULT_MEDIUM_PHASE_MODEL = 0;
219
221
  var WORKGROUP_SIZE = 64;
220
222
  var rendererWavefrontComputeMode = "webgpu-compute";
221
223
  var rendererWavefrontComputeWorkgroupSize = WORKGROUP_SIZE;
222
224
  var rendererWavefrontComputeStatsStride = 8;
223
225
  var RAY_RECORD_BYTES = 80;
224
226
  var HIT_RECORD_BYTES = 256;
225
- var SCENE_OBJECT_RECORD_BYTES = 144;
227
+ var SCENE_OBJECT_RECORD_BYTES = 160;
226
228
  var MESH_VERTEX_RECORD_BYTES = 48;
227
229
  var MESH_RANGE_RECORD_BYTES = 240;
228
230
  var TRIANGLE_RECORD_BYTES = 352;
@@ -231,6 +233,7 @@ var BVH_NODE_RECORD_BYTES = 48;
231
233
  var BVH_LEAF_REF_RECORD_BYTES = 16;
232
234
  var EMISSIVE_TRIANGLE_INDEX_BYTES = 4;
233
235
  var ENVIRONMENT_PORTAL_RECORD_BYTES = 96;
236
+ var MEDIUM_TABLE_ROWS = 2;
234
237
  var ACCUMULATION_RECORD_BYTES = 16;
235
238
  var PATH_VERTEX_RECORD_BYTES = 16;
236
239
  var GPU_SUBMITTED_WORK_TIMEOUT_MS = 5e3;
@@ -577,6 +580,156 @@ function deriveBounds(input) {
577
580
  }
578
581
  return null;
579
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
+ }
580
733
  function normalizeWavefrontSceneObject(input = {}, index = 0) {
581
734
  const bounds = deriveBounds(input);
582
735
  const kind = readObjectKind(input.kind ?? input.type ?? (bounds ? "box" : "sphere"));
@@ -607,12 +760,19 @@ function normalizeWavefrontSceneObject(input = {}, index = 0) {
607
760
  input.specularColor ?? input.material?.specularColor,
608
761
  [1, 1, 1, 1]
609
762
  ).map((value, componentIndex) => componentIndex < 3 ? clamp(value, 0, 1) : 1);
763
+ const medium = deriveWavefrontTransportMedium(input, index + 1);
610
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;
611
765
  return Object.freeze({
612
766
  id: readNonNegativeInteger("id", input.id, index + 1),
613
767
  kind,
614
768
  materialKind: resolvedMaterialKind,
615
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,
616
776
  center: Object.freeze(center),
617
777
  halfExtent: Object.freeze(halfExtent),
618
778
  color: Object.freeze(color),
@@ -636,6 +796,7 @@ function normalizeWavefrontSceneObject(input = {}, index = 0) {
636
796
  ),
637
797
  specular: clamp(readFiniteNumber("specular", input.specular ?? input.material?.specular, 1), 0, 1),
638
798
  specularColor: Object.freeze(specularColor),
799
+ thickness: normalizeWavefrontThickness(input, "thickness"),
639
800
  transmission
640
801
  });
641
802
  }
@@ -729,6 +890,7 @@ function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
729
890
  input.specularColor ?? input.material?.specularColor,
730
891
  [1, 1, 1, 1]
731
892
  ).map((value, componentIndex) => componentIndex < 3 ? clamp(value, 0, 1) : 1);
893
+ const medium = deriveWavefrontTransportMedium(input, meshIndex + 1);
732
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;
733
895
  return Object.freeze({
734
896
  id: readNonNegativeInteger("mesh id", input.id, meshIndex + 1),
@@ -745,9 +907,10 @@ function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
745
907
  ),
746
908
  mediumRefId: readNonNegativeInteger(
747
909
  "mesh mediumRefId",
748
- input.mediumRefId ?? input.medium?.id ?? input.mediumId,
910
+ input.mediumRefId ?? medium?.id ?? input.medium?.id ?? input.mediumId ?? input.material?.mediumId,
749
911
  0
750
912
  ),
913
+ medium,
751
914
  color: Object.freeze(color),
752
915
  emission: Object.freeze(emission),
753
916
  roughness: clamp(readFiniteNumber("roughness", input.roughness ?? input.material?.roughness, 0.72), 0, 1),
@@ -769,6 +932,7 @@ function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
769
932
  ),
770
933
  specular: clamp(readFiniteNumber("specular", input.specular ?? input.material?.specular, 1), 0, 1),
771
934
  specularColor: Object.freeze(specularColor),
935
+ thickness: normalizeWavefrontThickness(input, "mesh thickness"),
772
936
  transmission,
773
937
  baseColorTexture: input.baseColorTexture ?? input.material?.baseColorTexture ?? null,
774
938
  metallicRoughnessTexture: input.metallicRoughnessTexture ?? input.material?.metallicRoughnessTexture ?? null,
@@ -980,7 +1144,7 @@ function createMeshTriangleRecords(meshes) {
980
1144
  mesh.clearcoatRoughness,
981
1145
  mesh.specular,
982
1146
  mesh.transmission,
983
- 0
1147
+ mesh.thickness
984
1148
  ]),
985
1149
  specularColor: Object.freeze([
986
1150
  mesh.specularColor[0] ?? 1,
@@ -1274,7 +1438,7 @@ function createWavefrontGpuMaterialSource(meshes = []) {
1274
1438
  mesh.clearcoatRoughness,
1275
1439
  mesh.specular,
1276
1440
  mesh.transmission,
1277
- 0
1441
+ mesh.thickness
1278
1442
  ]);
1279
1443
  writeVec4(floatView, byteOffset + 80, [
1280
1444
  mesh.specularColor[0] ?? 1,
@@ -1442,7 +1606,7 @@ function createWavefrontGpuMeshSource(meshes = [], gpuMaterialSourceInput = null
1442
1606
  mesh.clearcoatRoughness,
1443
1607
  mesh.specular,
1444
1608
  mesh.transmission,
1445
- 0
1609
+ mesh.thickness
1446
1610
  ]);
1447
1611
  writeVec4(meshFloats, floatOffset * 4 + 128, [
1448
1612
  mesh.specularColor[0] ?? 1,
@@ -1543,12 +1707,16 @@ function normalizeSceneObjects(sceneObjects, useDefaultScene = true) {
1543
1707
  const source = Array.isArray(sceneObjects) && sceneObjects.length > 0 ? sceneObjects : useDefaultScene ? createDefaultWavefrontSceneObjects() : [];
1544
1708
  return source.map((object, index) => normalizeWavefrontSceneObject(object, index));
1545
1709
  }
1710
+ function normalizeWavefrontMeshes(meshes) {
1711
+ const source = Array.isArray(meshes) ? meshes : [];
1712
+ return source.map((mesh, index) => normalizeWavefrontMesh(mesh, index));
1713
+ }
1546
1714
  function normalizeMeshes(options = {}) {
1547
1715
  if (Array.isArray(options.meshes)) {
1548
- return options.meshes;
1716
+ return normalizeWavefrontMeshes(options.meshes);
1549
1717
  }
1550
1718
  if (options.mesh) {
1551
- return [options.mesh];
1719
+ return normalizeWavefrontMeshes([options.mesh]);
1552
1720
  }
1553
1721
  return [];
1554
1722
  }
@@ -1855,6 +2023,7 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1855
2023
  const sceneObjects = Object.freeze(
1856
2024
  normalizeSceneObjects(options.sceneObjects, meshes.length === 0)
1857
2025
  );
2026
+ const mediums = collectWavefrontMediums(options, meshes, sceneObjects);
1858
2027
  const sceneObjectCapacity = Math.max(
1859
2028
  sceneObjects.length,
1860
2029
  readPositiveInteger("sceneObjectCapacity", options.sceneObjectCapacity, DEFAULT_SCENE_OBJECT_CAPACITY)
@@ -1913,6 +2082,8 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1913
2082
  sceneObjects,
1914
2083
  sceneObjectCount: sceneObjects.length,
1915
2084
  sceneObjectCapacity,
2085
+ mediums,
2086
+ mediumCount: mediums.length,
1916
2087
  accelerationBuildMode,
1917
2088
  gpuAccelerationBuildRequired: accelerationBuildMode === "gpu" && triangleCount > 0,
1918
2089
  gpuMeshSource,
@@ -2014,29 +2185,30 @@ function packWavefrontSceneObjects(sceneObjects, capacity = sceneObjects.length)
2014
2185
  uintView[u32 + 1] = object.id;
2015
2186
  uintView[u32 + 2] = object.materialKind;
2016
2187
  uintView[u32 + 3] = object.flags;
2017
- writeVec4(floatView, byteOffset + 16, [...object.center, 0]);
2018
- writeVec4(floatView, byteOffset + 32, [...object.halfExtent, 0]);
2019
- writeVec4(floatView, byteOffset + 48, object.color);
2020
- writeVec4(floatView, byteOffset + 64, object.emission);
2021
- 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, [
2022
2194
  object.roughness,
2023
2195
  object.metallic,
2024
2196
  object.opacity,
2025
2197
  object.ior
2026
2198
  ]);
2027
- writeVec4(floatView, byteOffset + 96, [
2199
+ writeVec4(floatView, byteOffset + 112, [
2028
2200
  object.sheenColor[0] ?? 0,
2029
2201
  object.sheenColor[1] ?? 0,
2030
2202
  object.sheenColor[2] ?? 0,
2031
2203
  object.clearcoat
2032
2204
  ]);
2033
- writeVec4(floatView, byteOffset + 112, [
2205
+ writeVec4(floatView, byteOffset + 128, [
2034
2206
  object.clearcoatRoughness,
2035
2207
  object.specular,
2036
2208
  object.transmission,
2037
- 0
2209
+ object.thickness
2038
2210
  ]);
2039
- writeVec4(floatView, byteOffset + 128, [
2211
+ writeVec4(floatView, byteOffset + 144, [
2040
2212
  object.specularColor[0] ?? 1,
2041
2213
  object.specularColor[1] ?? 1,
2042
2214
  object.specularColor[2] ?? 1,
@@ -2572,7 +2744,7 @@ function integrateBrdfSample(nDotV, roughness, sampleCount) {
2572
2744
  }
2573
2745
  return [scaleTerm / sampleCount, biasTerm / sampleCount];
2574
2746
  }
2575
- function createBrdfLutUploadBytes(size = DEFAULT_BRDF_LUT_SIZE, sampleCount = 1024) {
2747
+ function createBrdfLutUploadBytes(size = DEFAULT_BRDF_LUT_SIZE, sampleCount = DEFAULT_BRDF_LUT_SAMPLE_COUNT) {
2576
2748
  const cacheKey = `${Math.max(1, Math.trunc(size))}:${Math.max(1, Math.trunc(sampleCount))}`;
2577
2749
  const cached = BRDF_LUT_UPLOAD_CACHE.get(cacheKey);
2578
2750
  if (cached) {
@@ -2944,6 +3116,80 @@ function createBrdfLutResource(device, constants, size = DEFAULT_BRDF_LUT_SIZE)
2944
3116
  height: upload.height
2945
3117
  });
2946
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
+ }
2947
3193
  function createAtlasTextureResource(device, constants, atlas, label) {
2948
3194
  const upload = createRgba8TextureUpload(atlas);
2949
3195
  const texture = device.createTexture({
@@ -3082,6 +3328,10 @@ struct SceneObject {
3082
3328
  objectId: u32,
3083
3329
  materialKind: u32,
3084
3330
  flags: u32,
3331
+ mediumRefId: u32,
3332
+ pad0: u32,
3333
+ pad1: u32,
3334
+ pad2: u32,
3085
3335
  center: vec4<f32>,
3086
3336
  halfExtent: vec4<f32>,
3087
3337
  color: vec4<f32>,
@@ -3142,9 +3392,9 @@ struct BvhLeafRef {
3142
3392
  struct ScatterResult {
3143
3393
  direction: vec4<f32>,
3144
3394
  pdf: f32,
3395
+ mediumRefId: u32,
3145
3396
  flags: u32,
3146
3397
  pad0: u32,
3147
- pad1: u32,
3148
3398
  };
3149
3399
 
3150
3400
  struct MeshVertex {
@@ -3290,6 +3540,7 @@ struct EnvironmentPortal {
3290
3540
  @group(0) @binding(29) var brdfLutTexture: texture_2d<f32>;
3291
3541
  @group(0) @binding(30) var brdfLutSampler: sampler;
3292
3542
  @group(0) @binding(31) var environmentSamplingTexture: texture_2d<f32>;
3543
+ @group(0) @binding(32) var mediumTableTexture: texture_2d<f32>;
3293
3544
 
3294
3545
  fn hash_u32(value: u32) -> u32 {
3295
3546
  var x = value;
@@ -3955,6 +4206,60 @@ fn gated_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f
3955
4206
  return environment_radiance(origin, direction);
3956
4207
  }
3957
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
+
3958
4263
  fn surface_path_response(hit: HitRecord) -> vec3<f32> {
3959
4264
  let color = clamp(hit.color.xyz, vec3<f32>(0.0), vec3<f32>(1.0));
3960
4265
  let opacity = clamp(hit.material.z, 0.0, 1.0);
@@ -4053,11 +4358,15 @@ fn terminal_surface_environment_source(ray: RayRecord, hit: HitRecord) -> vec3<f
4053
4358
  return clamp_sample_radiance(environmentFloor * materialFloor);
4054
4359
  }
4055
4360
 
4056
- 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> {
4057
4366
  let surfaceColor = max(hit.color.xyz, config.ambientColor.xyz);
4058
4367
  let occlusion = mix(0.75, 1.0, clamp(hit.occlusion, 0.0, 1.0));
4059
4368
  return clamp_sample_radiance(
4060
- ray.throughput.xyz *
4369
+ throughput *
4061
4370
  surfaceColor *
4062
4371
  terminal_surface_environment_source(ray, hit) *
4063
4372
  occlusion
@@ -4531,7 +4840,7 @@ fn intersect_sphere(ray: RayRecord, object: SceneObject) -> Candidate {
4531
4840
  0xffffffffu,
4532
4841
  object.objectId,
4533
4842
  object.objectId,
4534
- 0u
4843
+ object.mediumRefId
4535
4844
  );
4536
4845
  }
4537
4846
 
@@ -4583,7 +4892,7 @@ fn intersect_box(ray: RayRecord, object: SceneObject) -> Candidate {
4583
4892
  0xffffffffu,
4584
4893
  object.objectId,
4585
4894
  object.objectId,
4586
- 0u
4895
+ object.mediumRefId
4587
4896
  );
4588
4897
  }
4589
4898
 
@@ -4834,6 +5143,10 @@ fn intersectActiveQueue(@builtin(global_invocation_id) globalId: vec3<u32>) {
4834
5143
  let ray = activeQueue[index];
4835
5144
  var nearest = 1000000.0;
4836
5145
  var hitObject = SceneObject(
5146
+ 0u,
5147
+ 0u,
5148
+ 0u,
5149
+ 0u,
4837
5150
  0u,
4838
5151
  0u,
4839
5152
  0u,
@@ -5037,9 +5350,9 @@ fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult
5037
5350
  return ScatterResult(
5038
5351
  vec4<f32>(reflect(ray.direction.xyz, normal), 0.0),
5039
5352
  1.0,
5353
+ ray.mediumRefId,
5040
5354
  RAY_FLAG_DELTA_SAMPLE,
5041
5355
  0u,
5042
- 0u
5043
5356
  );
5044
5357
  }
5045
5358
 
@@ -5059,17 +5372,17 @@ fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult
5059
5372
  return ScatterResult(
5060
5373
  vec4<f32>(reflect(ray.direction.xyz, normal), 0.0),
5061
5374
  1.0,
5375
+ ray.mediumRefId,
5062
5376
  RAY_FLAG_DELTA_SAMPLE,
5063
5377
  0u,
5064
- 0u
5065
5378
  );
5066
5379
  }
5067
5380
  return ScatterResult(
5068
5381
  vec4<f32>(refract_direction(ray.direction.xyz, normal, etaRatio), 0.0),
5069
5382
  1.0,
5383
+ transmitted_medium_ref_id(ray, hit),
5070
5384
  RAY_FLAG_DELTA_SAMPLE,
5071
5385
  0u,
5072
- 0u
5073
5386
  );
5074
5387
  }
5075
5388
 
@@ -5084,9 +5397,9 @@ fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult
5084
5397
  return ScatterResult(
5085
5398
  vec4<f32>(guidedDirection, 0.0),
5086
5399
  guidedPdf,
5400
+ ray.mediumRefId,
5087
5401
  RAY_FLAG_GUIDED_EMISSIVE,
5088
5402
  0u,
5089
- 0u
5090
5403
  );
5091
5404
  }
5092
5405
  }
@@ -5094,7 +5407,7 @@ fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult
5094
5407
  let guidedDirection = sample_environment_portal_direction(hit, seed + 131u, normal);
5095
5408
  if (dot(normal, guidedDirection) > 0.000001) {
5096
5409
  let guidedPdf = max(evaluate_surface_bsdf_pdf(hit, viewDirection, guidedDirection), 0.000001);
5097
- return ScatterResult(vec4<f32>(guidedDirection, 0.0), guidedPdf, 0u, 0u, 0u);
5410
+ return ScatterResult(vec4<f32>(guidedDirection, 0.0), guidedPdf, ray.mediumRefId, 0u, 0u);
5098
5411
  }
5099
5412
  }
5100
5413
 
@@ -5128,7 +5441,7 @@ fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult
5128
5441
  );
5129
5442
  }
5130
5443
  let pdf = max(evaluate_surface_bsdf_pdf(hit, viewDirection, lightDirection), 0.000001);
5131
- return ScatterResult(vec4<f32>(lightDirection, 0.0), pdf, 0u, 0u, 0u);
5444
+ return ScatterResult(vec4<f32>(lightDirection, 0.0), pdf, ray.mediumRefId, 0u, 0u);
5132
5445
  }
5133
5446
 
5134
5447
  @compute @workgroup_size(64)
@@ -5141,15 +5454,17 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5141
5454
 
5142
5455
  let ray = activeQueue[index];
5143
5456
  let hit = hits[index];
5457
+ let segmentTransmittance = medium_transmittance(ray.mediumRefId, hit.distance);
5458
+ let arrivingThroughput = ray.throughput.xyz * segmentTransmittance;
5144
5459
  var contribution = vec3<f32>(0.0);
5145
5460
 
5146
5461
  if (hit.hitType == 1u) {
5147
5462
  let guidedLightWeight = select(1.0, 0.24, (ray.flags & RAY_FLAG_GUIDED_EMISSIVE) != 0u);
5148
5463
  let sourceRadiance = max(hit.emission.xyz, hit.color.xyz) * guidedLightWeight;
5149
5464
  if (deferred_path_resolve_enabled()) {
5150
- record_deferred_terminal_source(ray, sourceRadiance);
5465
+ record_deferred_terminal_source(ray, sourceRadiance * segmentTransmittance);
5151
5466
  } else {
5152
- contribution = clamp_sample_radiance(ray.throughput.xyz * sourceRadiance);
5467
+ contribution = clamp_sample_radiance(arrivingThroughput * sourceRadiance);
5153
5468
  accumulation[ray.rayId] =
5154
5469
  accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
5155
5470
  }
@@ -5166,9 +5481,9 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5166
5481
  sourceRadiance = sourceRadiance * misWeight;
5167
5482
  }
5168
5483
  if (deferred_path_resolve_enabled()) {
5169
- record_deferred_terminal_source(ray, sourceRadiance);
5484
+ record_deferred_terminal_source(ray, sourceRadiance * segmentTransmittance);
5170
5485
  } else {
5171
- contribution = clamp_sample_radiance(ray.throughput.xyz * sourceRadiance);
5486
+ contribution = clamp_sample_radiance(arrivingThroughput * sourceRadiance);
5172
5487
  accumulation[ray.rayId] =
5173
5488
  accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
5174
5489
  }
@@ -5176,7 +5491,11 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5176
5491
  return;
5177
5492
  }
5178
5493
 
5179
- let response = stabilize_surface_path_response(ray, hit, surface_path_response(hit));
5494
+ let response = stabilize_surface_path_response(
5495
+ ray,
5496
+ hit,
5497
+ surface_path_response(hit) * segmentTransmittance
5498
+ );
5180
5499
  record_deferred_path_response(ray, response);
5181
5500
 
5182
5501
  let shouldEstimateDirectEnvironment =
@@ -5184,7 +5503,22 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5184
5503
  hit.material.z >= 0.95 &&
5185
5504
  ray.bounce < 2u;
5186
5505
  if (shouldEstimateDirectEnvironment) {
5187
- 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
+ );
5188
5522
  accumulation[ray.rayId] =
5189
5523
  accumulation[ray.rayId] + vec4<f32>(directEnvironment * sample_weight(), 0.0);
5190
5524
  }
@@ -5193,7 +5527,11 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5193
5527
  if (deferred_path_resolve_enabled()) {
5194
5528
  record_deferred_terminal_source(ray, terminal_surface_environment_source(ray, hit));
5195
5529
  } else {
5196
- let terminalEnvironment = terminal_surface_environment_contribution(ray, hit);
5530
+ let terminalEnvironment = terminal_surface_environment_contribution(
5531
+ ray,
5532
+ arrivingThroughput,
5533
+ hit
5534
+ );
5197
5535
  accumulation[ray.rayId] =
5198
5536
  accumulation[ray.rayId] + vec4<f32>(terminalEnvironment * sample_weight(), 1.0);
5199
5537
  }
@@ -5208,7 +5546,11 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5208
5546
  if (deferred_path_resolve_enabled()) {
5209
5547
  record_deferred_terminal_source(ray, terminal_surface_environment_source(ray, hit));
5210
5548
  } else {
5211
- let overflowEnvironment = terminal_surface_environment_contribution(ray, hit);
5549
+ let overflowEnvironment = terminal_surface_environment_contribution(
5550
+ ray,
5551
+ arrivingThroughput,
5552
+ hit
5553
+ );
5212
5554
  accumulation[ray.rayId] =
5213
5555
  accumulation[ray.rayId] + vec4<f32>(overflowEnvironment * sample_weight(), 1.0);
5214
5556
  }
@@ -5222,7 +5564,7 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5222
5564
  ray.sourcePixelId,
5223
5565
  ray.sampleId,
5224
5566
  ray.bounce + 1u,
5225
- ray.mediumRefId,
5567
+ scatter.mediumRefId,
5226
5568
  scatter.flags,
5227
5569
  0u,
5228
5570
  vec4<f32>(offset_origin(hit.position.xyz, hit.shadingNormal.xyz), 1.0),
@@ -5711,6 +6053,11 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
5711
6053
  config.environmentMap,
5712
6054
  config.environmentColor
5713
6055
  );
6056
+ let mediumTextureResource = createMediumTextureResource(
6057
+ device,
6058
+ constants,
6059
+ config.mediums
6060
+ );
5714
6061
  config = Object.freeze({
5715
6062
  ...config,
5716
6063
  environmentMap: Object.freeze({
@@ -5797,7 +6144,8 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
5797
6144
  { binding: 28, visibility: constants.shader.COMPUTE, sampler: { type: "filtering" } },
5798
6145
  { binding: 29, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
5799
6146
  { binding: 30, visibility: constants.shader.COMPUTE, sampler: { type: "filtering" } },
5800
- { binding: 31, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } }
6147
+ { binding: 31, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
6148
+ { binding: 32, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } }
5801
6149
  ]
5802
6150
  });
5803
6151
  const accelerationBindGroupLayout = device.createBindGroupLayout({
@@ -5984,14 +6332,18 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
5984
6332
  { binding: 28, resource: materialAtlasSampler },
5985
6333
  { binding: 29, resource: brdfLutResource.view },
5986
6334
  { binding: 30, resource: brdfLutResource.sampler },
5987
- { binding: 31, resource: environmentSamplingResource.view }
6335
+ { binding: 31, resource: environmentSamplingResource.view },
6336
+ { binding: 32, resource: mediumTextureResource.view }
5988
6337
  ]
5989
6338
  });
5990
6339
  }
5991
- const bindGroups = [
5992
- createTraceBindGroup(activeQueue, nextQueue, "plasius.wavefront.bind.activeNext"),
5993
- createTraceBindGroup(nextQueue, activeQueue, "plasius.wavefront.bind.nextActive")
5994
- ];
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();
5995
6347
  const bvhBuildBindGroup = device.createBindGroup({
5996
6348
  label: "plasius.wavefront.bind.bvhBuild",
5997
6349
  layout: accelerationBindGroupLayout,
@@ -6169,6 +6521,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
6169
6521
  emissiveTriangleCount: config.emissiveTriangleCount,
6170
6522
  environmentPortalCount: config.environmentPortalCount,
6171
6523
  environmentPortalMode: config.environmentPortalMode,
6524
+ mediumCount: config.mediumCount,
6172
6525
  environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
6173
6526
  deferredPathResolve: config.deferredPathResolve,
6174
6527
  bvhNodeCount: config.bvhNodeCount,
@@ -6716,10 +7069,22 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
6716
7069
  ...overrides
6717
7070
  });
6718
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
+ }
6719
7080
  function updateSceneObjects(sceneObjects) {
6720
7081
  const nextPackedScene = packWavefrontSceneObjects(sceneObjects, config.sceneObjectCapacity);
6721
7082
  packedScene = nextPackedScene;
6722
- config = rebuildLiveConfig();
7083
+ const nextConfig = rebuildLiveConfig();
7084
+ if (!mediumTablesEqual(config.mediums, nextConfig.mediums)) {
7085
+ rebuildMediumResources(nextConfig);
7086
+ }
7087
+ config = nextConfig;
6723
7088
  device.queue.writeBuffer(sceneObjectBuffer, 0, packedScene.buffer);
6724
7089
  return config;
6725
7090
  }
@@ -6743,6 +7108,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
6743
7108
  emissiveTriangleCount: config.emissiveTriangleCount,
6744
7109
  environmentPortalCount: config.environmentPortalCount,
6745
7110
  environmentPortalMode: config.environmentPortalMode,
7111
+ mediumCount: config.mediumCount,
6746
7112
  environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
6747
7113
  deferredPathResolve: config.deferredPathResolve,
6748
7114
  bvhNodeCount: config.bvhNodeCount,
@@ -6783,6 +7149,9 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
6783
7149
  if (environmentSamplingResource.ownsTexture) {
6784
7150
  environmentSamplingResource.texture?.destroy?.();
6785
7151
  }
7152
+ if (mediumTextureResource.ownsTexture) {
7153
+ mediumTextureResource.texture?.destroy?.();
7154
+ }
6786
7155
  brdfLutResource.texture?.destroy?.();
6787
7156
  if (baseColorAtlasResource.ownsTexture) {
6788
7157
  baseColorAtlasResource.texture?.destroy?.();