@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.js CHANGED
@@ -138,17 +138,19 @@ var DEFAULT_MAX_DEPTH = 6;
138
138
  var DEFAULT_TILE_SIZE = 128;
139
139
  var DEFAULT_SAMPLES_PER_PIXEL = 1;
140
140
  var MAX_SAMPLES_PER_PIXEL = 256;
141
- var DEFAULT_BRDF_LUT_SIZE = 256;
141
+ var DEFAULT_BRDF_LUT_SIZE = 128;
142
+ var DEFAULT_BRDF_LUT_SAMPLE_COUNT = 256;
142
143
  var DEFAULT_MAX_FRAME_PASSES_PER_SUBMISSION = 256;
143
144
  var DEFAULT_SCENE_OBJECT_CAPACITY = 128;
144
145
  var DEFAULT_ENVIRONMENT_PORTAL_CAPACITY = 32;
146
+ var DEFAULT_MEDIUM_PHASE_MODEL = 0;
145
147
  var WORKGROUP_SIZE = 64;
146
148
  var rendererWavefrontComputeMode = "webgpu-compute";
147
149
  var rendererWavefrontComputeWorkgroupSize = WORKGROUP_SIZE;
148
150
  var rendererWavefrontComputeStatsStride = 8;
149
151
  var RAY_RECORD_BYTES = 80;
150
152
  var HIT_RECORD_BYTES = 256;
151
- var SCENE_OBJECT_RECORD_BYTES = 144;
153
+ var SCENE_OBJECT_RECORD_BYTES = 160;
152
154
  var MESH_VERTEX_RECORD_BYTES = 48;
153
155
  var MESH_RANGE_RECORD_BYTES = 240;
154
156
  var TRIANGLE_RECORD_BYTES = 352;
@@ -157,6 +159,7 @@ var BVH_NODE_RECORD_BYTES = 48;
157
159
  var BVH_LEAF_REF_RECORD_BYTES = 16;
158
160
  var EMISSIVE_TRIANGLE_INDEX_BYTES = 4;
159
161
  var ENVIRONMENT_PORTAL_RECORD_BYTES = 96;
162
+ var MEDIUM_TABLE_ROWS = 2;
160
163
  var ACCUMULATION_RECORD_BYTES = 16;
161
164
  var PATH_VERTEX_RECORD_BYTES = 16;
162
165
  var GPU_SUBMITTED_WORK_TIMEOUT_MS = 5e3;
@@ -503,6 +506,156 @@ function deriveBounds(input) {
503
506
  }
504
507
  return null;
505
508
  }
