@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.
@@ -13,17 +13,19 @@ const DEFAULT_MAX_DEPTH = 6;
13
13
  const DEFAULT_TILE_SIZE = 128;
14
14
  const DEFAULT_SAMPLES_PER_PIXEL = 1;
15
15
  const MAX_SAMPLES_PER_PIXEL = 256;
16
- const DEFAULT_BRDF_LUT_SIZE = 256;
16
+ const DEFAULT_BRDF_LUT_SIZE = 128;
17
+ const DEFAULT_BRDF_LUT_SAMPLE_COUNT = 256;
17
18
  const DEFAULT_MAX_FRAME_PASSES_PER_SUBMISSION = 256;
18
19
  const DEFAULT_SCENE_OBJECT_CAPACITY = 128;
19
20
  const DEFAULT_ENVIRONMENT_PORTAL_CAPACITY = 32;
21
+ const DEFAULT_MEDIUM_PHASE_MODEL = 0;
20
22
  const WORKGROUP_SIZE = 64;
21
23
  export const rendererWavefrontComputeMode = "webgpu-compute";
22
24
  export const rendererWavefrontComputeWorkgroupSize = WORKGROUP_SIZE;
23
25
  export const rendererWavefrontComputeStatsStride = 8;
24
26
  const RAY_RECORD_BYTES = 80;
25
27
  const HIT_RECORD_BYTES = 256;
26
- const SCENE_OBJECT_RECORD_BYTES = 144;
28
+ const SCENE_OBJECT_RECORD_BYTES = 160;
27
29
  const MESH_VERTEX_RECORD_BYTES = 48;
28
30
  const MESH_RANGE_RECORD_BYTES = 240;
29
31
  const TRIANGLE_RECORD_BYTES = 352;
@@ -32,6 +34,7 @@ const BVH_NODE_RECORD_BYTES = 48;
32
34
  const BVH_LEAF_REF_RECORD_BYTES = 16;
33
35
  const EMISSIVE_TRIANGLE_INDEX_BYTES = 4;
34
36
  const ENVIRONMENT_PORTAL_RECORD_BYTES = 96;
37
+ const MEDIUM_TABLE_ROWS = 2;
35
38
  const ACCUMULATION_RECORD_BYTES = 16;
36
39
  const PATH_VERTEX_RECORD_BYTES = 16;
37
40
  const GPU_SUBMITTED_WORK_TIMEOUT_MS = 5_000;
@@ -437,6 +440,183 @@ function deriveBounds(input) {
437
440
  return null;
438
441
  }
439
442
 
