rayzee 11.0.0 → 11.1.1

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.
Files changed (36) hide show
  1. package/README.md +1 -1
  2. package/dist/{ArchiveImporter-BRQvLggb.js → ArchiveImporter-CFcggGqi.js} +1525 -680
  3. package/dist/BidirectionalIntegrator-DIPN8lJW.js +946 -0
  4. package/dist/{NeuralRetouch-BCXylThH.js → NeuralRetouch-Doo3a4vY.js} +1 -1
  5. package/dist/{NeuralSuperRes-sZAeS142.js → NeuralSuperRes-B-rNIU_R.js} +1 -1
  6. package/dist/{PathTracer-CQPW06Mm.js → PathTracer-D_LmDeLh.js} +79 -74
  7. package/dist/{PhysicalSky-BvqCw_dy.js → PhysicalSky-DYffGJ3O.js} +1 -1
  8. package/dist/{RayzeeRenderer-COpwotpU.js → RayzeeRenderer-DWx7D714.js} +3 -3
  9. package/dist/addons/archives.es.js +1 -1
  10. package/dist/addons/bidirectional.es.js +1 -1
  11. package/dist/addons/physical-sky.es.js +1 -1
  12. package/dist/{archiveFormats-BZw7TiDp.js → archiveFormats-tnAoUL1j.js} +28 -9
  13. package/dist/rayzee-core.es.js +4 -4
  14. package/dist/rayzee.es.js +12 -12
  15. package/dist/rayzee.umd.js +43 -43
  16. package/dist/{sceneCachePolicy-BSSsKY98.js → sceneCachePolicy-Bk8sI5PZ.js} +1 -1
  17. package/package.json +1 -1
  18. package/src/PathTracerApp.js +1 -0
  19. package/src/Processor/ArchiveCache.js +19 -6
  20. package/src/Processor/ArchiveImporter.js +9 -3
  21. package/src/Processor/ArchiveReader.js +22 -4
  22. package/src/Processor/PBRT/PBRTMaterials.js +132 -8
  23. package/src/Processor/PBRT/PBRTParser.js +20 -3
  24. package/src/Processor/PBRT/PBRTScattering.js +93 -0
  25. package/src/Processor/PBRT/PBRTSceneBuilder.js +513 -49
  26. package/src/Processor/PBRT/PBRTTextureBake.js +32 -7
  27. package/src/Processor/SceneMetadata.js +30 -5
  28. package/src/RayzeeRenderer.js +3 -2
  29. package/src/Stages/PathTracer.js +3 -5
  30. package/src/TSL/Bidirectional.js +6 -2
  31. package/src/TSL/ConnectKernel.js +31 -11
  32. package/src/TSL/LightSplatKernel.js +22 -7
  33. package/src/TSL/MergeKernel.js +13 -7
  34. package/src/TSL/ShadeKernel.js +51 -30
  35. package/src/integrators/BidirectionalIntegrator.js +6 -1
  36. package/dist/BidirectionalIntegrator-9lZQxFVH.js +0 -934
@@ -3,8 +3,8 @@
3
3
  * an image texture's `scale` above 1 or `invert`, and a `mix` material whose amount is a texture.
4
4
  *
5
5
  * A layer is an image as pbrt evaluates it: linear values, top row first, `channels` 1 (a float texture reads
6
- * the red channel) or 3, already scaled and inverted, and the uv mapping pbrt applies before the lookup
7
- * (st = scale · uv + delta, image row from the top = 1 − t).
6
+ * an image's alpha where it has one, else the mean of its colour) or 3, already scaled and inverted, and the uv
7
+ * mapping pbrt applies before the lookup (st = scale · uv + delta, image row from the top = 1 − t).
8
8
  */
9
9
 
10
10
  const MAX_BAKE_SIZE = 4096;