509
+ function deriveBeerLambertAbsorptionFromAttenuationColor(attenuationColor, attenuationDistance, density = 1) {
510
+ const distance = Number(attenuationDistance);
511
+ const densityScale = Math.max(0, Number(density) || 0);
512
+ if (!Number.isFinite(distance) || distance <= 0 || densityScale <= 0) {
513
+ return [0, 0, 0];
514
+ }
515
+ return attenuationColor.slice(0, 3).map((channel) => {
516
+ const clamped = clamp(Number(channel) || 0, 1e-4, 1);
517
+ return Math.max(0, -Math.log(clamped) / distance * densityScale);
518
+ });
519
+ }
520
+ function readMediumPhaseModel(value) {
521
+ if (typeof value === "number" && Number.isFinite(value)) {
522
+ return Math.max(0, Math.trunc(value));
523
+ }
524
+ switch (String(value ?? "").trim().toLowerCase()) {
525
+ case "isotropic":
526
+ default:
527
+ return DEFAULT_MEDIUM_PHASE_MODEL;
528
+ }
529
+ }
530
+ function resolveWavefrontVolumeInput(input) {
531
+ return input?.volume ?? input?.material?.volume ?? null;
532
+ }
533
+ function normalizeWavefrontThickness(input, label) {
534
+ const volume = resolveWavefrontVolumeInput(input);
535
+ return Math.max(
536
+ 0,
537
+ readFiniteNumber(
538
+ label,
539
+ input?.thickness ?? volume?.thickness ?? input?.material?.thickness,
540
+ 0
541
+ )
542
+ );
543
+ }
544
+ function resolveWavefrontMediumId(input, fallbackId = 1) {
545
+ return input?.mediumRefId ?? input?.mediumId ?? input?.material?.mediumId ?? input?.materialRefId ?? input?.material?.id ?? input?.materialId ?? input?.id ?? fallbackId;
546
+ }
547
+ function deriveWavefrontTransportMedium(input, fallbackId = 1) {
548
+ const resolvedId = resolveWavefrontMediumId(input, fallbackId);
549
+ if (input?.medium) {
550
+ return normalizeWavefrontMedium(
551
+ {
552
+ ...input.medium,
553
+ id: input.medium.id ?? input.medium.mediumId ?? resolvedId
554
+ },
555
+ fallbackId
556
+ );
557
+ }
558
+ const volume = resolveWavefrontVolumeInput(input);
559
+ if (!volume) {
560
+ return null;
561
+ }
562
+ return normalizeWavefrontMedium(
563
+ {
564
+ id: resolvedId,
565
+ phaseModel: volume.phaseModel,
566
+ density: volume.density,
567
+ attenuationColor: volume.attenuationColor,
568
+ attenuationDistance: volume.attenuationDistance,
569
+ absorption: volume.absorption,
570
+ scattering: volume.scattering
571
+ },
572
+ fallbackId
573
+ );
574
+ }
575
+ function normalizeWavefrontMedium(input = {}, index = 0) {
576
+ const id = readNonNegativeInteger("medium id", input.id ?? input.mediumId, index);
577
+ const density = Math.max(0, readFiniteNumber("medium density", input.density, 1));
578
+ const attenuationColor = asColor(
579
+ input.attenuationColor ?? input.color ?? input.medium?.attenuationColor,
580
+ [1, 1, 1, 1]
581
+ );
582
+ const attenuationDistance = readFiniteNumber(
583
+ "medium attenuationDistance",
584
+ input.attenuationDistance ?? input.distance ?? input.medium?.attenuationDistance,
585
+ 0
586
+ );
587
+ const absorption = Array.isArray(input.absorption) || Array.isArray(input.medium?.absorption) ? asVec3(input.absorption ?? input.medium?.absorption, [0, 0, 0]).map(
588
+ (value) => Math.max(0, Number(value) || 0)
589
+ ) : deriveBeerLambertAbsorptionFromAttenuationColor(
590
+ attenuationColor,
591
+ attenuationDistance,
592
+ density
593
+ );
594
+ const scattering = asVec3(
595
+ input.scattering ?? input.medium?.scattering,
596
+ [0, 0, 0]
597
+ ).map((value) => Math.max(0, Number(value) || 0));
598
+ return Object.freeze({
599
+ id,
600
+ phaseModel: readMediumPhaseModel(input.phaseModel ?? input.medium?.phaseModel),
601
+ density,
602
+ attenuationColor: Object.freeze(attenuationColor),
603
+ attenuationDistance,
604
+ absorption: Object.freeze(absorption),
605
+ scattering: Object.freeze(scattering)
606
+ });
607
+ }
608
+ function collectWavefrontMediums(options, meshes, sceneObjects = []) {
609
+ const mediumsById = /* @__PURE__ */ new Map();
610
+ mediumsById.set(
611
+ 0,
612
+ Object.freeze({
613
+ id: 0,
614
+ phaseModel: DEFAULT_MEDIUM_PHASE_MODEL,
615
+ density: 0,
616
+ attenuationColor: Object.freeze([1, 1, 1, 1]),
617
+ attenuationDistance: 0,
618
+ absorption: Object.freeze([0, 0, 0]),
619
+ scattering: Object.freeze([0, 0, 0])
620
+ })
621
+ );
622
+ const register = (input, fallbackId = mediumsById.size) => {
623
+ if (!input) {
624
+ return;
625
+ }
626
+ const normalized = normalizeWavefrontMedium(
627
+ typeof input === "object" ? { id: fallbackId, ...input } : { id: fallbackId },
628
+ fallbackId
629
+ );
630
+ const existing = mediumsById.get(normalized.id);
631
+ if (existing && JSON.stringify(existing) !== JSON.stringify(normalized)) {
632
+ throw new Error(`Medium id ${normalized.id} is defined more than once with different values.`);
633
+ }
634
+ mediumsById.set(normalized.id, normalized);
635
+ };
636
+ for (const medium of options.mediums ?? []) {
637
+ register(medium);
638
+ }
639
+ for (const mesh of meshes) {
640
+ register(mesh.medium, mesh.mediumRefId ?? mesh.medium?.id ?? 0);
641
+ }
642
+ for (const mesh of meshes) {
643
+ if ((mesh.mediumRefId ?? 0) > 0 && !mediumsById.has(mesh.mediumRefId)) {
644
+ register({ id: mesh.mediumRefId });
645
+ }
646
+ }
647
+ for (const object of sceneObjects) {
648
+ register(object.medium, object.mediumRefId ?? object.medium?.id ?? 0);
649
+ }
650
+ for (const object of sceneObjects) {
651
+ if ((object.mediumRefId ?? 0) > 0 && !mediumsById.has(object.mediumRefId)) {
652
+ register({ id: object.mediumRefId });
653
+ }
654
+ }
655
+ return Object.freeze(
656
+ Array.from(mediumsById.values()).sort((left, right) => left.id - right.id)
657
+ );
658
+ }
506
659
  function normalizeWavefrontSceneObject(input = {}, index = 0) {
507
660
  const bounds = deriveBounds(input);
508
661
  const kind = readObjectKind(input.kind ?? input.type ?? (bounds ? "box" : "sphere"));
@@ -533,12 +686,19 @@ function normalizeWavefrontSceneObject(input = {}, index = 0) {
533
686
  input.specularColor ?? input.material?.specularColor,
534
687
  [1, 1, 1, 1]
535
688
  ).map((value, componentIndex) => componentIndex < 3 ? clamp(value, 0, 1) : 1);
689
+ const medium = deriveWavefrontTransportMedium(input, index + 1);
536
690
  const resolvedMaterialKind = emission[0] > 0 || emission[1] > 0 || emission[2] > 0 ? MATERIAL_EMISSIVE : materialKindInput === void 0 || materialKindInput === null ? transmission > 1e-3 || opacity < 0.999 ? MATERIAL_TRANSPARENT : materialKind : materialKind;
537
691
  return Object.freeze({
538
692
  id: readNonNegativeInteger("id", input.id, index + 1),
539
693
  kind,
540
694
  materialKind: resolvedMaterialKind,
541
695
  flags: readNonNegativeInteger("flags", input.flags, 0),
696
+ mediumRefId: readNonNegativeInteger(
697
+ "mediumRefId",
698
+ input.mediumRefId ?? medium?.id ?? input.medium?.id ?? input.mediumId,
699
+ 0
700
+ ),
701
+ medium,
542
702
  center: Object.freeze(center),
543
703
  halfExtent: Object.freeze(halfExtent),
544
704
  color: Object.freeze(color),
@@ -562,6 +722,7 @@ function normalizeWavefrontSceneObject(input = {}, index = 0) {
562
722
  ),
563
723
  specular: clamp(readFiniteNumber("specular", input.specular ?? input.material?.specular, 1), 0, 1),
564
724
  specularColor: Object.freeze(specularColor),
725
+ thickness: normalizeWavefrontThickness(input, "thickness"),
565
726
  transmission
566
727
  });
567
728
  }