443
+ function deriveBeerLambertAbsorptionFromAttenuationColor(
444
+ attenuationColor,
445
+ attenuationDistance,
446
+ density = 1
447
+ ) {
448
+ const distance = Number(attenuationDistance);
449
+ const densityScale = Math.max(0, Number(density) || 0);
450
+ if (!Number.isFinite(distance) || distance <= 0 || densityScale <= 0) {
451
+ return [0, 0, 0];
452
+ }
453
+ return attenuationColor.slice(0, 3).map((channel) => {
454
+ const clamped = clamp(Number(channel) || 0, 0.0001, 1);
455
+ return Math.max(0, (-Math.log(clamped) / distance) * densityScale);
456
+ });
457
+ }
458
+
459
+ function readMediumPhaseModel(value) {
460
+ if (typeof value === "number" && Number.isFinite(value)) {
461
+ return Math.max(0, Math.trunc(value));
462
+ }
463
+ switch (String(value ?? "").trim().toLowerCase()) {
464
+ case "isotropic":
465
+ default:
466
+ return DEFAULT_MEDIUM_PHASE_MODEL;
467
+ }
468
+ }
469
+
470
+ function resolveWavefrontVolumeInput(input) {
471
+ return input?.volume ?? input?.material?.volume ?? null;
472
+ }
473
+
474
+ function normalizeWavefrontThickness(input, label) {
475
+ const volume = resolveWavefrontVolumeInput(input);
476
+ return Math.max(
477
+ 0,
478
+ readFiniteNumber(
479
+ label,
480
+ input?.thickness ?? volume?.thickness ?? input?.material?.thickness,
481
+ 0
482
+ )
483
+ );
484
+ }
485
+
486
+ function resolveWavefrontMediumId(input, fallbackId = 1) {
487
+ return (
488
+ input?.mediumRefId ??
489
+ input?.mediumId ??
490
+ input?.material?.mediumId ??
491
+ input?.materialRefId ??
492
+ input?.material?.id ??
493
+ input?.materialId ??
494
+ input?.id ??
495
+ fallbackId
496
+ );
497
+ }
498
+
499
+ function deriveWavefrontTransportMedium(input, fallbackId = 1) {
500
+ const resolvedId = resolveWavefrontMediumId(input, fallbackId);
501
+ if (input?.medium) {
502
+ return normalizeWavefrontMedium(
503
+ {
504
+ ...input.medium,
505
+ id: input.medium.id ?? input.medium.mediumId ?? resolvedId,
506
+ },
507
+ fallbackId
508
+ );
509
+ }
510
+ const volume = resolveWavefrontVolumeInput(input);
511
+ if (!volume) {
512
+ return null;
513
+ }
514
+ return normalizeWavefrontMedium(
515
+ {
516
+ id: resolvedId,
517
+ phaseModel: volume.phaseModel,
518
+ density: volume.density,
519
+ attenuationColor: volume.attenuationColor,
520
+ attenuationDistance: volume.attenuationDistance,
521
+ absorption: volume.absorption,
522
+ scattering: volume.scattering,
523
+ },
524
+ fallbackId
525
+ );
526
+ }
527
+
528
+ function normalizeWavefrontMedium(input = {}, index = 0) {
529
+ const id = readNonNegativeInteger("medium id", input.id ?? input.mediumId, index);
530
+ const density = Math.max(0, readFiniteNumber("medium density", input.density, 1));
531
+ const attenuationColor = asColor(
532
+ input.attenuationColor ?? input.color ?? input.medium?.attenuationColor,
533
+ [1, 1, 1, 1]
534
+ );
535
+ const attenuationDistance = readFiniteNumber(
536
+ "medium attenuationDistance",
537
+ input.attenuationDistance ?? input.distance ?? input.medium?.attenuationDistance,
538
+ 0
539
+ );
540
+ const absorption =
541
+ Array.isArray(input.absorption) || Array.isArray(input.medium?.absorption)
542
+ ? asVec3(input.absorption ?? input.medium?.absorption, [0, 0, 0]).map((value) =>
543
+ Math.max(0, Number(value) || 0)
544
+ )
545
+ : deriveBeerLambertAbsorptionFromAttenuationColor(
546
+ attenuationColor,
547
+ attenuationDistance,
548
+ density
549
+ );
550
+ const scattering = asVec3(
551
+ input.scattering ?? input.medium?.scattering,
552
+ [0, 0, 0]
553
+ ).map((value) => Math.max(0, Number(value) || 0));
554
+ return Object.freeze({
555
+ id,
556
+ phaseModel: readMediumPhaseModel(input.phaseModel ?? input.medium?.phaseModel),
557
+ density,
558
+ attenuationColor: Object.freeze(attenuationColor),
559
+ attenuationDistance,
560
+ absorption: Object.freeze(absorption),
561
+ scattering: Object.freeze(scattering),
562
+ });
563
+ }
564
+
565
+ function collectWavefrontMediums(options, meshes, sceneObjects = []) {
566
+ const mediumsById = new Map();
567
+ mediumsById.set(
568
+ 0,
569
+ Object.freeze({
570
+ id: 0,
571
+ phaseModel: DEFAULT_MEDIUM_PHASE_MODEL,
572
+ density: 0,
573
+ attenuationColor: Object.freeze([1, 1, 1, 1]),
574
+ attenuationDistance: 0,
575
+ absorption: Object.freeze([0, 0, 0]),
576
+ scattering: Object.freeze([0, 0, 0]),
577
+ })
578
+ );
579
+
580
+ const register = (input, fallbackId = mediumsById.size) => {
581
+ if (!input) {
582
+ return;
583
+ }
584
+ const normalized = normalizeWavefrontMedium(
585
+ typeof input === "object" ? { id: fallbackId, ...input } : { id: fallbackId },
586
+ fallbackId
587
+ );
588
+ const existing = mediumsById.get(normalized.id);
589
+ if (existing && JSON.stringify(existing) !== JSON.stringify(normalized)) {
590
+ throw new Error(`Medium id ${normalized.id} is defined more than once with different values.`);
591
+ }
592
+ mediumsById.set(normalized.id, normalized);
593
+ };
594
+
595
+ for (const medium of options.mediums ?? []) {
596
+ register(medium);
597
+ }
598
+ for (const mesh of meshes) {
599
+ register(mesh.medium, mesh.mediumRefId ?? mesh.medium?.id ?? 0);
600
+ }
601
+ for (const mesh of meshes) {
602
+ if ((mesh.mediumRefId ?? 0) > 0 && !mediumsById.has(mesh.mediumRefId)) {
603
+ register({ id: mesh.mediumRefId });
604
+ }
605
+ }
606
+ for (const object of sceneObjects) {
607
+ register(object.medium, object.mediumRefId ?? object.medium?.id ?? 0);
608
+ }
609
+ for (const object of sceneObjects) {
610
+ if ((object.mediumRefId ?? 0) > 0 && !mediumsById.has(object.mediumRefId)) {
611
+ register({ id: object.mediumRefId });
612
+ }
613
+ }
614
+
615
+ return Object.freeze(
616
+ Array.from(mediumsById.values()).sort((left, right) => left.id - right.id)
617
+ );
618
+ }
619
+
440
620
  export function normalizeWavefrontSceneObject(input = {}, index = 0) {
441
621
  const bounds = deriveBounds(input);
442
622
  const kind = readObjectKind(input.kind ?? input.type ?? (bounds ? "box" : "sphere"));
@@ -474,6 +654,7 @@ export function normalizeWavefrontSceneObject(input = {}, index = 0) {
474
654
  input.specularColor ?? input.material?.specularColor,
475
655
  [1, 1, 1, 1]
476
656
  ).map((value, componentIndex) => (componentIndex < 3 ? clamp(value, 0, 1) : 1));
657
+ const medium = deriveWavefrontTransportMedium(input, index + 1);
477
658
  const resolvedMaterialKind =
478
659
  emission[0] > 0 || emission[1] > 0 || emission[2] > 0
479
660
  ? MATERIAL_EMISSIVE
@@ -488,6 +669,12 @@ export function normalizeWavefrontSceneObject(input = {}, index = 0) {
488
669
  kind,
489
670
  materialKind: resolvedMaterialKind,
490
671
  flags: readNonNegativeInteger("flags", input.flags, 0),
672
+ mediumRefId: readNonNegativeInteger(
673
+ "mediumRefId",
674
+ input.mediumRefId ?? medium?.id ?? input.medium?.id ?? input.mediumId,
675
+ 0
676
+ ),
677
+ medium,
491
678
  center: Object.freeze(center),
492
679
  halfExtent: Object.freeze(halfExtent),
493
680
  color: Object.freeze(color),
@@ -511,6 +698,7 @@ export function normalizeWavefrontSceneObject(input = {}, index = 0) {
511
698
  ),
512
699
  specular: clamp(readFiniteNumber("specular", input.specular ?? input.material?.specular, 1), 0, 1),
513
700
  specularColor: Object.freeze(specularColor),
701
+ thickness: normalizeWavefrontThickness(input, "thickness"),
514
702
  transmission,
515
703
  });