@@ -28,6 +28,10 @@ export function makeLayer( pixels, { channels, decode = null, scale = 1, invert
28
28
  const stride = pixels.channels ?? 4;
29
29
  const data = new Float32Array( width * height * channels );
30
30
  const topDown = pixels.topDown !== false;
31
+ // pbrt's encoding applies to every channel of an 8-bit image, alpha included.
32
+ const read = ( i ) => ( decode ? decode( pixels.data[ i ] / 255 ) : pixels.data[ i ] );
33
+ const fromAlpha = channels === 1 && stride === 4 && hasAlpha( pixels );
34
+ const mean = channels === 1 && stride >= 3 && ! fromAlpha;
31
35
 
32
36
  for ( let y = 0; y < height; y ++ ) {
33
37
 
@@ -37,8 +41,10 @@ export function makeLayer( pixels, { channels, decode = null, scale = 1, invert
37
41
  const src = ( row * width + x ) * stride, dst = ( y * width + x ) * channels;
38
42
  for ( let c = 0; c < channels; c ++ ) {
39
43
 
40
- const raw = pixels.data[ src + Math.min( c, stride - 1 ) ];
41
- const v = scale * ( decode ? decode( raw / 255 ) : raw );
44
+ const raw = fromAlpha ? read( src + 3 )
45
+ : mean ? ( read( src ) + read( src + 1 ) + read( src + 2 ) ) / 3
46
+ : read( src + Math.min( c, stride - 1 ) );
47
+ const v = scale * raw;
42
48
  data[ dst + c ] = invert ? Math.max( 0, 1 - v ) : v;
43
49
 
44
50
  }
@@ -51,6 +57,16 @@ export function makeLayer( pixels, { channels, decode = null, scale = 1, invert
51
57
 
52
58
  }
53
59
 
60
+ /** Whether an RGBA image's alpha is anything but opaque — pbrt reads it as an RGB image when it is not. */
61
+ export function hasAlpha( pixels ) {
62
+
63
+ const { data } = pixels;
64
+ const one = pixels.float ? 1 : 255;
65
+ for ( let i = 3; i < data.length; i += 4 ) if ( data[ i ] !== one ) return true;
66
+ return false;
67
+
68
+ }
69
+
54
70
  /** The layer's mean value (a float layer's only channel; a spectrum layer's average of three). */
55
71
  export function layerMean( layer ) {
56
72
 
@@ -91,9 +107,12 @@ function sample( layer, s, t, out ) {
91
107
  * (bottom row first), clamped to [0, 1] as pbrt clamps an albedo. Null when every term is a constant.
92
108
  * @param {Array<{ rgb?: number[], layer?: object, tint?: number[] }>} terms
93
109
  * @param {(values: number[][]) => number[]} combine - linear RGB from each term's linear RGB
110
+ * @param {object} [options]
111
+ * @param {(values: number[][]) => number} [options.alphaOf] - the alpha channel, linear; opaque without it
112
+ * @param {boolean} [options.linear=false] - linear bytes, for a data map
94
113
  * @returns {{ data: Uint8Array, width: number, height: number, mapping: { su: number, sv: number, du: number, dv: number } } | null}
95
114
  */
96
- export function bake( terms, combine ) {
115
+ export function bake( terms, combine, { alphaOf = null, linear = false } = {} ) {
97
116
 
98
117
  let driver = null;
99
118
  for ( const term of terms ) if ( term.layer && ( ! driver || term.layer.width * term.layer.height > driver.width * driver.height ) ) driver = term.layer;
@@ -134,8 +153,14 @@ export function bake( terms, combine ) {
134
153
 
135
154
  const rgb = combine( values );
136
155
  const o = ( j * width + i ) * 4;
137
- for ( let c = 0; c < 3; c ++ ) out[ o + c ] = Math.round( 255 * linearToSRGB( Math.min( 1, Math.max( 0, rgb[ c ] ) ) ) );
138
- out[ o + 3 ] = 255;
156
+ for ( let c = 0; c < 3; c ++ ) {
157
+
158
+ const v = Math.min( 1, Math.max( 0, rgb[ c ] ) );
159
+ out[ o + c ] = Math.round( 255 * ( linear ? v : linearToSRGB( v ) ) );
160
+
161
+ }
162
+
163
+ out[ o + 3 ] = alphaOf ? Math.round( 255 * Math.min( 1, Math.max( 0, alphaOf( values ) ) ) ) : 255;
139
164
 
140
165
  }
141
166
 
@@ -14,6 +14,9 @@
14
14
  * "sourceFile": "https://.../brown_photostudio_02_1k.hdr",
15
15
  * "rotation": 0, // degrees around Y, matches the environmentRotation setting
16
16
  * "intensity": 1 // drives both environmentIntensity and backgroundIntensity
17
+ * },
18
+ * "render": { // what the file asks the renderer for; reported, not applied
19
+ * "maxBounces": 15, "samples": 1024, "width": 1920, "height": 1080
17
20
  * }
18
21
  * }
19
22
  *
@@ -56,10 +59,11 @@ function pickPayload( container ) {
56
59
  const obj = asObject( container );
57
60
  if ( ! obj ) return null;
58
61
 
62
+ const has = ( o ) => o && ( o.environment !== undefined || o.render !== undefined );
59
63
  const scoped = asObject( obj.rayzee );
60
- if ( scoped && scoped.environment !== undefined ) return scoped;
64
+ if ( has( scoped ) ) return scoped;
61
65
 
62
- return obj.environment !== undefined ? obj : null;
66
+ return has( obj ) ? obj : null;
63
67
 
64
68
  }
65
69
 
@@ -84,10 +88,29 @@ function normalizeEnvironment( raw ) {
84
88
 
85
89
  }
86
90
 
91
+ const RENDER_KEYS = [ 'maxBounces', 'samples', 'width', 'height' ];
92
+
93
+ function normalizeRender( raw ) {
94
+
95
+ const render = asObject( raw );
96
+ if ( ! render ) return null;
97
+
98
+ const normalized = {};
99
+ for ( const key of RENDER_KEYS ) {
100
+
101
+ const n = asNumber( render[ key ] );
102
+ if ( n !== undefined && n > 0 ) normalized[ key ] = Math.round( n );
103
+
104
+ }
105
+
106
+ return Object.keys( normalized ).length ? normalized : null;
107
+
108
+ }
109
+
87
110
  /**
88
111
  * Reads scene metadata out of one `extras`-shaped container.
89
112
  * @param {Object|string|null} container
90
- * @returns {{ environment?: { sourceFile: string, rotation?: number, intensity?: number } }|null}
113
+ * @returns {{ environment?: { sourceFile: string, rotation?: number, intensity?: number }, render?: { maxBounces?: number, samples?: number, width?: number, height?: number } }|null}
91
114
  */
92
115
  export function parseSceneMetadata( container ) {
93
116
 
@@ -95,14 +118,16 @@ export function parseSceneMetadata( container ) {
95
118
  if ( ! payload ) return null;
96
119
 
97
120
  const environment = normalizeEnvironment( payload.environment );
98
- return environment ? { environment } : null;
121
+ const render = normalizeRender( payload.render );
122
+ if ( ! environment && ! render ) return null;
123
+ return { ...( environment ? { environment } : {} ), ...( render ? { render } : {} ) };
99
124
 
100
125
  }
101
126
 
102
127
  /**
103
128
  * Reads scene metadata out of a GLTFLoader parse result.
104
129
  * @param {Object} gltf - The object GLTFLoader resolves with
105
- * @returns {{ environment?: { sourceFile: string, rotation?: number, intensity?: number } }|null}
130
+ * @returns {ReturnType<typeof parseSceneMetadata>}
106
131
  */
107
132
  export function extractSceneMetadata( gltf ) {
108
133
 
@@ -1216,8 +1216,9 @@ export class RayzeeRenderer extends EventDispatcher {
1216
1216
  /**
1217
1217
  * Scene-level authoring metadata carried by the current model file (glTF `extras`),
1218
1218
  * or null when the file has none. See {@link module:Processor/SceneMetadata}. A pbrt scene
1219
- * without an infinite light carries `environment: { enabled: false }`.
1220
- * @type {{ environment?: { sourceFile?: string, rotation?: number, intensity?: number, enabled?: boolean } }|null}
1219
+ * without an infinite light carries `environment: { enabled: false }`. `render` is what the file asks
1220
+ * the renderer for (pbrt's maxdepth, pixelsamples and film resolution); nothing applies it.
1221
+ * @type {{ environment?: { sourceFile?: string, rotation?: number, intensity?: number, enabled?: boolean }, render?: { maxBounces?: number, samples?: number, width?: number, height?: number } }|null}
1221
1222
  */
1222
1223
  get sceneMetadata() {
1223
1224
 
@@ -1443,12 +1443,10 @@ export class PathTracer extends PathTracerStage {
1443
1443
 
1444
1444
  }
1445
1445
 
1446
- /** The material layers the scene uses; bidirectional leaves diffuse transmission out (its paths do not cross surfaces). */
1446
+ /** The material layers the scene uses. */
1447
1447
  _layersToCompile() {
1448
1448
 
1449
- const layers = this.materialData.materialLayers();
1450
- if ( this._integrator ) layers.diffuseTransmission = false;
1451
- return layers;
1449
+ return this.materialData.materialLayers();
1452
1450
 
1453
1451
  }
1454
1452
 
@@ -2083,7 +2081,7 @@ export class PathTracer extends PathTracerStage {
2083
2081
 
2084
2082
  this._integrator?.registerKernels( {
2085
2083
  km: this._kernelManager, qm, pb, counters, bounceCountsBuf, wfCurrentBounce, copyReadB, copyWriteA, maxRays, w, own,
2086
- freshLight, freshTri, freshBvh, freshMat, freshEnvTex, freshEnvCDF,
2084
+ freshLight, freshTri, freshBvh, freshMat, freshEnvTex, freshEnvCDF, materialLayers: this._compiledLayers,
2087
2085
  } );
2088
2086
 
2089
2087
  const fwFn = buildFinalWriteKernel( {
@@ -156,8 +156,6 @@ export function resolveSurfaceMaterial( materialIndex, uv, N, materialBuffer ) {
156
156
  material.normalMapIndex.assign( int( - 1 ) );
157
157
  material.bumpMapIndex.assign( int( - 1 ) );
158
158
  material.displacementMapIndex.assign( int( - 1 ) );
159
- // Shade leaves diffuse transmission out in this integrator (its strategies do not cross surfaces yet).
160
- material.diffuseTransmission.assign( 0.0 );
161
159
 
162
160
  const samples = MaterialSamples.wrap( sampleAllMaterialTextures( material, uv, N, vec4( 0.0 ) ) ).toVar();
163
161
  const rawRough = samples.roughness.toVar();
@@ -187,6 +185,8 @@ export function resolveSurfaceMaterial( materialIndex, uv, N, materialBuffer ) {
187
185
  material.iridescenceThicknessRange.assign( vec2( material.iridescenceThicknessRange.x, ext.iridescenceThickness ) );
188
186
  material.specularIntensity.assign( ext.specularIntensity );
189
187
  material.specularColor.assign( ext.specularColor );
188
+ material.diffuseTransmission.assign( ext.diffuseTransmission );
189
+ material.diffuseTransmissionColor.assign( ext.diffuseTransmissionColor );
190
190
 
191
191
  If( rawRough.lessThan( MIN_ROUGHNESS ).and( material.anisotropy.equal( 0.0 ) ).and( material.clearcoat.equal( 0.0 ) )
192
192
  .and( material.transmission.equal( 0.0 ) ).and( material.subsurface.equal( 0.0 ) ), () => {
@@ -199,6 +199,10 @@ export function resolveSurfaceMaterial( materialIndex, uv, N, materialBuffer ) {
199
199
 
200
200
  }
201
201
 
202
+ // The normal of the frame with `toward` on top: a surface that passes light through diffusely is seen from either side.
203
+ export const facingSide = ( N, toward, material ) => select(
204
+ material.diffuseTransmission.greaterThan( 0.0 ).and( dot( N, toward ).lessThan( 0.0 ) ), N.negate(), N );
205
+
202
206
  // The light end's cosine with the shading-normal correction (Veach 5.3.2): |V·N| |d·Ng| / |V·Ng|.
203
207
  export const lightEndCosine = ( V, N, facetN, toOther ) =>
204
208
  abs( dot( V, N ) ).mul( abs( dot( toOther, facetN ) ) ).div( max( abs( dot( V, facetN ) ), 1e-4 ) );
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import {
7
- Fn, float, vec2, vec4, int, uint, If, instanceIndex, atomicLoad, Return, dot, sqrt, min, max, length,
7
+ Fn, float, vec2, vec4, int, uint, If, instanceIndex, atomicLoad, Return, dot, sqrt, min, max, length, select, abs,
8
8
  } from 'three/tsl';
9
9
 
10
10
  import { getRandomSample1D, pcgHash, SAMPLER_DIMS_PER_BOUNCE } from './Random.js';
@@ -15,7 +15,7 @@ import { traverseBVHShadow } from './BVHTraversal.js';
15
15
  import { offsetRayOrigin, SHADOW_END, luminance } from './Common.js';
16
16
  import { regularizePathContribution } from './PathTracerCore.js';
17
17
  import {
18
- resolveSurfaceMaterial, lightEndCosine, misPartial, misWeight, strategyWeight, STRATEGY, DIM_CONNECT_PATH, DIM_CONNECT_VERTEX,
18
+ resolveSurfaceMaterial, lightEndCosine, misPartial, misWeight, strategyWeight, facingSide, STRATEGY, DIM_CONNECT_PATH, DIM_CONNECT_VERTEX,
19
19
  } from './Bidirectional.js';
20
20
  import { COUNTER } from '../Processor/QueueManager.js';
21
21
  import {
@@ -35,6 +35,8 @@ export function buildConnectKernel( params ) {
35
35
  maxBounceCount, globalIlluminationIntensity, fireflyThreshold,
36
36
  // Vertex merging's η² at a point (Bidirectional.js mergeVmAt), or null without it.
37
37
  mergeVm = null,
38
+ // Some material passes light through diffusely: a connection may then reach either end from behind.
39
+ diffuseTransmission = true,
38
40
  } = params;
39
41
 
40
42
  return Fn( () => {
@@ -89,7 +91,15 @@ export function buildConnectKernel( params ) {
89
91
  const cosCameraFacet = dot( dir, cam.facetN ).toVar();
90
92
  const cosLightFacet = dot( dir, light.facetN ).negate().toVar();
91
93
  // The camera end tests as NEE does; at the light end the camera vertex is the viewer, which only the facet bounds.
92
- If( cosCamera.lessThanEqual( 0.0 ).or( cosCameraFacet.lessThanEqual( 0.0 ) ).or( cosLightFacet.lessThanEqual( 0.0 ) ), () => {
94
+ // Through a surface that transmits diffusely, either end may also be reached from behind (checked below).
95
+ const inFront = cosCamera.greaterThan( 0.0 ).and( cosCameraFacet.greaterThan( 0.0 ) ).and( cosLightFacet.greaterThan( 0.0 ) );
96
+ const cameraBehind = cosCamera.lessThan( 0.0 ).and( cosCameraFacet.lessThan( 0.0 ) );
97
+ const lightBehind = cosLightFacet.lessThan( 0.0 );
98
+ const possible = diffuseTransmission
99
+ ? cameraBehind.or( cosCamera.greaterThan( 0.0 ).and( cosCameraFacet.greaterThan( 0.0 ) ) )
100
+ .and( lightBehind.or( cosLightFacet.greaterThan( 0.0 ) ) )
101
+ : inFront;
102
+ If( possible.not(), () => {
93
103
 
94
104
  Return();
95
105
 
@@ -98,19 +108,28 @@ export function buildConnectKernel( params ) {
98
108
  const cameraMaterial = resolveSurfaceMaterial( cam.materialIndex, cam.uv, cam.N, materialBuffer );
99
109
  const lightMaterial = resolveSurfaceMaterial( light.materialIndex, light.uv, light.N, materialBuffer );
100
110
  const toCamera = dir.negate();
111
+ if ( diffuseTransmission ) If( cameraBehind.and( cameraMaterial.diffuseTransmission.lessThanEqual( 0.0 ) )
112
+ .or( lightBehind.and( lightMaterial.diffuseTransmission.lessThanEqual( 0.0 ) ) ), () => {
101
113
 
114
+ Return();
115
+
116
+ } );
117
+
118
+ // Each end's BSDF and densities read in the frame with their own first direction on top.
119
+ const camN = ( toward ) => ( diffuseTransmission ? facingSide( cam.N, toward, cameraMaterial ) : cam.N );
120
+ const lightN = ( toward ) => ( diffuseTransmission ? facingSide( light.N, toward, lightMaterial ) : light.N );
102
121
  const fCamera = evaluateMaterialResponse( cam.V, dir, cam.N, cameraMaterial ).toVar();
103
- const fLight = evaluateMaterialResponse( toCamera, light.V, light.N, lightMaterial ).toVar();
104
- const geometry = cosCamera.mul( lightEndCosine( light.V, light.N, light.facetN, toCamera ) ).div( dist2 );
122
+ const fLight = evaluateMaterialResponse( toCamera, light.V, lightN( toCamera ), lightMaterial ).toVar();
123
+ const geometry = ( diffuseTransmission ? abs( cosCamera ) : cosCamera ).mul( lightEndCosine( light.V, light.N, light.facetN, toCamera ) ).div( dist2 );
105
124
 
106
125
  const cameraForward = calculateMaterialPDF( cam.V, dir, cam.N, cameraMaterial );
107
- const cameraReverse = calculateMaterialPDF( dir, cam.V, cam.N, cameraMaterial );
126
+ const cameraReverse = calculateMaterialPDF( dir, cam.V, camN( dir ), cameraMaterial );
108
127
  const lightForward = calculateMaterialPDF( light.V, toCamera, light.N, lightMaterial );
109
- const lightReverse = calculateMaterialPDF( toCamera, light.V, light.N, lightMaterial );
128
+ const lightReverse = calculateMaterialPDF( toCamera, light.V, lightN( toCamera ), lightMaterial );
110
129
 
111
130
  // Georgiev (40)-(41): each side's sum, from the other side's density of reaching it.
112
- const wLight = misPartial( cameraForward.mul( cosLightFacet ).div( dist2 ), light, lightReverse );
113
- const wCamera = misPartial( lightForward.mul( cosCameraFacet ).div( dist2 ), cam, cameraReverse );
131
+ const wLight = misPartial( cameraForward.mul( diffuseTransmission ? abs( cosLightFacet ) : cosLightFacet ).div( dist2 ), light, lightReverse );
132
+ const wCamera = misPartial( lightForward.mul( diffuseTransmission ? abs( cosCameraFacet ) : cosCameraFacet ).div( dist2 ), cam, cameraReverse );
114
133
 
115
134
  const scattering = cameraVertices.add( c ).add( int( 1 ) );
116
135
  // Alone, a path of k scattering vertices is reached by k − 1 connections.
@@ -123,8 +142,9 @@ export function buildConnectKernel( params ) {
123
142
 
124
143
  If( luminance( contribution ).greaterThan( 0.0 ), () => {
125
144
 
126
- const origin = offsetRayOrigin( cam.position, cam.facetN ).toVar();
127
- const segment = offsetRayOrigin( light.position, light.facetN ).sub( origin ).toVar();
145
+ // Each end of the shadow ray leaves from the side the connection takes.
146
+ const origin = offsetRayOrigin( cam.position, diffuseTransmission ? select( cameraBehind, cam.facetN.negate(), cam.facetN ) : cam.facetN ).toVar();
147
+ const segment = offsetRayOrigin( light.position, diffuseTransmission ? select( lightBehind, light.facetN.negate(), light.facetN ) : light.facetN ).sub( origin ).toVar();
128
148
  const segmentLength = length( segment ).toVar();
129
149
  const visibility = traceShadowRayRefractiveOpaque(
130
150
  origin, segment.div( segmentLength ), segmentLength.mul( SHADOW_END ),
@@ -5,7 +5,7 @@
5
5
 
6
6
  import {
7
7
  Fn, float, vec3, vec4, int, uint, If, instanceIndex, atomicAdd, atomicLoad, atomicStore, Return,
8
- dot, sqrt, max, min, floor, length, localId, workgroupId,
8
+ dot, sqrt, max, min, floor, length, localId, workgroupId, select, abs,
9
9
  } from 'three/tsl';
10
10
 
11
11
  import { evaluateMaterialResponse } from './MaterialEvaluation.js';
@@ -15,7 +15,7 @@ import { traverseBVHShadowCameraCulled } from './BVHTraversal.js';
15
15
  import { offsetRayOrigin, SHADOW_END, sanitizeRGB } from './Common.js';
16
16
  import { pcgHash } from './Random.js';
17
17
  import { regularizePathContribution } from './PathTracerCore.js';
18
- import { resolveSurfaceMaterial, lightEndCosine, misPartial, misWeight, strategyWeight, STRATEGY, SPLAT_SCALE, SPLAT_MAX } from './Bidirectional.js';
18
+ import { resolveSurfaceMaterial, lightEndCosine, misPartial, misWeight, strategyWeight, facingSide, STRATEGY, SPLAT_SCALE, SPLAT_MAX } from './Bidirectional.js';
19
19
  import {
20
20
  cachedVertex, readVertexRecord, readLightPathLength, readRayRadiance, writeRayRadiance,
21
21
  } from '../Processor/PackedRayBuffer.js';
@@ -33,6 +33,8 @@ export function buildLightSplatKernel( params ) {
33
33
  renderWidth, renderHeight,
34
34
  globalIlluminationIntensity, fireflyThreshold, accumFrame, frame,
35
35
  mergeVm = null,
36
+ // Some material passes light through diffusely: the camera may then see a light vertex from behind its surface.
37
+ diffuseTransmission = true,
36
38
  } = params;
37
39
 
38
40
  return Fn( () => {
@@ -60,8 +62,13 @@ export function buildLightSplatKernel( params ) {
60
62
  const dir = toCamera.div( sqrt( dist2 ) ).toVar();
61
63
  const cosFacet = dot( dir, v.facetN ).toVar();
62
64
  const cosCamera = dot( dir, cameraForward ).negate().toVar();
63
- // The camera is this vertex's viewer: like a camera path, only the facet decides which side it is on.
64
- If( cosFacet.lessThanEqual( 0.0 ).or( cosCamera.lessThanEqual( 1e-6 ) ).or( v.extra.notEqual( uint( 0 ) ) ), () => {
65
+ // The camera is this vertex's viewer: like a camera path, only the facet decides which side it is on. Behind the
66
+ // surface (only through diffuse transmission, checked below) the cull flag is the far side's, bit 1.
67
+ const behind = cosFacet.lessThan( 0.0 );
68
+ const culled = diffuseTransmission
69
+ ? select( behind, v.extra.bitAnd( uint( 2 ) ), v.extra.bitAnd( uint( 1 ) ) ).notEqual( uint( 0 ) )
70
+ : v.extra.notEqual( uint( 0 ) );
71
+ If( ( diffuseTransmission ? cosFacet.equal( 0.0 ) : cosFacet.lessThanEqual( 0.0 ) ).or( cosCamera.lessThanEqual( 1e-6 ) ).or( culled ), () => {
65
72
 
66
73
  Return();
67
74
 
@@ -83,14 +90,22 @@ export function buildLightSplatKernel( params ) {
83
90
  } );
84
91
 
85
92
  const material = resolveSurfaceMaterial( v.materialIndex, v.uv, v.N, materialBuffer );
86
- const f = evaluateMaterialResponse( dir, v.V, v.N, material ).toVar();
93
+ if ( diffuseTransmission ) If( behind.and( material.diffuseTransmission.lessThanEqual( 0.0 ) ), () => {
94
+
95
+ Return();
96
+
97
+ } );
98
+
99
+ // Read in the frame with the camera on top.
100
+ const viewN = diffuseTransmission ? facingSide( v.N, dir, material ) : v.N;
101
+ const f = evaluateMaterialResponse( dir, v.V, viewN, material ).toVar();
87
102
 
88
103
  // The pinhole's solid-angle density for a uniform point in a pixel: 1 / (A_pixel cos³θ).
89
104
  const cameraPdfW = float( 1.0 ).div( pixelArea.mul( cosCamera ).mul( cosCamera ).mul( cosCamera ) );
90
105
  const toSurface = cameraPdfW.div( dist2 ).toVar();
91
106
 
92
107
  // Georgiev (46); the camera side has no sum of its own.
93
- const wLight = misPartial( toSurface.mul( cosFacet ).div( float( lightPaths ) ), v, calculateMaterialPDF( dir, v.V, v.N, material ) );
108
+ const wLight = misPartial( toSurface.mul( diffuseTransmission ? abs( cosFacet ) : cosFacet ).div( float( lightPaths ) ), v, calculateMaterialPDF( dir, v.V, viewN, material ) );
94
109
  const bounces = c.add( uint( 1 ) );
95
110
  const gi = float( 1.0 ).toVar();
96
111
  If( bounces.greaterThan( uint( 1 ) ), () => {
@@ -105,7 +120,7 @@ export function buildLightSplatKernel( params ) {
105
120
  float( c ), fireflyThreshold, int( accumFrame ),
106
121
  ).toVar();
107
122
 
108
- const origin = offsetRayOrigin( v.position, v.facetN ).toVar();
123
+ const origin = offsetRayOrigin( v.position, diffuseTransmission ? select( behind, v.facetN.negate(), v.facetN ) : v.facetN ).toVar();
109
124
  const segment = cameraPosition.sub( origin ).toVar();
110
125
  const segmentLength = length( segment ).toVar();
111
126
  // This segment stands in for the primary ray, which sees through the faces it culls.
@@ -12,14 +12,14 @@
12
12
 
13
13
  import {
14
14
  Fn, float, vec3, vec4, int, uint, ivec3, If, Loop, instanceIndex, atomicLoad, atomicStore, atomicFunc, Return, dot, floor, max, min, select,
15
- length, log2, exp2, Break,
15
+ length, log2, exp2, Break, abs,
16
16
  } from 'three/tsl';
17
17
 
18
18
  import { evaluateMaterialResponse } from './MaterialEvaluation.js';
19
19
  import { calculateMaterialPDF } from './LightsSampling.js';
20
20
  import { luminance } from './Common.js';
21
21
  import { regularizePathContribution } from './PathTracerCore.js';
22
- import { resolveSurfaceMaterial, misMergePartial, misWeight, strategyWeight, mergeRadiusAt, mergeEta, STRATEGY } from './Bidirectional.js';
22
+ import { resolveSurfaceMaterial, misMergePartial, misWeight, strategyWeight, mergeRadiusAt, mergeEta, facingSide, STRATEGY } from './Bidirectional.js';
23
23
  import { COUNTER } from '../Processor/QueueManager.js';
24
24
  import {
25
25
  readRayRadiance, writeRayRadiance, pendingVertex, cachedVertex, readVertexTag, readVertexRecord, readVertexPosition,
@@ -105,6 +105,8 @@ export function buildMergeKernel( params ) {
105
105
  const {
106
106
  rayBufferRW, hitBufferRO, activeIndicesRO, counters, head, materialBuffer, bidirectional: bdpt,
107
107
  maxBounceCount, globalIlluminationIntensity, fireflyThreshold, accumFrame,
108
+ // Some material passes light through diffusely: light arriving from behind its surface merges too.
109
+ diffuseTransmission = true,
108
110
  } = params;
109
111
  const { passTag, slotsPerPath, strategyView, hashMask } = bdpt;
110
112
 
@@ -173,19 +175,23 @@ export function buildMergeKernel( params ) {
173
175
  const light = readVertexRecord( hitBufferRO, at );
174
176
  const cosShading = dot( light.V, cam.N ).toVar();
175
177
  const cosFacet = dot( light.V, cam.facetN ).toVar();
176
- // Light arriving on this side. A corner's other wall stays in: its light vertices stand in for the
177
- // part of the sphere past the corner, which the density divides by too.
178
- If( cosShading.greaterThan( 0.0 ).and( cosFacet.greaterThan( 0.0 ) ), () => {
178
+ // Light arriving on this side, or from behind a surface that passes it through diffusely. A corner's
179
+ // other wall stays in: its light vertices stand in for the part of the sphere past the corner, which
180
+ // the density divides by too.
181
+ const onThisSide = cosShading.greaterThan( 0.0 ).and( cosFacet.greaterThan( 0.0 ) );
182
+ const throughSurface = material.diffuseTransmission.greaterThan( 0.0 ).and( cosShading.lessThan( 0.0 ) ).and( cosFacet.lessThan( 0.0 ) );
183
+ If( diffuseTransmission ? onThisSide.or( throughSurface ) : onThisSide, () => {
179
184
 
180
185
  const f = evaluateMaterialResponse( cam.V, light.V, cam.N, material );
181
186
  const wLight = misMergePartial( light, calculateMaterialPDF( cam.V, light.V, cam.N, material ), vc );
182
- const wCamera = misMergePartial( cam, calculateMaterialPDF( light.V, cam.V, cam.N, material ), vc );
187
+ const reverseN = diffuseTransmission ? facingSide( cam.N, light.V, material ) : cam.N;
188
+ const wCamera = misMergePartial( cam, calculateMaterialPDF( light.V, cam.V, reverseN, material ), vc );
183
189
  const scattering = cameraDepth.add( l );
184
190
  // Alone, a path of k scattering vertices is reached by k merges.
185
191
  const weight = strategyWeight( strategyView, STRATEGY.MERGE, misWeight( wLight, wCamera ), float( 1.0 ).div( float( scattering ) ) );
186
192
  const gi = select( scattering.greaterThan( int( 1 ) ), globalIlluminationIntensity, float( 1.0 ) );
187
193
  // The photon's flux is per unit area; the BSDF wants it per projected solid angle (Veach 5.3.2).
188
- const correction = cosShading.div( max( cosFacet, 1e-4 ) );
194
+ const correction = diffuseTransmission ? abs( cosShading ).div( max( abs( cosFacet ), 1e-4 ) ) : cosShading.div( max( cosFacet, 1e-4 ) );
189
195
  gathered.addAssign( regularizePathContribution(
190
196
  cam.throughput.mul( f ).mul( light.throughput ).mul( weight.mul( correction ).div( eta ).mul( gi ) ),
191
197
  float( scattering.sub( int( 1 ) ) ), fireflyThreshold, int( accumFrame ),