@@ -655,6 +816,7 @@ function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
655
816
  input.specularColor ?? input.material?.specularColor,
656
817
  [1, 1, 1, 1]
657
818
  ).map((value, componentIndex) => componentIndex < 3 ? clamp(value, 0, 1) : 1);
819
+ const medium = deriveWavefrontTransportMedium(input, meshIndex + 1);
658
820
  const resolvedMaterialKind = emission[0] > 0 || emission[1] > 0 || emission[2] > 0 ? MATERIAL_EMISSIVE : materialKindInput === void 0 || materialKindInput === null ? transmission > 1e-3 || opacity < 0.999 ? MATERIAL_TRANSPARENT : materialKind : materialKind;
659
821
  return Object.freeze({
660
822
  id: readNonNegativeInteger("mesh id", input.id, meshIndex + 1),
@@ -671,9 +833,10 @@ function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
671
833
  ),
672
834
  mediumRefId: readNonNegativeInteger(
673
835
  "mesh mediumRefId",
674
- input.mediumRefId ?? input.medium?.id ?? input.mediumId,
836
+ input.mediumRefId ?? medium?.id ?? input.medium?.id ?? input.mediumId ?? input.material?.mediumId,
675
837
  0
676
838
  ),
839
+ medium,
677
840
  color: Object.freeze(color),
678
841
  emission: Object.freeze(emission),
679
842
  roughness: clamp(readFiniteNumber("roughness", input.roughness ?? input.material?.roughness, 0.72), 0, 1),
@@ -695,6 +858,7 @@ function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
695
858
  ),
696
859
  specular: clamp(readFiniteNumber("specular", input.specular ?? input.material?.specular, 1), 0, 1),
697
860
  specularColor: Object.freeze(specularColor),
861
+ thickness: normalizeWavefrontThickness(input, "mesh thickness"),
698
862
  transmission,
699
863
  baseColorTexture: input.baseColorTexture ?? input.material?.baseColorTexture ?? null,
700
864
  metallicRoughnessTexture: input.metallicRoughnessTexture ?? input.material?.metallicRoughnessTexture ?? null,
@@ -906,7 +1070,7 @@ function createMeshTriangleRecords(meshes) {
906
1070
  mesh.clearcoatRoughness,
907
1071
  mesh.specular,
908
1072
  mesh.transmission,
909
- 0
1073
+ mesh.thickness
910
1074
  ]),