516
704
  }
@@ -620,6 +808,7 @@ export function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
620
808
  input.specularColor ?? input.material?.specularColor,
621
809
  [1, 1, 1, 1]
622
810
  ).map((value, componentIndex) => (componentIndex < 3 ? clamp(value, 0, 1) : 1));
811
+ const medium = deriveWavefrontTransportMedium(input, meshIndex + 1);
623
812
  const resolvedMaterialKind =
624
813
  emission[0] > 0 || emission[1] > 0 || emission[2] > 0
625
814
  ? MATERIAL_EMISSIVE
@@ -644,9 +833,14 @@ export function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
644
833
  ),
645
834
  mediumRefId: readNonNegativeInteger(
646
835
  "mesh mediumRefId",
647
- input.mediumRefId ?? input.medium?.id ?? input.mediumId,
836
+ input.mediumRefId ??
837
+ medium?.id ??
838
+ input.medium?.id ??
839
+ input.mediumId ??
840
+ input.material?.mediumId,
648
841
  0
649
842
  ),
843
+ medium,
650
844
  color: Object.freeze(color),
651
845
  emission: Object.freeze(emission),
652
846
  roughness: clamp(readFiniteNumber("roughness", input.roughness ?? input.material?.roughness, 0.72), 0, 1),
@@ -668,6 +862,7 @@ export function normalizeWavefrontMesh(input = {}, meshIndex = 0) {
668
862
  ),
669
863
  specular: clamp(readFiniteNumber("specular", input.specular ?? input.material?.specular, 1), 0, 1),
670
864
  specularColor: Object.freeze(specularColor),
865
+ thickness: normalizeWavefrontThickness(input, "mesh thickness"),
671
866
  transmission,
672
867
  baseColorTexture: input.baseColorTexture ?? input.material?.baseColorTexture ?? null,
673
868
  metallicRoughnessTexture:
@@ -902,7 +1097,7 @@ function createMeshTriangleRecords(meshes) {
902
1097
  mesh.clearcoatRoughness,
903
1098
  mesh.specular,
904
1099
  mesh.transmission,
905
- 0,
1100
+ mesh.thickness,
906
1101
  ]),
