rayzee 8.8.1 → 9.0.0

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/src/Stages/NRD.js CHANGED
@@ -6,7 +6,7 @@ import { DataTexture, HalfFloatType, FloatType, RGBAFormat, LinearFilter, Matrix
6
6
  import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
7
7
  import { createStorageTexture } from '../Processor/StorageTexturePool.js';
8
8
  import { sanitizeRGB, sanitize1, FP16_MAX } from '../TSL/Common.js';
9
- import { cameraRayDirection } from '../TSL/CameraRay.js';
9
+ import { cameraRayPoint, cameraRayUniforms, isOrthographic } from '../TSL/CameraRay.js';
10
10
  import {
11
11
  ALBEDO_EPS, GBUFFER_MISS_THRESHOLD as MISS_THRESHOLD, NRD_DEFAULTS,
12
12
  NRD_HIT_DIST_A, NRD_HIT_DIST_B,
@@ -168,10 +168,10 @@ export class NRD extends RenderStage {
168
168
  this.prevCamView = uniform( new Matrix4(), 'mat4' );
169
169
  this.camPos = uniform( new Vector3(), 'vec3' );
170
170
  this.prevCamPos = uniform( new Vector3(), 'vec3' );
171
- this.cameraProjection = uniform( 0, 'int' );
172
- this.panoLonRange = uniform( new Vector2(), 'vec2' );
173
- this.panoLatRange = uniform( new Vector2(), 'vec2' );
174
- this.panoLevelHorizon = uniform( 1, 'int' );
171
+ // Surface to camera while orthographic, where it is the same for every pixel.
172
+ this.viewVector = uniform( new Vector3(), 'vec3' );
173
+ this.prevViewVector = uniform( new Vector3(), 'vec3' );
174
+ this.cameraRay = cameraRayUniforms();
175
175
 
176
176
  // Declared here, valued by _syncUniforms below — which is the only place a setting becomes a
177
177
  // uniform, so there is no second copy of the defaults to keep in step.
@@ -322,11 +322,20 @@ export class NRD extends RenderStage {
322
322
  // Same ray the depth was measured along — NormalDepth generates it with this helper too.
323
323
  _worldPosAt( uv, dist, cwm, cpi ) {
324
324
 
325
- const dir = cameraRayDirection(
326
- uv, cwm, cpi,
327
- this.cameraProjection, this.panoLonRange, this.panoLatRange, this.panoLevelHorizon
328
- );
329
- return vec3( cwm[ 3 ] ).add( dir.mul( dist ) );
325
+ return cameraRayPoint( uv, dist, cwm, cpi, this.cameraRay );
326
+
327
+ }
328
+
329
+ // NRD's gOrthoMode: a constant view vector, and a pixel's footprint that no longer grows with depth.
330
+ _viewVector( X, camPos, orthoV ) {
331
+
332
+ return select( isOrthographic( this.cameraRay.cameraProjection ), orthoV, normalize( camPos.sub( X ) ) );
333
+
334
+ }
335
+
336
+ _frustumSize( viewZ ) {
337
+
338
+ return this.minRectDimMulUnproject.mul( select( isOrthographic( this.cameraRay.cameraProjection ), float( 1.0 ), viewZ ) );
330
339
 
331
340
  }
332
341
 
@@ -547,9 +556,9 @@ export class NRD extends RenderStage {
547
556
 
548
557
  If( maxRadiusU.greaterThan( 0.0 ), () => {
549
558
 
550
- const V = normalize( camPos.sub( X ) );
559
+ const V = this._viewVector( X, camPos, this.viewVector );
551
560
  const NoV = dot( N, V ).abs().toVar();
552
- const frustumSize = this.minRectDimMulUnproject.mul( viewZ ).toVar();
561
+ const frustumSize = this._frustumSize( viewZ ).toVar();
553
562
 
554
563
  const k = inputFrames.add( 1.0 );
555
564
  const accumSpeedEff = isPre
@@ -701,9 +710,9 @@ export class NRD extends RenderStage {
701
710
  const pixelUv = this._pixelUv( gx, gy );
702
711
  const X = this._worldPosAt( pixelUv, dist, cwm, cpi ).toVar();
703
712
  const viewZ = camView.mul( vec4( X, 1.0 ) ).z.abs().toVar();
704
- const V = normalize( camPos.sub( X ) );
713
+ const V = this._viewVector( X, camPos, this.viewVector );
705
714
  const NoV = dot( N, V ).abs().toVar();
706
- const frustumSize = this.minRectDimMulUnproject.mul( viewZ ).toVar();
715
+ const frustumSize = this._frustumSize( viewZ ).toVar();
707
716
 
708
717
  // Unnormalized, as NRD requires — its length carries the normal variance. A miss neighbour's
709
718
  // shadingNormal is a literal (0,0,0), which decodes to a fixed fake direction, so substitute
@@ -753,7 +762,7 @@ export class NRD extends RenderStage {
753
762
 
754
763
  const accumSpeed = this._applyCustomWeights( prevAccum, occW, float( 0.0 ) ).toVar();
755
764
 
756
- const Vprev = normalize( prevCamPos.sub( X ) );
765
+ const Vprev = this._viewVector( X, prevCamPos, this.prevViewVector );
757
766
  const NoVprev = dot( N, Vprev ).abs();
758
767
  const sizeQuality = NoVprev.add( 1e-3 ).div( NoV.add( 1e-3 ) ).toVar();
759
768
  sizeQuality.assign( mix( float( 0.1 ), float( 1.0 ), saturate( sizeQuality.mul( sizeQuality ) ) ) );
@@ -853,7 +862,7 @@ export class NRD extends RenderStage {
853
862
  const pixelUv = this._pixelUv( gx, gy );
854
863
  const X = this._worldPosAt( pixelUv, dist, cwm, cpi ).toVar();
855
864
  const viewZ = camView.mul( vec4( X, 1.0 ) ).z.abs();
856
- const frustumSize = this.minRectDimMulUnproject.mul( viewZ ).toVar();
865
+ const frustumSize = this._frustumSize( viewZ ).toVar();
857
866
 
858
867
  const diff = textureLoad( inTex, coord ).toVar();
859
868
  const nonLinear = float( 1.0 ).div( frameNumEff.add( 1.0 ) ).toVar();
@@ -1200,6 +1209,7 @@ export class NRD extends RenderStage {
1200
1209
 
1201
1210
  this.prevCamView.value.copy( this.camView.value );
1202
1211
  this.prevCamPos.value.copy( this.camPos.value );
1212
+ this.prevViewVector.value.copy( this.viewVector.value );
1203
1213
 
1204
1214
  }
1205
1215
 
@@ -1207,20 +1217,19 @@ export class NRD extends RenderStage {
1207
1217
  this.camProjInv.value.copy( projInv );
1208
1218
  this.camView.value.copy( view );
1209
1219
  this.camPos.value.setFromMatrixPosition( world );
1210
- this.cameraProjection.value = pt.uniforms.get( 'cameraProjection' ).value;
1211
- this.panoLonRange.value.copy( pt.uniforms.get( 'panoLonRange' ).value );
1212
- this.panoLatRange.value.copy( pt.uniforms.get( 'panoLatRange' ).value );
1213
- this.panoLevelHorizon.value = pt.uniforms.get( 'panoLevelHorizon' ).value;
1220
+ this.viewVector.value.setFromMatrixColumn( world, 2 ).normalize();
1221
+ this.cameraRay.sync( pt.uniforms );
1214
1222
 
1215
1223
  if ( ! this._camInitialized ) {
1216
1224
 
1217
1225
  this.prevCamView.value.copy( view );
1218
1226
  this.prevCamPos.value.copy( this.camPos.value );
1227
+ this.prevViewVector.value.copy( this.viewVector.value );
1219
1228
  this._camInitialized = true;
1220
1229
 
1221
1230
  }
1222
1231
 
1223
- // NRD gUnproject; frustum size follows the shorter edge.
1232
+ // NRD gUnproject; frustum size follows the shorter edge. Orthographic, it is world units per pixel.
1224
1233
  const projY = proj.elements[ 5 ] || 1;
1225
1234
  const unproject = 1 / ( 0.5 * this.resH.value * projY );
1226
1235
  this.minRectDimMulUnproject.value = Math.min( this.resW.value, this.resH.value ) * unproject;
@@ -1,4 +1,4 @@
1
- import { Fn, vec2, vec3, vec4, float, int, uint, uvec2, uvec4, uniform, min, storage, If,
1
+ import { Fn, vec2, vec4, float, int, uint, uvec2, uvec4, uniform, min, storage, If,
2
2
  textureStore, workgroupId, localId } from 'three/tsl';
3
3
  import { RenderTarget, StorageTexture } from 'three/webgpu';
4
4
  import { HalfFloatType, RGBAFormat, RedIntegerFormat, UnsignedIntType, NearestFilter, Matrix4, Box2, Vector2 } from 'three';
@@ -6,7 +6,7 @@ import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
6
6
  import { MAX_STORAGE_TEXTURE_SIZE } from '../EngineDefaults.js';
7
7
  import { Ray, HitInfo, RayTracingMaterial, UVCache } from '../TSL/Struct.js';
8
8
  import { traverseBVH } from '../TSL/BVHTraversal.js';
9
- import { cameraRayDirection } from '../TSL/CameraRay.js';
9
+ import { cameraRayOf, cameraRayUniforms } from '../TSL/CameraRay.js';
10
10
  import { getMaterial } from '../TSL/Common.js';
11
11
  import { computeUVCache, processNormal, processBump, processMetalnessRoughness, triangleUVTangent, buildBucketTextureNodes, refreshBucketTextureNodes, setMaterialBucketTextures } from '../TSL/TextureSampling.js';
12
12
 
@@ -53,10 +53,7 @@ export class NormalDepth extends RenderStage {
53
53
  this.cameraProjectionMatrixInverse = uniform( new Matrix4(), 'mat4' );
54
54
  // Mirrored from the path tracer each frame — these rays must use the same camera model
55
55
  // as the colour buffer or the denoiser's normal/depth edge-stops fight the image.
56
- this.cameraProjection = uniform( 0, 'int' );
57
- this.panoLonRange = uniform( new Vector2(), 'vec2' );
58
- this.panoLatRange = uniform( new Vector2(), 'vec2' );
59
- this.panoLevelHorizon = uniform( 1, 'int' );
56
+ this.cameraRay = cameraRayUniforms();
60
57
  this.resolutionWidth = uniform( options.width || 1 );
61
58
  this.resolutionHeight = uniform( options.height || 1 );
62
59
 
@@ -236,13 +233,7 @@ export class NormalDepth extends RenderStage {
236
233
  // No jitter — deterministic per-pixel ray so the temporal gate
237
234
  // sees stable per-pixel normals across frames.
238
235
  const uv = vec2( float( gx ).add( 0.5 ).div( resW ), float( gy ).add( 0.5 ).div( resH ) );
239
- const rayDirWorld = cameraRayDirection(
240
- uv, camWorldMat, camProjInvMat,
241
- this.cameraProjection, this.panoLonRange, this.panoLatRange, this.panoLevelHorizon
242
- );
243
- const rayOrigin = vec3( camWorldMat[ 3 ] );
244
-
245
- const ray = Ray( { origin: rayOrigin, direction: rayDirWorld } );
236
+ const ray = Ray( cameraRayOf( uv, camWorldMat, camProjInvMat, this.cameraRay ) );
246
237
  const hit = HitInfo.wrap( traverseBVH( ray, bvhStorage, triStorage ) );
247
238
 
248
239
  const encodedNormal = hit.normal.mul( 0.5 ).add( 0.5 );
@@ -339,10 +330,7 @@ export class NormalDepth extends RenderStage {
339
330
 
340
331
  this.cameraWorldMatrix.value.copy( pt.uniforms.get( 'cameraWorldMatrix' ).value );
341
332
  this.cameraProjectionMatrixInverse.value.copy( pt.uniforms.get( 'cameraProjectionMatrixInverse' ).value );
342
- this.cameraProjection.value = pt.uniforms.get( 'cameraProjection' ).value;
343
- this.panoLonRange.value.copy( pt.uniforms.get( 'panoLonRange' ).value );
344
- this.panoLatRange.value.copy( pt.uniforms.get( 'panoLatRange' ).value );
345
- this.panoLevelHorizon.value = pt.uniforms.get( 'panoLevelHorizon' ).value;
333
+ this.cameraRay.sync( pt.uniforms );
346
334
 
347
335
  }
348
336
 
@@ -1163,9 +1163,11 @@ export class PathTracer extends PathTracerStage {
1163
1163
  focalLength: this.focalLength,
1164
1164
  aperture: this.aperture,
1165
1165
  focusDistance: this.focusDistance,
1166
- sceneScale: this.sceneScale,
1166
+ unitsPerMetre: this.unitsPerMetre,
1167
1167
  apertureScale: this.apertureScale,
1168
1168
  anamorphicRatio: this.anamorphicRatio,
1169
+ dofMode: this.dofMode,
1170
+ dofBlur: this.dofBlur,
1169
1171
  renderWidth: this._wfRenderWidth,
1170
1172
  renderHeight: this._wfRenderHeight,
1171
1173
  chunkRowBase: this._wfChunkRowBase,
@@ -1643,9 +1645,11 @@ export class PathTracer extends PathTracerStage {
1643
1645
  focalLength: this.focalLength,
1644
1646
  aperture: this.aperture,
1645
1647
  focusDistance: this.focusDistance,
1646
- sceneScale: this.sceneScale,
1648
+ unitsPerMetre: this.unitsPerMetre,
1647
1649
  apertureScale: this.apertureScale,
1648
1650
  anamorphicRatio: this.anamorphicRatio,
1651
+ dofMode: this.dofMode,
1652
+ dofBlur: this.dofBlur,
1649
1653
  bvhBuffer: freshBvh,
1650
1654
  triangleBuffer: freshTri,
1651
1655
  materialBuffer: freshMat,
@@ -1013,6 +1013,8 @@ export class PathTracerStage extends RenderStage {
1013
1013
 
1014
1014
  if ( ! attr ) return;
1015
1015
  attr.array.set( data );
1016
+ // A pending update range would cut this full upload down to that range.
1017
+ attr.clearUpdateRanges();
1016
1018
  attr.needsUpdate = true;
1017
1019
 
1018
1020
  }
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * CameraRay.js — primary camera ray generation.
3
3
  *
4
- * Two projections behind one `cameraProjection` uniform: 0 = pinhole (NDC through
5
- * cameraProjectionMatrixInverse), 1 = equirectangular 360 panorama. Both branches live in the same
6
- * kernel, so switching modes writes a uniform and resets accumulation — it never recompiles WGSL.
4
+ * Three projections behind one `cameraProjection` uniform (CAMERA_PROJECTION_IDS): pinhole (NDC through
5
+ * cameraProjectionMatrixInverse), equirectangular 360 panorama, and orthographic (parallel rays from the
6
+ * camera's image plane). All live in the same kernel, so switching modes writes a uniform and resets
7
+ * accumulation — it never recompiles WGSL.
7
8
  *
8
9
  * Equirect mapping, for uv ∈ [0,1] with v = 0 at the top row:
9
10
  * lon = mix( lonMin, lonMax, u ), lat = mix( latMax, latMin, v )
@@ -12,15 +13,81 @@
12
13
  */
13
14
 
14
15
  import {
15
- Fn, vec3, vec4, float, int,
16
- If, normalize, mat3, mix, sin, cos, cross, dot,
16
+ Fn, vec3, vec4, float, int, uniform,
17
+ If, normalize, mat3, mix, sin, cos, cross, dot, select,
17
18
  } from 'three/tsl';
19
+ import { Vector2 } from 'three';
18
20
 
19
21
  import { Ray } from './Struct.js';
20
22
  import { constructTBN } from './Common.js';
21
23
  import { RandomPointInCircle } from './Random.js';
24
+ import { CAMERA_PROJECTION_IDS } from '../EngineDefaults.js';
22
25
 
23
- /** World-space primary ray direction for pixel uv. Origin is always cameraWorldMatrix[3]. */
26
+ const PANORAMA = int( CAMERA_PROJECTION_IDS.equirectangular );
27
+ const ORTHOGRAPHIC = int( CAMERA_PROJECTION_IDS.orthographic );
28
+
29
+ export const isOrthographic = cameraProjection => cameraProjection.equal( ORTHOGRAPHIC );
30
+
31
+ /**
32
+ * The projection uniforms a stage re-tracing the path tracer's primary rays needs. `sync` copies the path
33
+ * tracer's, once a frame.
34
+ */
35
+ export function cameraRayUniforms() {
36
+
37
+ const u = {
38
+ cameraProjection: uniform( 0, 'int' ),
39
+ panoLonRange: uniform( new Vector2(), 'vec2' ),
40
+ panoLatRange: uniform( new Vector2(), 'vec2' ),
41
+ panoLevelHorizon: uniform( 1, 'int' ),
42
+ /** @param {import('../managers/UniformManager.js').UniformManager} source */
43
+ sync( source ) {
44
+
45
+ u.cameraProjection.value = source.get( 'cameraProjection' ).value;
46
+ u.panoLonRange.value.copy( source.get( 'panoLonRange' ).value );
47
+ u.panoLatRange.value.copy( source.get( 'panoLatRange' ).value );
48
+ u.panoLevelHorizon.value = source.get( 'panoLevelHorizon' ).value;
49
+
50
+ },
51
+ };
52
+ return u;
53
+
54
+ }
55
+
56
+ /** Pixel uv's primary ray, for a stage holding {@link cameraRayUniforms}. */
57
+ export const cameraRayOf = ( uv01, cameraWorldMatrix, cameraProjectionMatrixInverse, u ) => ( {
58
+ origin: cameraRayOrigin( uv01, cameraWorldMatrix, cameraProjectionMatrixInverse, u.cameraProjection ),
59
+ direction: cameraRayDirection(
60
+ uv01, cameraWorldMatrix, cameraProjectionMatrixInverse,
61
+ u.cameraProjection, u.panoLonRange, u.panoLatRange, u.panoLevelHorizon
62
+ ),
63
+ } );
64
+
65
+ /** The point `distance` along pixel uv's primary ray — where a depth the path tracer measured lies. */
66
+ export const cameraRayPoint = ( uv01, distance, cameraWorldMatrix, cameraProjectionMatrixInverse, u ) => {
67
+
68
+ const { origin, direction } = cameraRayOf( uv01, cameraWorldMatrix, cameraProjectionMatrixInverse, u );
69
+ return origin.add( direction.mul( distance ) );
70
+
71
+ };
72
+
73
+ /** World-space primary ray origin for pixel uv: the camera, or its image plane when orthographic. */
74
+ export const cameraRayOrigin = Fn( ( [ uv01, cameraWorldMatrix, cameraProjectionMatrixInverse, cameraProjection ] ) => {
75
+
76
+ const origin = vec3( cameraWorldMatrix[ 3 ] ).toVar();
77
+
78
+ If( isOrthographic( cameraProjection ), () => {
79
+
80
+ // An orthographic inverse maps x and y alone, whatever the depth.
81
+ const onPlane = cameraProjectionMatrixInverse.mul( vec4( uv01.x.mul( 2.0 ).sub( 1.0 ), float( 1.0 ).sub( uv01.y.mul( 2.0 ) ), 0.0, 1.0 ) );
82
+ origin.assign( cameraWorldMatrix.mul( vec4( onPlane.xy, 0.0, 1.0 ) ).xyz );
83
+
84
+ } );
85
+
86
+ return origin;
87
+
88
+ } );
89
+
90
+ /** World-space primary ray direction for pixel uv, from {@link cameraRayOrigin}. */
24
91
  export const cameraRayDirection = Fn( ( [
25
92
  uv01, cameraWorldMatrix, cameraProjectionMatrixInverse,
26
93
  cameraProjection, panoLonRange, panoLatRange, panoLevelHorizon
@@ -28,7 +95,7 @@ export const cameraRayDirection = Fn( ( [
28
95
 
29
96
  const direction = vec3( 0.0 ).toVar();
30
97
 
31
- If( cameraProjection.equal( int( 1 ) ), () => {
98
+ If( cameraProjection.equal( PANORAMA ), () => {
32
99
 
33
100
  const lon = mix( panoLonRange.x, panoLonRange.y, uv01.x ).toVar();
34
101
  const lat = mix( panoLatRange.y, panoLatRange.x, uv01.y ).toVar();
@@ -68,6 +135,10 @@ export const cameraRayDirection = Fn( ( [
68
135
  .sub( back.mul( cos( lon ).mul( cosLat ) ) )
69
136
  ) );
70
137
 
138
+ } ).ElseIf( isOrthographic( cameraProjection ), () => {
139
+
140
+ direction.assign( normalize( vec3( cameraWorldMatrix[ 2 ] ) ).negate() );
141
+
71
142
  } ).Else( () => {
72
143
 
73
144
  const ndcPos = vec3( uv01.x.mul( 2.0 ).sub( 1.0 ), float( 1.0 ).sub( uv01.y.mul( 2.0 ) ), 1.0 );
@@ -89,10 +160,10 @@ export const generateRayFromCamera = Fn( ( [
89
160
  uv01, rngState,
90
161
  cameraWorldMatrix, cameraProjectionMatrixInverse,
91
162
  cameraProjection, panoLonRange, panoLatRange, panoLevelHorizon,
92
- enableDOF, focalLength, aperture, focusDistance, sceneScale, apertureScale, anamorphicRatio
163
+ enableDOF, focalLength, aperture, focusDistance, unitsPerMetre, apertureScale, anamorphicRatio, dofMode, dofBlur
93
164
  ] ) => {
94
165
 
95
- const rayOriginWorld = vec3( cameraWorldMatrix[ 3 ] ).toVar();
166
+ const rayOriginWorld = cameraRayOrigin( uv01, cameraWorldMatrix, cameraProjectionMatrixInverse, cameraProjection ).toVar();
96
167
  // .toVar() so the two reads below don't inline the whole projection graph twice.
97
168
  const rayDirectionWorld = cameraRayDirection(
98
169
  uv01, cameraWorldMatrix, cameraProjectionMatrixInverse,
@@ -102,10 +173,20 @@ export const generateRayFromCamera = Fn( ( [
102
173
  const resultOrigin = rayOriginWorld.toVar();
103
174
  const resultDirection = rayDirectionWorld.toVar();
104
175
 
105
- If( enableDOF.and( focalLength.greaterThan( 0.0 ) ).and( aperture.lessThan( 64.0 ) ).and( focusDistance.greaterThan( 0.001 ) ), () => {
176
+ const lookMode = dofMode.equal( int( 1 ) );
177
+ const lensOpen = lookMode.and( dofBlur.greaterThan( 0.0 ) ).or( lookMode.not().and( focalLength.greaterThan( 0.0 ) ).and( aperture.lessThan( 64.0 ) ) );
106
178
 
107
- const effectiveAperture = focalLength.div( aperture );
108
- const apertureRadius = effectiveAperture.mul( 0.001 ).mul( sceneScale ).mul( apertureScale );
179
+ If( enableDOF.and( lensOpen ).and( focusDistance.greaterThan( 0.001 ) ), () => {
180
+
181
+ // Orthographic 1: the blur then does not change with where the camera stands.
182
+ const halfViewHeight = select( cameraProjection.equal( PANORAMA ), panoLatRange.y.sub( panoLatRange.x ).mul( 0.5 ),
183
+ select( isOrthographic( cameraProjection ), float( 1.0 ), cameraProjectionMatrixInverse[ 1 ].y ) );
184
+ const apertureRadius = select( lookMode,
185
+ // A far background blurs by dofBlur of the image height, whatever the scene's scale.
186
+ dofBlur.mul( focusDistance ).mul( halfViewHeight ),
187
+ // f/N is the aperture's diameter, not its radius.
188
+ focalLength.div( aperture.mul( 2.0 ) ).mul( 0.001 ).mul( unitsPerMetre ).mul( apertureScale )
189
+ );
109
190
 
110
191
  const randomPoint = RandomPointInCircle( rngState );
111
192
  // Anamorphic squeeze — stretch horizontally for oval bokeh
@@ -113,8 +194,9 @@ export const generateRayFromCamera = Fn( ( [
113
194
  const lensY = randomPoint.y;
114
195
 
115
196
  const lensOffset = vec3( 0.0 ).toVar();
197
+ const focusAlongRay = focusDistance.toVar();
116
198
 
117
- If( cameraProjection.equal( int( 1 ) ), () => {
199
+ If( cameraProjection.equal( PANORAMA ), () => {
118
200
 
119
201
  // Every pixel points a different way, so the lens plane is built around the ray. The
120
202
  // camera's right/up would skew bokeh into a slit away from the image centre.
@@ -126,10 +208,13 @@ export const generateRayFromCamera = Fn( ( [
126
208
  const camUp = normalize( vec3( cameraWorldMatrix[ 1 ] ) );
127
209
  lensOffset.assign( camRight.mul( lensX ).add( camUp.mul( lensY ) ) );
128
210
 
211
+ // A flat focal plane: focusDistance is depth along the view axis, not distance along the ray.
212
+ focusAlongRay.assign( focusDistance.div( dot( rayDirectionWorld, normalize( vec3( cameraWorldMatrix[ 2 ] ) ) ).negate() ) );
213
+
129
214
  } );
130
215
 
131
216
  resultOrigin.assign( rayOriginWorld.add( lensOffset.mul( apertureRadius ) ) );
132
- resultDirection.assign( normalize( rayOriginWorld.add( rayDirectionWorld.mul( focusDistance ) ).sub( resultOrigin ) ) );
217
+ resultDirection.assign( normalize( rayOriginWorld.add( rayDirectionWorld.mul( focusAlongRay ) ).sub( resultOrigin ) ) );
133
218
 
134
219
  } );
135
220
 
@@ -27,7 +27,7 @@ export function buildDebugKernel( params ) {
27
27
  resolution, renderWidth, renderHeight,
28
28
  cameraWorldMatrix, cameraProjectionMatrixInverse, cameraProjectionMatrix, cameraViewMatrix,
29
29
  cameraProjection, panoLonRange, panoLatRange, panoLevelHorizon,
30
- enableDOF, focalLength, aperture, focusDistance, sceneScale, apertureScale, anamorphicRatio,
30
+ enableDOF, focalLength, aperture, focusDistance, unitsPerMetre, apertureScale, anamorphicRatio, dofMode, dofBlur,
31
31
  bvhBuffer, triangleBuffer, materialBuffer,
32
32
  envTexture, environmentMatrix, environmentIntensity, enableEnvironmentLight,
33
33
  visMode, debugVisScale,
@@ -50,7 +50,7 @@ export function buildDebugKernel( params ) {
50
50
  pixelCoord.div( resolution ), seed,
51
51
  cameraWorldMatrix, cameraProjectionMatrixInverse,
52
52
  cameraProjection, panoLonRange, panoLatRange, panoLevelHorizon,
53
- enableDOF, focalLength, aperture, focusDistance, sceneScale, apertureScale, anamorphicRatio,
53
+ enableDOF, focalLength, aperture, focusDistance, unitsPerMetre, apertureScale, anamorphicRatio, dofMode, dofBlur,
54
54
  ) );
55
55
 
56
56
  const color = vec4( 1.0, 0.0, 1.0, 1.0 ).toVar();
@@ -40,7 +40,7 @@ export function buildGenerateKernel( params ) {
40
40
  resolution, frame,
41
41
  cameraWorldMatrix, cameraProjectionMatrixInverse,
42
42
  cameraProjection, panoLonRange, panoLatRange, panoLevelHorizon,
43
- enableDOF, focalLength, aperture, focusDistance, sceneScale, apertureScale, anamorphicRatio,
43
+ enableDOF, focalLength, aperture, focusDistance, unitsPerMetre, apertureScale, anamorphicRatio, dofMode, dofBlur,
44
44
  renderWidth,
45
45
  chunkRowBase, chunkRows, // row band offset (global first row) + row count for this chunk
46
46
  transmissiveBounces, // per-ray refraction budget (megakernel parity: PathTracerCore.js:606)
@@ -78,7 +78,7 @@ export function buildGenerateKernel( params ) {
78
78
  jitteredUV, seed,
79
79
  cameraWorldMatrix, cameraProjectionMatrixInverse,
80
80
  cameraProjection, panoLonRange, panoLatRange, panoLevelHorizon,
81
- enableDOF, focalLength, aperture, focusDistance, sceneScale, apertureScale, anamorphicRatio,
81
+ enableDOF, focalLength, aperture, focusDistance, unitsPerMetre, apertureScale, anamorphicRatio, dofMode, dofBlur,
82
82
  ) );
83
83
 
84
84
  writeRayOriginMeta( rayBufferRW, rayID, ray.origin, int( 0 ), int( 0 ) );
package/src/index.js CHANGED
@@ -96,6 +96,10 @@ export { TransformManager } from './managers/TransformManager.js';
96
96
  // Video rendering
97
97
  export { VideoRenderManager } from './managers/VideoRenderManager.js';
98
98
 
99
+ // Authored animation: keyframed tracks on one timeline (engine.timeline)
100
+ export { TimelineManager } from './managers/timeline/TimelineManager.js';
101
+ export { CameraTrack } from './managers/timeline/CameraTrack.js';
102
+
99
103
  // Interaction
100
104
  export { InteractionManager } from './managers/InteractionManager.js';
101
105