911
1075
  specularColor: Object.freeze([
912
1076
  mesh.specularColor[0] ?? 1,
@@ -1200,7 +1364,7 @@ function createWavefrontGpuMaterialSource(meshes = []) {
1200
1364
  mesh.clearcoatRoughness,
1201
1365
  mesh.specular,
1202
1366
  mesh.transmission,
1203
- 0
1367
+ mesh.thickness
1204
1368
  ]);
1205
1369
  writeVec4(floatView, byteOffset + 80, [
1206
1370
  mesh.specularColor[0] ?? 1,
@@ -1368,7 +1532,7 @@ function createWavefrontGpuMeshSource(meshes = [], gpuMaterialSourceInput = null
1368
1532
  mesh.clearcoatRoughness,
1369
1533
  mesh.specular,
1370
1534
  mesh.transmission,
1371
- 0
1535
+ mesh.thickness
1372
1536
  ]);
1373
1537
  writeVec4(meshFloats, floatOffset * 4 + 128, [
1374
1538
  mesh.specularColor[0] ?? 1,
@@ -1469,12 +1633,16 @@ function normalizeSceneObjects(sceneObjects, useDefaultScene = true) {
1469
1633
  const source = Array.isArray(sceneObjects) && sceneObjects.length > 0 ? sceneObjects : useDefaultScene ? createDefaultWavefrontSceneObjects() : [];
1470
1634
  return source.map((object, index) => normalizeWavefrontSceneObject(object, index));
1471
1635
  }
1636
+ function normalizeWavefrontMeshes(meshes) {
1637
+ const source = Array.isArray(meshes) ? meshes : [];
1638
+ return source.map((mesh, index) => normalizeWavefrontMesh(mesh, index));
1639
+ }
1472
1640
  function normalizeMeshes(options = {}) {
1473
1641
  if (Array.isArray(options.meshes)) {
1474
- return options.meshes;
1642
+ return normalizeWavefrontMeshes(options.meshes);
1475
1643
  }
1476
1644
  if (options.mesh) {
1477
- return [options.mesh];
1645
+ return normalizeWavefrontMeshes([options.mesh]);
1478
1646
  }
1479
1647
  return [];
1480
1648
  }
@@ -1781,6 +1949,7 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1781
1949
  const sceneObjects = Object.freeze(
1782
1950
  normalizeSceneObjects(options.sceneObjects, meshes.length === 0)
1783
1951
  );
1952
+ const mediums = collectWavefrontMediums(options, meshes, sceneObjects);
1784
1953
  const sceneObjectCapacity = Math.max(
1785
1954
  sceneObjects.length,
1786
1955
  readPositiveInteger("sceneObjectCapacity", options.sceneObjectCapacity, DEFAULT_SCENE_OBJECT_CAPACITY)
@@ -1839,6 +2008,8 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
1839
2008
  sceneObjects,
1840
2009
  sceneObjectCount: sceneObjects.length,
1841
2010
  sceneObjectCapacity,
2011
+ mediums,
2012
+ mediumCount: mediums.length,
1842
2013
  accelerationBuildMode,
1843
2014
  gpuAccelerationBuildRequired: accelerationBuildMode === "gpu" && triangleCount > 0,
1844
2015
  gpuMeshSource,
@@ -1940,29 +2111,30 @@ function packWavefrontSceneObjects(sceneObjects, capacity = sceneObjects.length)
1940
2111
  uintView[u32 + 1] = object.id;
1941
2112
  uintView[u32 + 2] = object.materialKind;
1942
2113
  uintView[u32 + 3] = object.flags;
1943
- writeVec4(floatView, byteOffset + 16, [...object.center, 0]);
1944
- writeVec4(floatView, byteOffset + 32, [...object.halfExtent, 0]);
1945
- writeVec4(floatView, byteOffset + 48, object.color);
1946
- writeVec4(floatView, byteOffset + 64, object.emission);
1947
- writeVec4(floatView, byteOffset + 80, [
2114
+ uintView[u32 + 4] = object.mediumRefId;
2115
+ writeVec4(floatView, byteOffset + 32, [...object.center, 0]);
2116
+ writeVec4(floatView, byteOffset + 48, [...object.halfExtent, 0]);
2117
+ writeVec4(floatView, byteOffset + 64, object.color);
2118
+ writeVec4(floatView, byteOffset + 80, object.emission);
2119
+ writeVec4(floatView, byteOffset + 96, [
1948
2120
  object.roughness,
1949
2121
  object.metallic,
1950
2122
  object.opacity,
1951
2123
  object.ior
1952
2124
  ]);
1953
- writeVec4(floatView, byteOffset + 96, [
2125
+ writeVec4(floatView, byteOffset + 112, [
1954
2126
  object.sheenColor[0] ?? 0,
1955
2127
  object.sheenColor[1] ?? 0,
1956
2128
  object.sheenColor[2] ?? 0,
1957
2129
  object.clearcoat
1958
2130
  ]);
1959
- writeVec4(floatView, byteOffset + 112, [
2131
+ writeVec4(floatView, byteOffset + 128, [
1960
2132
  object.clearcoatRoughness,
1961
2133
  object.specular,
1962
2134
  object.transmission,
1963
- 0
2135
+ object.thickness
1964
2136
  ]);
1965
- writeVec4(floatView, byteOffset + 128, [
2137
+ writeVec4(floatView, byteOffset + 144, [
1966
2138
  object.specularColor[0] ?? 1,
1967
2139
  object.specularColor[1] ?? 1,
1968
2140
  object.specularColor[2] ?? 1,
@@ -2498,7 +2670,7 @@ function integrateBrdfSample(nDotV, roughness, sampleCount) {
2498
2670
  }
2499
2671
  return [scaleTerm / sampleCount, biasTerm / sampleCount];
2500
2672
  }
2501
- function createBrdfLutUploadBytes(size = DEFAULT_BRDF_LUT_SIZE, sampleCount = 1024) {
2673
+ function createBrdfLutUploadBytes(size = DEFAULT_BRDF_LUT_SIZE, sampleCount = DEFAULT_BRDF_LUT_SAMPLE_COUNT) {
2502
2674
  const cacheKey = `${Math.max(1, Math.trunc(size))}:${Math.max(1, Math.trunc(sampleCount))}`;
2503
2675
  const cached = BRDF_LUT_UPLOAD_CACHE.get(cacheKey);
2504
2676
  if (cached) {
@@ -2870,6 +3042,80 @@ function createBrdfLutResource(device, constants, size = DEFAULT_BRDF_LUT_SIZE)
2870
3042
  height: upload.height
2871
3043
  });
2872
3044
  }
3045
+ function createMediumTextureResource(device, constants, mediums) {
3046
+ const normalized = Array.isArray(mediums) && mediums.length > 0 ? mediums : [{ id: 0 }];
3047
+ const width = Math.max(
3048
+ 1,
3049
+ normalized.reduce((maximum, medium) => Math.max(maximum, medium.id ?? 0), 0) + 1
3050
+ );
3051
+ const level = {
3052
+ width,
3053
+ height: MEDIUM_TABLE_ROWS,
3054
+ data: new Float32Array(width * MEDIUM_TABLE_ROWS * 4)
3055
+ };
3056
+ for (const medium of normalized) {
3057
+ const mediumId = Math.max(0, Math.trunc(Number(medium.id) || 0));
3058
+ const absorptionOffset = mediumId * 4;
3059
+ level.data[absorptionOffset] = Math.max(0, medium.absorption?.[0] ?? 0);
3060
+ level.data[absorptionOffset + 1] = Math.max(0, medium.absorption?.[1] ?? 0);
3061
+ level.data[absorptionOffset + 2] = Math.max(0, medium.absorption?.[2] ?? 0);
3062
+ level.data[absorptionOffset + 3] = Math.max(0, medium.phaseModel ?? 0);
3063
+ const scatteringOffset = (width + mediumId) * 4;
3064
+ level.data[scatteringOffset] = Math.max(0, medium.scattering?.[0] ?? 0);
3065
+ level.data[scatteringOffset + 1] = Math.max(0, medium.scattering?.[1] ?? 0);
3066
+ level.data[scatteringOffset + 2] = Math.max(0, medium.scattering?.[2] ?? 0);
3067
+ level.data[scatteringOffset + 3] = Math.max(0, medium.density ?? 0);
3068
+ }
3069
+ const upload = createFloat16RgbaUploadFromLevels([level])[0];
3070
+ const texture = device.createTexture({
3071
+ label: "plasius.wavefront.mediumTable",
3072
+ size: { width, height: MEDIUM_TABLE_ROWS },
3073
+ format: "rgba16float",
3074
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST
3075
+ });
3076
+ device.queue.writeTexture(
3077
+ { texture },
3078
+ upload.bytes,
3079
+ { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
3080
+ { width, height: MEDIUM_TABLE_ROWS, depthOrArrayLayers: 1 }
3081
+ );
3082
+ return Object.freeze({
3083
+ texture,
3084
+ view: texture.createView(),
3085
+ ownsTexture: true,
3086
+ count: normalized.length,
3087
+ width
3088
+ });
3089
+ }
3090
+ function mediumTablesEqual(left, right) {
3091
+ const leftMediums = Array.isArray(left) ? left : [];
3092
+ const rightMediums = Array.isArray(right) ? right : [];
3093
+ if (leftMediums.length !== rightMediums.length) {
3094
+ return false;
3095
+ }
3096
+ for (let index = 0; index < leftMediums.length; index += 1) {
3097
+ const leftMedium = leftMediums[index];
3098
+ const rightMedium = rightMediums[index];
3099
+ if ((leftMedium?.id ?? 0) !== (rightMedium?.id ?? 0)) {
3100
+ return false;
3101
+ }
3102
+ if ((leftMedium?.phaseModel ?? 0) !== (rightMedium?.phaseModel ?? 0)) {
3103
+ return false;
3104
+ }
3105
+ if ((leftMedium?.density ?? 0) !== (rightMedium?.density ?? 0)) {
3106
+ return false;
3107
+ }
3108
+ for (let component = 0; component < 3; component += 1) {
3109
+ if ((leftMedium?.absorption?.[component] ?? 0) !== (rightMedium?.absorption?.[component] ?? 0)) {
3110
+ return false;
3111
+ }
3112
+ if ((leftMedium?.scattering?.[component] ?? 0) !== (rightMedium?.scattering?.[component] ?? 0)) {
3113
+ return false;
3114
+ }
3115
+ }
3116
+ }
3117
+ return true;
3118
+ }
2873
3119
  function createAtlasTextureResource(device, constants, atlas, label) {
2874
3120
  const upload = createRgba8TextureUpload(atlas);
2875
3121
  const texture = device.createTexture({
@@ -3008,6 +3254,10 @@ struct SceneObject {
3008
3254
  objectId: u32,
3009
3255
  materialKind: u32,
3010
3256
  flags: u32,
3257
+ mediumRefId: u32,
3258
+ pad0: u32,
3259
+ pad1: u32,
3260
+ pad2: u32,
3011
3261
  center: vec4<f32>,
3012
3262
  halfExtent: vec4<f32>,
3013
3263
  color: vec4<f32>,
@@ -3068,9 +3318,9 @@ struct BvhLeafRef {
3068
3318
  struct ScatterResult {
3069
3319
  direction: vec4<f32>,
3070
3320
  pdf: f32,
3321
+ mediumRefId: u32,
3071
3322
  flags: u32,
3072
3323
  pad0: u32,
3073
- pad1: u32,
3074
3324
  };
3075
3325
 
3076
3326
  struct MeshVertex {
@@ -3216,6 +3466,7 @@ struct EnvironmentPortal {
3216
3466
  @group(0) @binding(29) var brdfLutTexture: texture_2d<f32>;
3217
3467
  @group(0) @binding(30) var brdfLutSampler: sampler;
3218
3468
  @group(0) @binding(31) var environmentSamplingTexture: texture_2d<f32>;
3469
+ @group(0) @binding(32) var mediumTableTexture: texture_2d<f32>;
3219
3470
 
3220
3471
  fn hash_u32(value: u32) -> u32 {
3221
3472
  var x = value;
@@ -3881,6 +4132,60 @@ fn gated_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f
3881
4132
  return environment_radiance(origin, direction);
3882
4133
  }
3883
4134
 
4135
+ fn medium_dimensions() -> vec2<u32> {
4136
+ return textureDimensions(mediumTableTexture);
4137
+ }
4138
+
4139
+ fn medium_valid(mediumRefId: u32) -> bool {
4140
+ let dimensions = medium_dimensions();
4141
+ return mediumRefId > 0u && mediumRefId < dimensions.x;
4142
+ }
4143
+
4144
+ fn medium_absorption(mediumRefId: u32) -> vec3<f32> {
4145
+ if (!medium_valid(mediumRefId)) {
4146
+ return vec3<f32>(0.0);
4147
+ }
4148
+ return max(
4149
+ textureLoad(mediumTableTexture, vec2<i32>(i32(mediumRefId), 0), 0).xyz,
4150
+ vec3<f32>(0.0)
4151
+ );
4152
+ }
4153
+
4154
+ fn medium_scattering(mediumRefId: u32) -> vec3<f32> {
4155
+ if (!medium_valid(mediumRefId)) {
4156
+ return vec3<f32>(0.0);
4157
+ }
4158
+ return max(
4159
+ textureLoad(mediumTableTexture, vec2<i32>(i32(mediumRefId), 1), 0).xyz,
4160
+ vec3<f32>(0.0)
4161
+ );
4162
+ }
4163
+
4164
+ fn medium_transmittance(mediumRefId: u32, distance: f32) -> vec3<f32> {
4165
+ if (!medium_valid(mediumRefId) || distance <= 0.000001) {
4166
+ return vec3<f32>(1.0);
4167
+ }
4168
+ let extinction = medium_absorption(mediumRefId) + medium_scattering(mediumRefId);
4169
+ return vec3<f32>(
4170
+ exp(-extinction.x * distance),
4171
+ exp(-extinction.y * distance),
4172
+ exp(-extinction.z * distance)
4173
+ );
4174
+ }
4175
+
4176
+ fn transmitted_medium_ref_id(ray: RayRecord, hit: HitRecord) -> u32 {
4177
+ if (hit.mediumRefId == 0u) {
4178
+ return ray.mediumRefId;
4179
+ }
4180
+ if (hit.frontFace == 1u) {
4181
+ return hit.mediumRefId;
4182
+ }
4183
+ if (ray.mediumRefId == hit.mediumRefId) {
4184
+ return 0u;
4185
+ }
4186
+ return ray.mediumRefId;
4187
+ }
4188
+
3884
4189
  fn surface_path_response(hit: HitRecord) -> vec3<f32> {
3885
4190
  let color = clamp(hit.color.xyz, vec3<f32>(0.0), vec3<f32>(1.0));
3886
4191
  let opacity = clamp(hit.material.z, 0.0, 1.0);
@@ -3979,11 +4284,15 @@ fn terminal_surface_environment_source(ray: RayRecord, hit: HitRecord) -> vec3<f
3979
4284
  return clamp_sample_radiance(environmentFloor * materialFloor);
3980
4285
  }
3981
4286
 
3982
- fn terminal_surface_environment_contribution(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
4287
+ fn terminal_surface_environment_contribution(
4288
+ ray: RayRecord,
4289
+ throughput: vec3<f32>,
4290
+ hit: HitRecord
4291
+ ) -> vec3<f32> {
3983
4292
  let surfaceColor = max(hit.color.xyz, config.ambientColor.xyz);
3984
4293
  let occlusion = mix(0.75, 1.0, clamp(hit.occlusion, 0.0, 1.0));
3985
4294
  return clamp_sample_radiance(
3986
- ray.throughput.xyz *
4295
+ throughput *
3987
4296
  surfaceColor *
3988
4297
  terminal_surface_environment_source(ray, hit) *
3989
4298
  occlusion
@@ -4457,7 +4766,7 @@ fn intersect_sphere(ray: RayRecord, object: SceneObject) -> Candidate {
4457
4766
  0xffffffffu,
4458
4767
  object.objectId,
4459
4768
  object.objectId,
4460
- 0u
4769
+ object.mediumRefId
4461
4770
  );
4462
4771
  }
4463
4772
 
@@ -4509,7 +4818,7 @@ fn intersect_box(ray: RayRecord, object: SceneObject) -> Candidate {
4509
4818
  0xffffffffu,
4510
4819
  object.objectId,
4511
4820
  object.objectId,
4512
- 0u
4821
+ object.mediumRefId
4513
4822
  );
4514
4823
  }
4515
4824
 
@@ -4760,6 +5069,10 @@ fn intersectActiveQueue(@builtin(global_invocation_id) globalId: vec3<u32>) {
4760
5069
  let ray = activeQueue[index];
4761
5070
  var nearest = 1000000.0;
4762
5071
  var hitObject = SceneObject(
5072
+ 0u,
5073
+ 0u,
5074
+ 0u,
5075
+ 0u,
4763
5076
  0u,
4764
5077
  0u,
4765
5078
  0u,
@@ -4963,9 +5276,9 @@ fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult
4963
5276
  return ScatterResult(
4964
5277
  vec4<f32>(reflect(ray.direction.xyz, normal), 0.0),
4965
5278
  1.0,
5279
+ ray.mediumRefId,
4966
5280
  RAY_FLAG_DELTA_SAMPLE,
4967
5281
  0u,
4968
- 0u
4969
5282
  );
4970
5283
  }
4971
5284
 
@@ -4985,17 +5298,17 @@ fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult
4985
5298
  return ScatterResult(
4986
5299
  vec4<f32>(reflect(ray.direction.xyz, normal), 0.0),
4987
5300
  1.0,
5301
+ ray.mediumRefId,
4988
5302
  RAY_FLAG_DELTA_SAMPLE,
4989
5303
  0u,
4990
- 0u
4991
5304
  );
4992
5305
  }
4993
5306
  return ScatterResult(
4994
5307
  vec4<f32>(refract_direction(ray.direction.xyz, normal, etaRatio), 0.0),
4995
5308
  1.0,
5309
+ transmitted_medium_ref_id(ray, hit),
4996
5310
  RAY_FLAG_DELTA_SAMPLE,
4997
5311
  0u,
4998
- 0u
4999
5312
  );
5000
5313
  }
5001
5314
 
@@ -5010,9 +5323,9 @@ fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult
5010
5323
  return ScatterResult(
5011
5324
  vec4<f32>(guidedDirection, 0.0),
5012
5325
  guidedPdf,
5326
+ ray.mediumRefId,
5013
5327
  RAY_FLAG_GUIDED_EMISSIVE,
5014
5328
  0u,
5015
- 0u
5016
5329
  );
5017
5330
  }
5018
5331
  }
@@ -5020,7 +5333,7 @@ fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult
5020
5333
  let guidedDirection = sample_environment_portal_direction(hit, seed + 131u, normal);
5021
5334
  if (dot(normal, guidedDirection) > 0.000001) {
5022
5335
  let guidedPdf = max(evaluate_surface_bsdf_pdf(hit, viewDirection, guidedDirection), 0.000001);
5023
- return ScatterResult(vec4<f32>(guidedDirection, 0.0), guidedPdf, 0u, 0u, 0u);
5336
+ return ScatterResult(vec4<f32>(guidedDirection, 0.0), guidedPdf, ray.mediumRefId, 0u, 0u);
5024
5337
  }
5025
5338
  }
5026
5339
 
@@ -5054,7 +5367,7 @@ fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult
5054
5367
  );
5055
5368
  }
5056
5369
  let pdf = max(evaluate_surface_bsdf_pdf(hit, viewDirection, lightDirection), 0.000001);
5057
- return ScatterResult(vec4<f32>(lightDirection, 0.0), pdf, 0u, 0u, 0u);
5370
+ return ScatterResult(vec4<f32>(lightDirection, 0.0), pdf, ray.mediumRefId, 0u, 0u);
5058
5371
  }
5059
5372
 
5060
5373
  @compute @workgroup_size(64)
@@ -5067,15 +5380,17 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5067
5380
 
5068
5381
  let ray = activeQueue[index];
5069
5382
  let hit = hits[index];
5383
+ let segmentTransmittance = medium_transmittance(ray.mediumRefId, hit.distance);
5384
+ let arrivingThroughput = ray.throughput.xyz * segmentTransmittance;
5070
5385
  var contribution = vec3<f32>(0.0);
5071
5386
 
5072
5387
  if (hit.hitType == 1u) {
5073
5388
  let guidedLightWeight = select(1.0, 0.24, (ray.flags & RAY_FLAG_GUIDED_EMISSIVE) != 0u);
5074
5389
  let sourceRadiance = max(hit.emission.xyz, hit.color.xyz) * guidedLightWeight;
5075
5390
  if (deferred_path_resolve_enabled()) {
5076
- record_deferred_terminal_source(ray, sourceRadiance);
5391
+ record_deferred_terminal_source(ray, sourceRadiance * segmentTransmittance);
5077
5392
  } else {
5078
- contribution = clamp_sample_radiance(ray.throughput.xyz * sourceRadiance);
5393
+ contribution = clamp_sample_radiance(arrivingThroughput * sourceRadiance);
5079
5394
  accumulation[ray.rayId] =
5080
5395
  accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
5081
5396
  }
@@ -5092,9 +5407,9 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5092
5407
  sourceRadiance = sourceRadiance * misWeight;
5093
5408
  }
5094
5409
  if (deferred_path_resolve_enabled()) {
5095
- record_deferred_terminal_source(ray, sourceRadiance);
5410
+ record_deferred_terminal_source(ray, sourceRadiance * segmentTransmittance);
5096
5411
  } else {
5097
- contribution = clamp_sample_radiance(ray.throughput.xyz * sourceRadiance);
5412
+ contribution = clamp_sample_radiance(arrivingThroughput * sourceRadiance);
5098
5413
  accumulation[ray.rayId] =
5099
5414
  accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
5100
5415
  }
@@ -5102,7 +5417,11 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5102
5417
  return;
5103
5418
  }
5104
5419
 
5105
- let response = stabilize_surface_path_response(ray, hit, surface_path_response(hit));
5420
+ let response = stabilize_surface_path_response(
5421
+ ray,
5422
+ hit,
5423
+ surface_path_response(hit) * segmentTransmittance
5424
+ );
5106
5425
  record_deferred_path_response(ray, response);
5107
5426
 
5108
5427
  let shouldEstimateDirectEnvironment =
@@ -5110,7 +5429,22 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5110
5429
  hit.material.z >= 0.95 &&
5111
5430
  ray.bounce < 2u;
5112
5431
  if (shouldEstimateDirectEnvironment) {
5113
- let directEnvironment = surface_direct_environment_contribution(ray, hit);
5432
+ let directEnvironment = surface_direct_environment_contribution(
5433
+ RayRecord(
5434
+ ray.rayId,
5435
+ ray.parentRayId,
5436
+ ray.sourcePixelId,
5437
+ ray.sampleId,
5438
+ ray.bounce,
5439
+ ray.mediumRefId,
5440
+ ray.flags,
5441
+ 0u,
5442
+ ray.origin,
5443
+ ray.direction,
5444
+ vec4<f32>(arrivingThroughput, ray.throughput.w)
5445
+ ),
5446
+ hit
5447
+ );
5114
5448
  accumulation[ray.rayId] =
5115
5449
  accumulation[ray.rayId] + vec4<f32>(directEnvironment * sample_weight(), 0.0);
5116
5450
  }
@@ -5119,7 +5453,11 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5119
5453
  if (deferred_path_resolve_enabled()) {
5120
5454
  record_deferred_terminal_source(ray, terminal_surface_environment_source(ray, hit));
5121
5455
  } else {
5122
- let terminalEnvironment = terminal_surface_environment_contribution(ray, hit);
5456
+ let terminalEnvironment = terminal_surface_environment_contribution(
5457
+ ray,
5458
+ arrivingThroughput,
5459
+ hit
5460
+ );
5123
5461
  accumulation[ray.rayId] =
5124
5462
  accumulation[ray.rayId] + vec4<f32>(terminalEnvironment * sample_weight(), 1.0);
5125
5463
  }
@@ -5134,7 +5472,11 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5134
5472
  if (deferred_path_resolve_enabled()) {
5135
5473
  record_deferred_terminal_source(ray, terminal_surface_environment_source(ray, hit));
5136
5474
  } else {
5137
- let overflowEnvironment = terminal_surface_environment_contribution(ray, hit);
5475
+ let overflowEnvironment = terminal_surface_environment_contribution(
5476
+ ray,
5477
+ arrivingThroughput,
5478
+ hit
5479
+ );
5138
5480
  accumulation[ray.rayId] =
5139
5481
  accumulation[ray.rayId] + vec4<f32>(overflowEnvironment * sample_weight(), 1.0);
5140
5482
  }
@@ -5148,7 +5490,7 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5148
5490
  ray.sourcePixelId,
5149
5491
  ray.sampleId,
5150
5492
  ray.bounce + 1u,
5151
- ray.mediumRefId,
5493
+ scatter.mediumRefId,
5152
5494
  scatter.flags,
5153
5495
  0u,
5154
5496
  vec4<f32>(offset_origin(hit.position.xyz, hit.shadingNormal.xyz), 1.0),
@@ -5637,6 +5979,11 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
5637
5979
  config.environmentMap,
5638
5980
  config.environmentColor
5639
5981
  );
5982
+ let mediumTextureResource = createMediumTextureResource(
5983
+ device,
5984
+ constants,
5985
+ config.mediums
5986
+ );
5640
5987
  config = Object.freeze({
5641
5988
  ...config,
5642
5989
  environmentMap: Object.freeze({
@@ -5723,7 +6070,8 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
5723
6070
  { binding: 28, visibility: constants.shader.COMPUTE, sampler: { type: "filtering" } },
5724
6071
  { binding: 29, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
5725
6072
  { binding: 30, visibility: constants.shader.COMPUTE, sampler: { type: "filtering" } },
5726
- { binding: 31, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } }
6073
+ { binding: 31, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
6074
+ { binding: 32, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } }
5727
6075
  ]
5728
6076
  });
5729
6077
  const accelerationBindGroupLayout = device.createBindGroupLayout({
@@ -5910,14 +6258,18 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
5910
6258
  { binding: 28, resource: materialAtlasSampler },
5911
6259
  { binding: 29, resource: brdfLutResource.view },
5912
6260
  { binding: 30, resource: brdfLutResource.sampler },
5913
- { binding: 31, resource: environmentSamplingResource.view }
6261
+ { binding: 31, resource: environmentSamplingResource.view },
6262
+ { binding: 32, resource: mediumTextureResource.view }
5914
6263
  ]
5915
6264
  });
5916
6265
  }
5917
- const bindGroups = [
5918
- createTraceBindGroup(activeQueue, nextQueue, "plasius.wavefront.bind.activeNext"),
5919
- createTraceBindGroup(nextQueue, activeQueue, "plasius.wavefront.bind.nextActive")
5920
- ];
6266
+ function createTraceBindGroups() {
6267
+ return [
6268
+ createTraceBindGroup(activeQueue, nextQueue, "plasius.wavefront.bind.activeNext"),
6269
+ createTraceBindGroup(nextQueue, activeQueue, "plasius.wavefront.bind.nextActive")
6270
+ ];
6271
+ }
6272
+ let bindGroups = createTraceBindGroups();
5921
6273
  const bvhBuildBindGroup = device.createBindGroup({
5922
6274
  label: "plasius.wavefront.bind.bvhBuild",
5923
6275
  layout: accelerationBindGroupLayout,
@@ -6095,6 +6447,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
6095
6447
  emissiveTriangleCount: config.emissiveTriangleCount,
6096
6448
  environmentPortalCount: config.environmentPortalCount,
6097
6449
  environmentPortalMode: config.environmentPortalMode,
6450
+ mediumCount: config.mediumCount,
6098
6451
  environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
6099
6452
  deferredPathResolve: config.deferredPathResolve,
6100
6453
  bvhNodeCount: config.bvhNodeCount,
@@ -6642,10 +6995,22 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
6642
6995
  ...overrides
6643
6996
  });
6644
6997
  }
6998
+ function rebuildMediumResources(nextConfig) {
6999
+ const previousMediumTextureResource = mediumTextureResource;
7000
+ mediumTextureResource = createMediumTextureResource(device, constants, nextConfig.mediums);
7001
+ bindGroups = createTraceBindGroups();
7002
+ if (previousMediumTextureResource?.ownsTexture) {
7003
+ previousMediumTextureResource.texture?.destroy?.();
7004
+ }
7005
+ }
6645
7006
  function updateSceneObjects(sceneObjects) {
6646
7007
  const nextPackedScene = packWavefrontSceneObjects(sceneObjects, config.sceneObjectCapacity);
6647
7008
  packedScene = nextPackedScene;
6648
- config = rebuildLiveConfig();
7009
+ const nextConfig = rebuildLiveConfig();
7010
+ if (!mediumTablesEqual(config.mediums, nextConfig.mediums)) {
7011
+ rebuildMediumResources(nextConfig);
7012
+ }
7013
+ config = nextConfig;
6649
7014
  device.queue.writeBuffer(sceneObjectBuffer, 0, packedScene.buffer);
6650
7015
  return config;
6651
7016
  }
@@ -6669,6 +7034,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
6669
7034
  emissiveTriangleCount: config.emissiveTriangleCount,
6670
7035
  environmentPortalCount: config.environmentPortalCount,
6671
7036
  environmentPortalMode: config.environmentPortalMode,
7037
+ mediumCount: config.mediumCount,
6672
7038
  environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
6673
7039
  deferredPathResolve: config.deferredPathResolve,
6674
7040
  bvhNodeCount: config.bvhNodeCount,
@@ -6709,6 +7075,9 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
6709
7075
  if (environmentSamplingResource.ownsTexture) {
6710
7076
  environmentSamplingResource.texture?.destroy?.();
6711
7077
  }
7078
+ if (mediumTextureResource.ownsTexture) {
7079
+ mediumTextureResource.texture?.destroy?.();
7080
+ }
6712
7081
  brdfLutResource.texture?.destroy?.();
6713
7082
  if (baseColorAtlasResource.ownsTexture) {
6714
7083
  baseColorAtlasResource.texture?.destroy?.();