907
1102
  specularColor: Object.freeze([
908
1103
  mesh.specularColor[0] ?? 1,
@@ -1231,7 +1426,7 @@ export function createWavefrontGpuMaterialSource(meshes = []) {
1231
1426
  mesh.clearcoatRoughness,
1232
1427
  mesh.specular,
1233
1428
  mesh.transmission,
1234
- 0,
1429
+ mesh.thickness,
1235
1430
  ]);
1236
1431
  writeVec4(floatView, byteOffset + 80, [
1237
1432
  mesh.specularColor[0] ?? 1,
@@ -1419,7 +1614,7 @@ export function createWavefrontGpuMeshSource(meshes = [], gpuMaterialSourceInput
1419
1614
  mesh.clearcoatRoughness,
1420
1615
  mesh.specular,
1421
1616
  mesh.transmission,
1422
- 0,
1617
+ mesh.thickness,
1423
1618
  ]);
1424
1619
  writeVec4(meshFloats, floatOffset * 4 + 128, [
1425
1620
  mesh.specularColor[0] ?? 1,
@@ -1534,12 +1729,17 @@ function normalizeSceneObjects(sceneObjects, useDefaultScene = true) {
1534
1729
  return source.map((object, index) => normalizeWavefrontSceneObject(object, index));
1535
1730
  }
1536
1731
 
1732
+ function normalizeWavefrontMeshes(meshes) {
1733
+ const source = Array.isArray(meshes) ? meshes : [];
1734
+ return source.map((mesh, index) => normalizeWavefrontMesh(mesh, index));
1735
+ }
1736
+
1537
1737
  function normalizeMeshes(options = {}) {
1538
1738
  if (Array.isArray(options.meshes)) {
1539
- return options.meshes;
1739
+ return normalizeWavefrontMeshes(options.meshes);
1540
1740
  }
1541
1741
  if (options.mesh) {
1542
- return [options.mesh];
1742
+ return normalizeWavefrontMeshes([options.mesh]);
1543
1743
  }
1544
1744
  return [];
1545
1745
  }
@@ -1899,6 +2099,7 @@ export function createWavefrontPathTracingComputeConfig(options = {}) {
1899
2099
  const sceneObjects = Object.freeze(
1900
2100
  normalizeSceneObjects(options.sceneObjects, meshes.length === 0)
1901
2101
  );
2102
+ const mediums = collectWavefrontMediums(options, meshes, sceneObjects);
1902
2103
  const sceneObjectCapacity = Math.max(
1903
2104
  sceneObjects.length,
1904
2105
  readPositiveInteger("sceneObjectCapacity", options.sceneObjectCapacity, DEFAULT_SCENE_OBJECT_CAPACITY)
@@ -1971,6 +2172,8 @@ export function createWavefrontPathTracingComputeConfig(options = {}) {
1971
2172
  sceneObjects,
1972
2173
  sceneObjectCount: sceneObjects.length,
1973
2174
  sceneObjectCapacity,
2175
+ mediums,
2176
+ mediumCount: mediums.length,
1974
2177
  accelerationBuildMode,
1975
2178
  gpuAccelerationBuildRequired: accelerationBuildMode === "gpu" && triangleCount > 0,
1976
2179
  gpuMeshSource,
@@ -2089,29 +2292,30 @@ export function packWavefrontSceneObjects(sceneObjects, capacity = sceneObjects.
2089
2292
  uintView[u32 + 1] = object.id;
2090
2293
  uintView[u32 + 2] = object.materialKind;
2091
2294
  uintView[u32 + 3] = object.flags;
2092
- writeVec4(floatView, byteOffset + 16, [...object.center, 0]);
2093
- writeVec4(floatView, byteOffset + 32, [...object.halfExtent, 0]);
2094
- writeVec4(floatView, byteOffset + 48, object.color);
2095
- writeVec4(floatView, byteOffset + 64, object.emission);
2096
- writeVec4(floatView, byteOffset + 80, [
2295
+ uintView[u32 + 4] = object.mediumRefId;
2296
+ writeVec4(floatView, byteOffset + 32, [...object.center, 0]);
2297
+ writeVec4(floatView, byteOffset + 48, [...object.halfExtent, 0]);
2298
+ writeVec4(floatView, byteOffset + 64, object.color);
2299
+ writeVec4(floatView, byteOffset + 80, object.emission);
2300
+ writeVec4(floatView, byteOffset + 96, [
2097
2301
  object.roughness,
2098
2302
  object.metallic,
2099
2303
  object.opacity,
2100
2304
  object.ior,
2101
2305
  ]);
2102
- writeVec4(floatView, byteOffset + 96, [
2306
+ writeVec4(floatView, byteOffset + 112, [
2103
2307
  object.sheenColor[0] ?? 0,
2104
2308
  object.sheenColor[1] ?? 0,
2105
2309
  object.sheenColor[2] ?? 0,
2106
2310
  object.clearcoat,
2107
2311
  ]);
2108
- writeVec4(floatView, byteOffset + 112, [
2312
+ writeVec4(floatView, byteOffset + 128, [
2109
2313
  object.clearcoatRoughness,
2110
2314
  object.specular,
2111
2315
  object.transmission,
2112
- 0,
2316
+ object.thickness,
2113
2317
  ]);
2114
- writeVec4(floatView, byteOffset + 128, [
2318
+ writeVec4(floatView, byteOffset + 144, [
2115
2319
  object.specularColor[0] ?? 1,
2116
2320
  object.specularColor[1] ?? 1,
2117
2321
  object.specularColor[2] ?? 1,
@@ -2710,7 +2914,10 @@ function integrateBrdfSample(nDotV, roughness, sampleCount) {
2710
2914
  return [scaleTerm / sampleCount, biasTerm / sampleCount];
2711
2915
  }
2712
2916
 
2713
- function createBrdfLutUploadBytes(size = DEFAULT_BRDF_LUT_SIZE, sampleCount = 1024) {
2917
+ function createBrdfLutUploadBytes(
2918
+ size = DEFAULT_BRDF_LUT_SIZE,
2919
+ sampleCount = DEFAULT_BRDF_LUT_SAMPLE_COUNT
2920
+ ) {
2714
2921
  const cacheKey = `${Math.max(1, Math.trunc(size))}:${Math.max(1, Math.trunc(sampleCount))}`;
2715
2922
  const cached = BRDF_LUT_UPLOAD_CACHE.get(cacheKey);
2716
2923
  if (cached) {
@@ -3137,6 +3344,85 @@ function createBrdfLutResource(device, constants, size = DEFAULT_BRDF_LUT_SIZE)
3137
3344
  });
3138
3345
  }
3139
3346
 
3347
+ function createMediumTextureResource(device, constants, mediums) {
3348
+ const normalized = Array.isArray(mediums) && mediums.length > 0 ? mediums : [{ id: 0 }];
3349
+ const width = Math.max(
3350
+ 1,
3351
+ normalized.reduce((maximum, medium) => Math.max(maximum, medium.id ?? 0), 0) + 1
3352
+ );
3353
+ const level = {
3354
+ width,
3355
+ height: MEDIUM_TABLE_ROWS,
3356
+ data: new Float32Array(width * MEDIUM_TABLE_ROWS * 4),
3357
+ };
3358
+
3359
+ for (const medium of normalized) {
3360
+ const mediumId = Math.max(0, Math.trunc(Number(medium.id) || 0));
3361
+ const absorptionOffset = mediumId * 4;
3362
+ level.data[absorptionOffset] = Math.max(0, medium.absorption?.[0] ?? 0);
3363
+ level.data[absorptionOffset + 1] = Math.max(0, medium.absorption?.[1] ?? 0);
3364
+ level.data[absorptionOffset + 2] = Math.max(0, medium.absorption?.[2] ?? 0);
3365
+ level.data[absorptionOffset + 3] = Math.max(0, medium.phaseModel ?? 0);
3366
+
3367
+ const scatteringOffset = (width + mediumId) * 4;
3368
+ level.data[scatteringOffset] = Math.max(0, medium.scattering?.[0] ?? 0);
3369
+ level.data[scatteringOffset + 1] = Math.max(0, medium.scattering?.[1] ?? 0);
3370
+ level.data[scatteringOffset + 2] = Math.max(0, medium.scattering?.[2] ?? 0);
3371
+ level.data[scatteringOffset + 3] = Math.max(0, medium.density ?? 0);
3372
+ }
3373
+
3374
+ const upload = createFloat16RgbaUploadFromLevels([level])[0];
3375
+ const texture = device.createTexture({
3376
+ label: "plasius.wavefront.mediumTable",
3377
+ size: { width, height: MEDIUM_TABLE_ROWS },
3378
+ format: "rgba16float",
3379
+ usage: constants.texture.TEXTURE_BINDING | constants.texture.COPY_DST,
3380
+ });
3381
+ device.queue.writeTexture(
3382
+ { texture },
3383
+ upload.bytes,
3384
+ { bytesPerRow: upload.bytesPerRow, rowsPerImage: upload.height },
3385
+ { width, height: MEDIUM_TABLE_ROWS, depthOrArrayLayers: 1 }
3386
+ );
3387
+ return Object.freeze({
3388
+ texture,
3389
+ view: texture.createView(),
3390
+ ownsTexture: true,
3391
+ count: normalized.length,
3392
+ width,
3393
+ });
3394
+ }
3395
+
3396
+ function mediumTablesEqual(left, right) {
3397
+ const leftMediums = Array.isArray(left) ? left : [];
3398
+ const rightMediums = Array.isArray(right) ? right : [];
3399
+ if (leftMediums.length !== rightMediums.length) {
3400
+ return false;
3401
+ }
3402
+ for (let index = 0; index < leftMediums.length; index += 1) {
3403
+ const leftMedium = leftMediums[index];
3404
+ const rightMedium = rightMediums[index];
3405
+ if ((leftMedium?.id ?? 0) !== (rightMedium?.id ?? 0)) {
3406
+ return false;
3407
+ }
3408
+ if ((leftMedium?.phaseModel ?? 0) !== (rightMedium?.phaseModel ?? 0)) {
3409
+ return false;
3410
+ }
3411
+ if ((leftMedium?.density ?? 0) !== (rightMedium?.density ?? 0)) {
3412
+ return false;
3413
+ }
3414
+ for (let component = 0; component < 3; component += 1) {
3415
+ if ((leftMedium?.absorption?.[component] ?? 0) !== (rightMedium?.absorption?.[component] ?? 0)) {
3416
+ return false;
3417
+ }
3418
+ if ((leftMedium?.scattering?.[component] ?? 0) !== (rightMedium?.scattering?.[component] ?? 0)) {
3419
+ return false;
3420
+ }
3421
+ }
3422
+ }
3423
+ return true;
3424
+ }
3425
+
3140
3426
  function createAtlasTextureResource(device, constants, atlas, label) {
3141
3427
  const upload = createRgba8TextureUpload(atlas);
3142
3428
  const texture = device.createTexture({
@@ -3283,6 +3569,10 @@ struct SceneObject {
3283
3569
  objectId: u32,
3284
3570
  materialKind: u32,
3285
3571
  flags: u32,
3572
+ mediumRefId: u32,
3573
+ pad0: u32,
3574
+ pad1: u32,
3575
+ pad2: u32,
3286
3576
  center: vec4<f32>,
3287
3577
  halfExtent: vec4<f32>,
3288
3578
  color: vec4<f32>,
@@ -3343,9 +3633,9 @@ struct BvhLeafRef {
3343
3633
  struct ScatterResult {
3344
3634
  direction: vec4<f32>,
3345
3635
  pdf: f32,
3636
+ mediumRefId: u32,
3346
3637
  flags: u32,
3347
3638
  pad0: u32,
3348
- pad1: u32,
3349
3639
  };
3350
3640
 
3351
3641
  struct MeshVertex {
@@ -3491,6 +3781,7 @@ struct EnvironmentPortal {
3491
3781
  @group(0) @binding(29) var brdfLutTexture: texture_2d<f32>;
3492
3782
  @group(0) @binding(30) var brdfLutSampler: sampler;
3493
3783
  @group(0) @binding(31) var environmentSamplingTexture: texture_2d<f32>;
3784
+ @group(0) @binding(32) var mediumTableTexture: texture_2d<f32>;
3494
3785
 
3495
3786
  fn hash_u32(value: u32) -> u32 {
3496
3787
  var x = value;
@@ -4156,6 +4447,60 @@ fn gated_environment_radiance(origin: vec3<f32>, direction: vec3<f32>) -> vec3<f
4156
4447
  return environment_radiance(origin, direction);
4157
4448
  }
4158
4449
 
4450
+ fn medium_dimensions() -> vec2<u32> {
4451
+ return textureDimensions(mediumTableTexture);
4452
+ }
4453
+
4454
+ fn medium_valid(mediumRefId: u32) -> bool {
4455
+ let dimensions = medium_dimensions();
4456
+ return mediumRefId > 0u && mediumRefId < dimensions.x;
4457
+ }
4458
+
4459
+ fn medium_absorption(mediumRefId: u32) -> vec3<f32> {
4460
+ if (!medium_valid(mediumRefId)) {
4461
+ return vec3<f32>(0.0);
4462
+ }
4463
+ return max(
4464
+ textureLoad(mediumTableTexture, vec2<i32>(i32(mediumRefId), 0), 0).xyz,
4465
+ vec3<f32>(0.0)
4466
+ );
4467
+ }
4468
+
4469
+ fn medium_scattering(mediumRefId: u32) -> vec3<f32> {
4470
+ if (!medium_valid(mediumRefId)) {
4471
+ return vec3<f32>(0.0);
4472
+ }
4473
+ return max(
4474
+ textureLoad(mediumTableTexture, vec2<i32>(i32(mediumRefId), 1), 0).xyz,
4475
+ vec3<f32>(0.0)
4476
+ );
4477
+ }
4478
+
4479
+ fn medium_transmittance(mediumRefId: u32, distance: f32) -> vec3<f32> {
4480
+ if (!medium_valid(mediumRefId) || distance <= 0.000001) {
4481
+ return vec3<f32>(1.0);
4482
+ }
4483
+ let extinction = medium_absorption(mediumRefId) + medium_scattering(mediumRefId);
4484
+ return vec3<f32>(
4485
+ exp(-extinction.x * distance),
4486
+ exp(-extinction.y * distance),
4487
+ exp(-extinction.z * distance)
4488
+ );
4489
+ }
4490
+
4491
+ fn transmitted_medium_ref_id(ray: RayRecord, hit: HitRecord) -> u32 {
4492
+ if (hit.mediumRefId == 0u) {
4493
+ return ray.mediumRefId;
4494
+ }
4495
+ if (hit.frontFace == 1u) {
4496
+ return hit.mediumRefId;
4497
+ }
4498
+ if (ray.mediumRefId == hit.mediumRefId) {
4499
+ return 0u;
4500
+ }
4501
+ return ray.mediumRefId;
4502
+ }
4503
+
4159
4504
  fn surface_path_response(hit: HitRecord) -> vec3<f32> {
4160
4505
  let color = clamp(hit.color.xyz, vec3<f32>(0.0), vec3<f32>(1.0));
4161
4506
  let opacity = clamp(hit.material.z, 0.0, 1.0);
@@ -4254,11 +4599,15 @@ fn terminal_surface_environment_source(ray: RayRecord, hit: HitRecord) -> vec3<f
4254
4599
  return clamp_sample_radiance(environmentFloor * materialFloor);
4255
4600
  }
4256
4601
 
4257
- fn terminal_surface_environment_contribution(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
4602
+ fn terminal_surface_environment_contribution(
4603
+ ray: RayRecord,
4604
+ throughput: vec3<f32>,
4605
+ hit: HitRecord
4606
+ ) -> vec3<f32> {
4258
4607
  let surfaceColor = max(hit.color.xyz, config.ambientColor.xyz);
4259
4608
  let occlusion = mix(0.75, 1.0, clamp(hit.occlusion, 0.0, 1.0));
4260
4609
  return clamp_sample_radiance(
4261
- ray.throughput.xyz *
4610
+ throughput *
4262
4611
  surfaceColor *
4263
4612
  terminal_surface_environment_source(ray, hit) *
4264
4613
  occlusion
@@ -4732,7 +5081,7 @@ fn intersect_sphere(ray: RayRecord, object: SceneObject) -> Candidate {
4732
5081
  0xffffffffu,
4733
5082
  object.objectId,
4734
5083
  object.objectId,
4735
- 0u
5084
+ object.mediumRefId
4736
5085
  );
4737
5086
  }
4738
5087
 
@@ -4784,7 +5133,7 @@ fn intersect_box(ray: RayRecord, object: SceneObject) -> Candidate {
4784
5133
  0xffffffffu,
4785
5134
  object.objectId,
4786
5135
  object.objectId,
4787
- 0u
5136
+ object.mediumRefId
4788
5137
  );
4789
5138
  }
4790
5139
 
@@ -5035,6 +5384,10 @@ fn intersectActiveQueue(@builtin(global_invocation_id) globalId: vec3<u32>) {
5035
5384
  let ray = activeQueue[index];
5036
5385
  var nearest = 1000000.0;
5037
5386
  var hitObject = SceneObject(
5387
+ 0u,
5388
+ 0u,
5389
+ 0u,
5390
+ 0u,
5038
5391
  0u,
5039
5392
  0u,
5040
5393
  0u,
@@ -5238,9 +5591,9 @@ fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult
5238
5591
  return ScatterResult(
5239
5592
  vec4<f32>(reflect(ray.direction.xyz, normal), 0.0),
5240
5593
  1.0,
5594
+ ray.mediumRefId,
5241
5595
  RAY_FLAG_DELTA_SAMPLE,
5242
5596
  0u,
5243
- 0u
5244
5597
  );
5245
5598
  }
5246
5599
 
@@ -5260,17 +5613,17 @@ fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult
5260
5613
  return ScatterResult(
5261
5614
  vec4<f32>(reflect(ray.direction.xyz, normal), 0.0),
5262
5615
  1.0,
5616
+ ray.mediumRefId,
5263
5617
  RAY_FLAG_DELTA_SAMPLE,
5264
5618
  0u,
5265
- 0u
5266
5619
  );
5267
5620
  }
5268
5621
  return ScatterResult(
5269
5622
  vec4<f32>(refract_direction(ray.direction.xyz, normal, etaRatio), 0.0),
5270
5623
  1.0,
5624
+ transmitted_medium_ref_id(ray, hit),
5271
5625
  RAY_FLAG_DELTA_SAMPLE,
5272
5626
  0u,
5273
- 0u
5274
5627
  );
5275
5628
  }
5276
5629
 
@@ -5285,9 +5638,9 @@ fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult
5285
5638
  return ScatterResult(
5286
5639
  vec4<f32>(guidedDirection, 0.0),
5287
5640
  guidedPdf,
5641
+ ray.mediumRefId,
5288
5642
  RAY_FLAG_GUIDED_EMISSIVE,
5289
5643
  0u,
5290
- 0u
5291
5644
  );
5292
5645
  }
5293
5646
  }
@@ -5295,7 +5648,7 @@ fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult
5295
5648
  let guidedDirection = sample_environment_portal_direction(hit, seed + 131u, normal);
5296
5649
  if (dot(normal, guidedDirection) > 0.000001) {
5297
5650
  let guidedPdf = max(evaluate_surface_bsdf_pdf(hit, viewDirection, guidedDirection), 0.000001);
5298
- return ScatterResult(vec4<f32>(guidedDirection, 0.0), guidedPdf, 0u, 0u, 0u);
5651
+ return ScatterResult(vec4<f32>(guidedDirection, 0.0), guidedPdf, ray.mediumRefId, 0u, 0u);
5299
5652
  }
5300
5653
  }
5301
5654
 
@@ -5329,7 +5682,7 @@ fn scatter_direction(ray: RayRecord, hit: HitRecord, seed: u32) -> ScatterResult
5329
5682
  );
5330
5683
  }
5331
5684
  let pdf = max(evaluate_surface_bsdf_pdf(hit, viewDirection, lightDirection), 0.000001);
5332
- return ScatterResult(vec4<f32>(lightDirection, 0.0), pdf, 0u, 0u, 0u);
5685
+ return ScatterResult(vec4<f32>(lightDirection, 0.0), pdf, ray.mediumRefId, 0u, 0u);
5333
5686
  }
5334
5687
 
5335
5688
  @compute @workgroup_size(64)
@@ -5342,15 +5695,17 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5342
5695
 
5343
5696
  let ray = activeQueue[index];
5344
5697
  let hit = hits[index];
5698
+ let segmentTransmittance = medium_transmittance(ray.mediumRefId, hit.distance);
5699
+ let arrivingThroughput = ray.throughput.xyz * segmentTransmittance;
5345
5700
  var contribution = vec3<f32>(0.0);
5346
5701
 
5347
5702
  if (hit.hitType == 1u) {
5348
5703
  let guidedLightWeight = select(1.0, 0.24, (ray.flags & RAY_FLAG_GUIDED_EMISSIVE) != 0u);
5349
5704
  let sourceRadiance = max(hit.emission.xyz, hit.color.xyz) * guidedLightWeight;
5350
5705
  if (deferred_path_resolve_enabled()) {
5351
- record_deferred_terminal_source(ray, sourceRadiance);
5706
+ record_deferred_terminal_source(ray, sourceRadiance * segmentTransmittance);
5352
5707
  } else {
5353
- contribution = clamp_sample_radiance(ray.throughput.xyz * sourceRadiance);
5708
+ contribution = clamp_sample_radiance(arrivingThroughput * sourceRadiance);
5354
5709
  accumulation[ray.rayId] =
5355
5710
  accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
5356
5711
  }
@@ -5367,9 +5722,9 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5367
5722
  sourceRadiance = sourceRadiance * misWeight;
5368
5723
  }
5369
5724
  if (deferred_path_resolve_enabled()) {
5370
- record_deferred_terminal_source(ray, sourceRadiance);
5725
+ record_deferred_terminal_source(ray, sourceRadiance * segmentTransmittance);
5371
5726
  } else {
5372
- contribution = clamp_sample_radiance(ray.throughput.xyz * sourceRadiance);
5727
+ contribution = clamp_sample_radiance(arrivingThroughput * sourceRadiance);
5373
5728
  accumulation[ray.rayId] =
5374
5729
  accumulation[ray.rayId] + vec4<f32>(contribution * sample_weight(), 1.0);
5375
5730
  }
@@ -5377,7 +5732,11 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5377
5732
  return;
5378
5733
  }
5379
5734
 
5380
- let response = stabilize_surface_path_response(ray, hit, surface_path_response(hit));
5735
+ let response = stabilize_surface_path_response(
5736
+ ray,
5737
+ hit,
5738
+ surface_path_response(hit) * segmentTransmittance
5739
+ );
5381
5740
  record_deferred_path_response(ray, response);
5382
5741
 
5383
5742
  let shouldEstimateDirectEnvironment =
@@ -5385,7 +5744,22 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5385
5744
  hit.material.z >= 0.95 &&
5386
5745
  ray.bounce < 2u;
5387
5746
  if (shouldEstimateDirectEnvironment) {
5388
- let directEnvironment = surface_direct_environment_contribution(ray, hit);
5747
+ let directEnvironment = surface_direct_environment_contribution(
5748
+ RayRecord(
5749
+ ray.rayId,
5750
+ ray.parentRayId,
5751
+ ray.sourcePixelId,
5752
+ ray.sampleId,
5753
+ ray.bounce,
5754
+ ray.mediumRefId,
5755
+ ray.flags,
5756
+ 0u,
5757
+ ray.origin,
5758
+ ray.direction,
5759
+ vec4<f32>(arrivingThroughput, ray.throughput.w)
5760
+ ),
5761
+ hit
5762
+ );
5389
5763
  accumulation[ray.rayId] =
5390
5764
  accumulation[ray.rayId] + vec4<f32>(directEnvironment * sample_weight(), 0.0);
5391
5765
  }
@@ -5394,7 +5768,11 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5394
5768
  if (deferred_path_resolve_enabled()) {
5395
5769
  record_deferred_terminal_source(ray, terminal_surface_environment_source(ray, hit));
5396
5770
  } else {
5397
- let terminalEnvironment = terminal_surface_environment_contribution(ray, hit);
5771
+ let terminalEnvironment = terminal_surface_environment_contribution(
5772
+ ray,
5773
+ arrivingThroughput,
5774
+ hit
5775
+ );
5398
5776
  accumulation[ray.rayId] =
5399
5777
  accumulation[ray.rayId] + vec4<f32>(terminalEnvironment * sample_weight(), 1.0);
5400
5778
  }
@@ -5409,7 +5787,11 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5409
5787
  if (deferred_path_resolve_enabled()) {
5410
5788
  record_deferred_terminal_source(ray, terminal_surface_environment_source(ray, hit));
5411
5789
  } else {
5412
- let overflowEnvironment = terminal_surface_environment_contribution(ray, hit);
5790
+ let overflowEnvironment = terminal_surface_environment_contribution(
5791
+ ray,
5792
+ arrivingThroughput,
5793
+ hit
5794
+ );
5413
5795
  accumulation[ray.rayId] =
5414
5796
  accumulation[ray.rayId] + vec4<f32>(overflowEnvironment * sample_weight(), 1.0);
5415
5797
  }
@@ -5423,7 +5805,7 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
5423
5805
  ray.sourcePixelId,
5424
5806
  ray.sampleId,
5425
5807
  ray.bounce + 1u,
5426
- ray.mediumRefId,
5808
+ scatter.mediumRefId,
5427
5809
  scatter.flags,
5428
5810
  0u,
5429
5811
  vec4<f32>(offset_origin(hit.position.xyz, hit.shadingNormal.xyz), 1.0),
@@ -5945,6 +6327,11 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
5945
6327
  config.environmentMap,
5946
6328
  config.environmentColor
5947
6329
  );
6330
+ let mediumTextureResource = createMediumTextureResource(
6331
+ device,
6332
+ constants,
6333
+ config.mediums
6334
+ );
5948
6335
  config = Object.freeze({
5949
6336
  ...config,
5950
6337
  environmentMap: Object.freeze({
@@ -6033,6 +6420,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
6033
6420
  { binding: 29, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
6034
6421
  { binding: 30, visibility: constants.shader.COMPUTE, sampler: { type: "filtering" } },
6035
6422
  { binding: 31, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
6423
+ { binding: 32, visibility: constants.shader.COMPUTE, texture: { sampleType: "float" } },
6036
6424
  ],
6037
6425
  });
6038
6426
  const accelerationBindGroupLayout = device.createBindGroupLayout({
@@ -6222,14 +6610,19 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
6222
6610
  { binding: 29, resource: brdfLutResource.view },
6223
6611
  { binding: 30, resource: brdfLutResource.sampler },
6224
6612
  { binding: 31, resource: environmentSamplingResource.view },
6613
+ { binding: 32, resource: mediumTextureResource.view },
6225
6614
  ],
6226
6615
  });
6227
6616
  }
6228
6617
 
6229
- const bindGroups = [
6230
- createTraceBindGroup(activeQueue, nextQueue, "plasius.wavefront.bind.activeNext"),
6231
- createTraceBindGroup(nextQueue, activeQueue, "plasius.wavefront.bind.nextActive"),
6232
- ];
6618
+ function createTraceBindGroups() {
6619
+ return [
6620
+ createTraceBindGroup(activeQueue, nextQueue, "plasius.wavefront.bind.activeNext"),
6621
+ createTraceBindGroup(nextQueue, activeQueue, "plasius.wavefront.bind.nextActive"),
6622
+ ];
6623
+ }
6624
+
6625
+ let bindGroups = createTraceBindGroups();
6233
6626
  const bvhBuildBindGroup = device.createBindGroup({
6234
6627
  label: "plasius.wavefront.bind.bvhBuild",
6235
6628
  layout: accelerationBindGroupLayout,
@@ -6418,6 +6811,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
6418
6811
  emissiveTriangleCount: config.emissiveTriangleCount,
6419
6812
  environmentPortalCount: config.environmentPortalCount,
6420
6813
  environmentPortalMode: config.environmentPortalMode,
6814
+ mediumCount: config.mediumCount,
6421
6815
  environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
6422
6816
  deferredPathResolve: config.deferredPathResolve,
6423
6817
  bvhNodeCount: config.bvhNodeCount,
@@ -7007,10 +7401,23 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
7007
7401
  });
7008
7402
  }
7009
7403
 
7404
+ function rebuildMediumResources(nextConfig) {
7405
+ const previousMediumTextureResource = mediumTextureResource;
7406
+ mediumTextureResource = createMediumTextureResource(device, constants, nextConfig.mediums);
7407
+ bindGroups = createTraceBindGroups();
7408
+ if (previousMediumTextureResource?.ownsTexture) {
7409
+ previousMediumTextureResource.texture?.destroy?.();
7410
+ }
7411
+ }
7412
+
7010
7413
  function updateSceneObjects(sceneObjects) {
7011
7414
  const nextPackedScene = packWavefrontSceneObjects(sceneObjects, config.sceneObjectCapacity);
7012
7415
  packedScene = nextPackedScene;
7013
- config = rebuildLiveConfig();
7416
+ const nextConfig = rebuildLiveConfig();
7417
+ if (!mediumTablesEqual(config.mediums, nextConfig.mediums)) {
7418
+ rebuildMediumResources(nextConfig);
7419
+ }
7420
+ config = nextConfig;
7014
7421
  device.queue.writeBuffer(sceneObjectBuffer, 0, packedScene.buffer);
7015
7422
  return config;
7016
7423
  }
@@ -7036,6 +7443,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
7036
7443
  emissiveTriangleCount: config.emissiveTriangleCount,
7037
7444
  environmentPortalCount: config.environmentPortalCount,
7038
7445
  environmentPortalMode: config.environmentPortalMode,
7446
+ mediumCount: config.mediumCount,
7039
7447
  environmentMap: createEnvironmentMapSnapshot(config.environmentMap),
7040
7448
  deferredPathResolve: config.deferredPathResolve,
7041
7449
  bvhNodeCount: config.bvhNodeCount,
@@ -7070,17 +7478,20 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
7070
7478
  activeDispatchBuffer.destroy?.();
7071
7479
  radianceTexture.destroy?.();
7072
7480
  denoiseScratchTexture.destroy?.();
7073
- outputTexture.destroy?.();
7074
- if (environmentMapResource.ownsTexture) {
7075
- environmentMapResource.texture?.destroy?.();
7076
- }
7077
- if (environmentSamplingResource.ownsTexture) {
7078
- environmentSamplingResource.texture?.destroy?.();
7079
- }
7080
- brdfLutResource.texture?.destroy?.();
7081
- if (baseColorAtlasResource.ownsTexture) {
7082
- baseColorAtlasResource.texture?.destroy?.();
7083
- }
7481
+ outputTexture.destroy?.();
7482
+ if (environmentMapResource.ownsTexture) {
7483
+ environmentMapResource.texture?.destroy?.();
7484
+ }
7485
+ if (environmentSamplingResource.ownsTexture) {
7486
+ environmentSamplingResource.texture?.destroy?.();
7487
+ }
7488
+ if (mediumTextureResource.ownsTexture) {
7489
+ mediumTextureResource.texture?.destroy?.();
7490
+ }
7491
+ brdfLutResource.texture?.destroy?.();
7492
+ if (baseColorAtlasResource.ownsTexture) {
7493
+ baseColorAtlasResource.texture?.destroy?.();
7494
+ }
7084
7495
  if (metallicRoughnessAtlasResource.ownsTexture) {
7085
7496
  metallicRoughnessAtlasResource.texture?.destroy?.();
7086
7497
  }