rayzee 8.8.2 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayzee",
3
- "version": "8.8.2",
3
+ "version": "9.0.0",
4
4
  "type": "module",
5
5
  "description": "Real-time WebGPU path tracing engine built on Three.js",
6
6
  "main": "dist/rayzee.umd.js",
@@ -14,6 +14,7 @@ export const RENDER_PROFILES = Object.freeze( {
14
14
  environmentRotation: 0.0, // degrees — the HDRI as authored, as Blender shows it
15
15
  toneMapping: 6, // AgXToneMapping
16
16
  saturation: 1.0, // no grade
17
+ dofMode: 'look', // depth of field set by how blurry it looks, not by a lens — right at any scene scale
17
18
  } ),
18
19
  physical: Object.freeze( {
19
20
  areaLightIntensityScale: 1.0,
@@ -22,6 +23,7 @@ export const RENDER_PROFILES = Object.freeze( {
22
23
  // engine's own corpus (shade 1.48 vs AgX 3.01 from identical radiance).
23
24
  toneMapping: 6,
24
25
  saturation: 1.0, // no grade
26
+ dofMode: 'physical',
25
27
  } ),
26
28
  } );
27
29
 
@@ -101,7 +103,7 @@ export const ENGINE_DEFAULTS = {
101
103
  skyMieAnisotropy: 0.76,
102
104
  skyPreset: 'clearMorning',
103
105
 
104
- // Camera projection — 'perspective' | 'equirectangular'. Panorama ranges are UI-facing degrees.
106
+ // Camera projection — 'perspective' | 'orthographic' | 'equirectangular'. Panorama ranges are UI-facing degrees.
105
107
  cameraProjection: 'perspective',
106
108
  panoramaLonRange: [ - 180, 180 ],
107
109
  panoramaLatRange: [ - 90, 90 ],
@@ -114,6 +116,9 @@ export const ENGINE_DEFAULTS = {
114
116
  focalLength: 50,
115
117
  apertureScale: 1.0,
116
118
  anamorphicRatio: 1.0,
119
+ unitsPerMetre: 1, // scene units per real metre; the lens is specified in mm
120
+ dofMode: RENDER_PROFILES.viewer.dofMode, // 'look' | 'physical'
121
+ dofBlur: 0.05, // look mode: a far background's blur, as a fraction of the image height
117
122
 
118
123
  // Auto-focus
119
124
  autoFocusMode: 'auto', // 'manual' | 'auto'
@@ -252,6 +257,9 @@ export const ENGINE_DEFAULTS = {
252
257
  autoExposureAdaptSpeedDark: 0.5,
253
258
  };
254
259
 
260
+ /** The `cameraProjection` uniform's value for each setting value. */
261
+ export const CAMERA_PROJECTION_IDS = Object.freeze( { perspective: 0, equirectangular: 1, orthographic: 2 } );
262
+
255
263
  // Ray distance NormalDepth writes on a miss. Max finite half-float, so miss−miss diffs stay 0
256
264
  // rather than Inf−Inf=NaN.
257
265
  export const GBUFFER_MISS_DEPTH = 65504.0;
@@ -474,44 +482,39 @@ export const CAMERA_PRESETS = {
474
482
  portrait: {
475
483
  name: "Portrait",
476
484
  description: "Shallow depth of field, background blur",
477
- fov: 45,
478
- focusDistance: 1.5,
485
+ dofBlur: 0.10,
479
486
  aperture: 1.4,
480
487
  focalLength: 135,
481
- apertureScale: 1.5
488
+ apertureScale: 1.0
482
489
  },
483
490
  landscape: {
484
491
  name: "Landscape",
485
492
  description: "Maximum depth of field, everything in focus",
486
- fov: 65,
487
- focusDistance: 10.0,
493
+ dofBlur: 0.001,
488
494
  aperture: 16.0,
489
495
  focalLength: 24,
490
496
  apertureScale: 0.5
491
497
  },
492
498
  macro: {
493
499
  name: "Macro",
494
- description: "Extreme close-up with thin focus plane",
495
- fov: 40,
496
- focusDistance: 0.3,
497
- aperture: 2.0,
500
+ description: "Very thin focus plane for close-ups",
501
+ dofBlur: 0.14,
502
+ aperture: 1.4,
498
503
  focalLength: 100,
499
504
  apertureScale: 2.0
500
505
  },
501
506
  product: {
502
507
  name: "Product",
503
508
  description: "Sharp detail with subtle background separation",
504
- fov: 50,
505
- focusDistance: 0.8,
509
+ dofBlur: 0.013,
506
510
  aperture: 2.8,
507
511
  focalLength: 85,
508
- apertureScale: 1.0
512
+ apertureScale: 0.4
509
513
  },
510
514
  architectural: {
511
515
  name: "Architectural",
512
- description: "Wide view with deep focus",
513
- fov: 75,
514
- focusDistance: 5.0,
516
+ description: "Deep focus for rooms and buildings",
517
+ dofBlur: 0.001,
515
518
  aperture: 11.0,
516
519
  focalLength: 16,
517
520
  apertureScale: 0.5
@@ -519,11 +522,10 @@ export const CAMERA_PRESETS = {
519
522
  cinematic: {
520
523
  name: "Cinematic",
521
524
  description: "Dramatic depth separation with anamorphic bokeh",
522
- fov: 35,
523
- focusDistance: 3.0,
525
+ dofBlur: 0.13,
524
526
  aperture: 1.4,
525
527
  focalLength: 200,
526
- apertureScale: 1.8,
528
+ apertureScale: 1.0,
527
529
  anamorphicRatio: 1.5
528
530
  }
529
531
  };
@@ -36,6 +36,7 @@ export const EngineEvents = {
36
36
 
37
37
  // Camera
38
38
  AUTO_FOCUS_UPDATED: 'engine:autoFocusUpdated',
39
+ ORTHO_HEIGHT_UPDATED: 'engine:orthoHeightUpdated',
39
40
  AUTO_EXPOSURE_UPDATED: 'engine:autoExposureUpdated',
40
41
  AF_POINT_PLACED: 'engine:afPointPlaced',
41
42
 
@@ -48,6 +49,9 @@ export const EngineEvents = {
48
49
  ANIMATION_STOPPED: 'engine:animationStopped',
49
50
  ANIMATION_FINISHED: 'engine:animationFinished',
50
51
 
52
+ // Timeline (authored keyframes)
53
+ TIMELINE_CHANGED: 'engine:timelineChanged',
54
+
51
55
  // Video rendering
52
56
  VIDEO_RENDER_PROGRESS: 'engine:videoRenderProgress',
53
57
  VIDEO_RENDER_COMPLETE: 'engine:videoRenderComplete',
@@ -22,10 +22,11 @@ export const HISTORY_DEFAULTS = Object.freeze( {
22
22
  } );
23
23
 
24
24
  const WG = 8;
25
- const PARAMS_BYTES = 288;
25
+ const PARAMS_BYTES = 304;
26
26
  const MIN_MOVED_CAPACITY = 16;
27
27
  const FLAG_CLEAN_AUX = 1;
28
28
  const FLAG_SPLIT_COPIES = 2;
29
+ const FLAG_ORTHOGRAPHIC = 4;
29
30
  const PICK_MOVED = 0x40000000;
30
31
 
31
32
  const COMMON_WGSL = /* wgsl */`
@@ -47,6 +48,7 @@ struct Params {
47
48
  prevCamPos: vec4<f32>,
48
49
  moved: vec4<u32>,
49
50
  movedLength: f32,
51
+ prevCamDir: vec4<f32>,
50
52
  };
51
53
 
52
54
  @group(0) @binding(0) var<uniform> P: Params;
@@ -104,9 +106,19 @@ fn main( @builtin(global_invocation_id) gid: vec3<u32> ) {
104
106
 
105
107
  let size = vec2<f32>( P.size );
106
108
  let uv = ( vec2<f32>( gid.xy ) + 0.5 ) / size;
107
- let rc = P.projInv * vec4<f32>( uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, 1.0, 1.0 );
108
- let dir = normalize( ( P.camWorld * vec4<f32>( rc.xyz / rc.w, 0.0 ) ).xyz );
109
- var wp = P.camWorld[ 3 ].xyz + dir * g.w;
109
+ // Depth is along the ray: from the camera, or from its image plane when orthographic.
110
+ let ortho = ( P.flags & ${ FLAG_ORTHOGRAPHIC }u ) != 0u;
111
+ let ndc = vec2<f32>( uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0 );
112
+ var origin = P.camWorld[ 3 ].xyz;
113
+ var dir: vec3<f32>;
114
+ if ( ortho ) {
115
+ origin = ( P.camWorld * vec4<f32>( ( P.projInv * vec4<f32>( ndc, 0.0, 1.0 ) ).xy, 0.0, 1.0 ) ).xyz;
116
+ dir = - normalize( P.camWorld[ 2 ].xyz );
117
+ } else {
118
+ let rc = P.projInv * vec4<f32>( ndc, 1.0, 1.0 );
119
+ dir = normalize( ( P.camWorld * vec4<f32>( rc.xyz / rc.w, 0.0 ) ).xyz );
120
+ }
121
+ var wp = origin + dir * g.w;
110
122
  var n = g.xyz * 2.0 - 1.0;
111
123
 
112
124
  // A moved object's surface is looked for where the object was.
@@ -132,7 +144,7 @@ fn main( @builtin(global_invocation_id) gid: vec3<u32> ) {
132
144
  let pos = vec2<f32>( clip.x / clip.w * 0.5 + 0.5, 0.5 - clip.y / clip.w * 0.5 ) * size - 0.5;
133
145
  let base = vec2<i32>( floor( pos ) );
134
146
  let f = fract( pos );
135
- let expected = length( wp - P.prevCamPos.xyz );
147
+ let expected = select( length( wp - P.prevCamPos.xyz ), dot( wp - P.prevCamPos.xyz, P.prevCamDir.xyz ), ortho );
136
148
 
137
149
  for ( var j = 0; j < 2; j++ ) {
138
150
  for ( var i = 0; i < 2; i++ ) {
@@ -306,6 +318,7 @@ export class OIDNTemporalHistory {
306
318
  this._paramF32 = new Float32Array( this._paramData );
307
319
  this._prevViewProj = new Float32Array( 16 );
308
320
  this._prevCamPos = new Float32Array( 4 );
321
+ this._prevCamDir = new Float32Array( 4 );
309
322
 
310
323
  }
311
324
 
@@ -325,7 +338,7 @@ export class OIDNTemporalHistory {
325
338
  /**
326
339
  * Blends the frame just traced (a restart: one sample) into the reprojected history.
327
340
  * @param {Object} src - GPUTextures { color, albedo, normal, geo, geoPrev, shading, leaf } and { width, height }
328
- * @param {{ world: Float32Array, projInv: Float32Array, viewProj: Float32Array }} camera - of that frame
341
+ * @param {{ world: Float32Array, projInv: Float32Array, viewProj: Float32Array, orthographic: boolean }} camera - of that frame
329
342
  * @param {?{ count: number, leaves: Uint32Array, toPrev: Float32Array }} [moved] - TLAS leaves
330
343
  * (ascending) that moved since the last frame, with each one's current→previous world matrix
331
344
  */
@@ -524,7 +537,7 @@ export class OIDNTemporalHistory {
524
537
  u[ 0 ] = this.width;
525
538
  u[ 1 ] = this.height;
526
539
  u[ 2 ] = reset ? 1 : 0;
527
- u[ 3 ] = ( s.cleanAux ? FLAG_CLEAN_AUX : 0 ) | ( s.splitCopies ? FLAG_SPLIT_COPIES : 0 );
540
+ u[ 3 ] = ( s.cleanAux ? FLAG_CLEAN_AUX : 0 ) | ( s.splitCopies ? FLAG_SPLIT_COPIES : 0 ) | ( camera.orthographic ? FLAG_ORTHOGRAPHIC : 0 );
528
541
  f[ 4 ] = s.maxLength;
529
542
  f[ 5 ] = s.smoothLength;
530
543
  f[ 6 ] = s.depthTolerance;
@@ -539,6 +552,7 @@ export class OIDNTemporalHistory {
539
552
  f.set( this._prevCamPos, 60 );
540
553
  u[ 64 ] = this._movedCount;
541
554
  f[ 68 ] = s.movedLength;
555
+ f.set( this._prevCamDir, 72 );
542
556
  this.device.queue.writeBuffer( this._paramsBuffer, 0, this._paramData );
543
557
 
544
558
  }
@@ -549,6 +563,10 @@ export class OIDNTemporalHistory {
549
563
  this._prevCamPos[ 0 ] = camera.world[ 12 ];
550
564
  this._prevCamPos[ 1 ] = camera.world[ 13 ];
551
565
  this._prevCamPos[ 2 ] = camera.world[ 14 ];
566
+ const back = Math.hypot( camera.world[ 8 ], camera.world[ 9 ], camera.world[ 10 ] );
567
+ this._prevCamDir[ 0 ] = - camera.world[ 8 ] / back;
568
+ this._prevCamDir[ 1 ] = - camera.world[ 9 ] / back;
569
+ this._prevCamDir[ 2 ] = - camera.world[ 10 ] / back;
552
570
 
553
571
  }
554
572
 
@@ -35,6 +35,7 @@ import { SceneProcessor } from './Processor/SceneProcessor.js';
35
35
  // Managers
36
36
  import { RenderSettings } from './RenderSettings.js';
37
37
  import { CameraManager } from './managers/CameraManager.js';
38
+ import { TimelineManager } from './managers/timeline/TimelineManager.js';
38
39
  import { LightManager } from './managers/LightManager.js';
39
40
  import { GoboManager } from './managers/GoboManager.js';
40
41
  import { IESManager } from './managers/IESManager.js';
@@ -181,6 +182,7 @@ export class PathTracerApp extends EventDispatcher {
181
182
  ...DEFAULT_STATE,
182
183
  environmentRotation: this._profile.environmentRotation,
183
184
  saturation: this._profile.saturation,
185
+ dofMode: this._profile.dofMode,
184
186
  },
185
187
  { issues: this._issues }
186
188
  );
@@ -214,6 +216,8 @@ export class PathTracerApp extends EventDispatcher {
214
216
  // ── Managers (direct public access) ──
215
217
  /** @type {CameraManager} */
216
218
  this.cameraManager = null;
219
+ /** @type {TimelineManager} Authored animation: keyframed tracks. */
220
+ this.timeline = null;
217
221
  /** @type {LightManager} */
218
222
  this.lightManager = null;
219
223
  /** @type {GoboManager} */
@@ -359,7 +363,8 @@ export class PathTracerApp extends EventDispatcher {
359
363
 
360
364
  }
361
365
 
362
- if ( this.cameraManager.controls ) this.cameraManager.controls.update();
366
+ this.timeline.update();
367
+ this.cameraManager.updateControls();
363
368
 
364
369
  this._applyPendingRenderScale();
365
370
 
@@ -638,6 +643,7 @@ export class PathTracerApp extends EventDispatcher {
638
643
  this.iesManager?.dispose();
639
644
  this.denoisingManager?.dispose();
640
645
  this.interactionManager?.dispose();
646
+ this.timeline?.dispose();
641
647
  this.cameraManager?.dispose();
642
648
 
643
649
  this.pipeline?.dispose();
@@ -2088,6 +2094,7 @@ export class PathTracerApp extends EventDispatcher {
2088
2094
  // refresh landing in the middle of that raced the renderer's own output pass.
2089
2095
  this.denoisingManager?.setCadenceSuspended( isProduction );
2090
2096
 
2097
+ this.timeline.stop();
2091
2098
  this.cameraManager.controls.enabled = ! isProduction;
2092
2099
 
2093
2100
  // Anything with a SETTING_ROUTES entry must go through settings, not setUniform: set() early-returns on
@@ -3390,6 +3397,7 @@ export class PathTracerApp extends EventDispatcher {
3390
3397
  _initCameraManager() {
3391
3398
 
3392
3399
  this.cameraManager = new CameraManager( this.canvas );
3400
+ this.timeline = new TimelineManager( { cameraManager: this.cameraManager, onReset: () => this.reset() } );
3393
3401
 
3394
3402
  }
3395
3403
 
@@ -3415,14 +3423,19 @@ export class PathTracerApp extends EventDispatcher {
3415
3423
  this.assetLoader.setRenderer( this.renderer );
3416
3424
  this.assetLoader.createFloorPlane();
3417
3425
 
3418
- this._addTrackedListener( this.cameraManager.controls, 'change', () => {
3426
+ const onCameraMoved = () => {
3419
3427
 
3420
3428
  this.needsReset = true;
3421
3429
  // Here rather than in render(), so the first frame of the move is already at the lower resolution.
3422
3430
  this.stages.pathTracer?.enterInteractionMode();
3423
3431
  this.wake();
3424
3432
 
3425
- } );
3433
+ };
3434
+
3435
+ this._addTrackedListener( this.cameraManager.controls, 'change', onCameraMoved );
3436
+ this._addTrackedListener( this.cameraManager.walkControls, 'change', onCameraMoved );
3437
+ // A held key moves the camera from the frame loop, which may be asleep.
3438
+ this._addTrackedListener( this.cameraManager.walkControls, 'start', () => this.wake() );
3426
3439
 
3427
3440
  }
3428
3441
 
@@ -3526,6 +3539,8 @@ export class PathTracerApp extends EventDispatcher {
3526
3539
  // Forward manager events → app events
3527
3540
  this._addTrackedListener( this.cameraManager, 'CameraSwitched', ( e ) => this.dispatchEvent( e ) );
3528
3541
  this._addTrackedListener( this.cameraManager, EngineEvents.AUTO_FOCUS_UPDATED, ( e ) => this.dispatchEvent( e ) );
3542
+ this._addTrackedListener( this.cameraManager, EngineEvents.ORTHO_HEIGHT_UPDATED, ( e ) => this.dispatchEvent( e ) );
3543
+ this._addTrackedListener( this.timeline, EngineEvents.TIMELINE_CHANGED, ( e ) => this.dispatchEvent( e ) );
3529
3544
 
3530
3545
  this._forwardEvents( this.denoisingManager, [
3531
3546
  EngineEvents.DENOISING_START, EngineEvents.DENOISING_END,
@@ -3566,9 +3581,7 @@ export class PathTracerApp extends EventDispatcher {
3566
3581
 
3567
3582
  // Auto-focus context — CameraManager stores it, reads it each frame
3568
3583
  this.cameraManager.initAutoFocus( {
3569
- meshScene: this.meshScene,
3570
3584
  assetLoader: this.assetLoader,
3571
- floorPlane: this.assetLoader.floorPlane,
3572
3585
  pathTracer: this.stages.pathTracer,
3573
3586
  settings: this.settings,
3574
3587
  softReset: () => this.reset( true ),
@@ -3582,12 +3595,12 @@ export class PathTracerApp extends EventDispatcher {
3582
3595
  resetCallback: () => this.reset(),
3583
3596
  reconcileCompletion: () => this._reconcileCompletion(),
3584
3597
  denoisingManager: this.denoisingManager,
3585
- cameraManager: this.cameraManager,
3586
3598
  onInteractionRenderScale: () => {
3587
3599
 
3588
3600
  if ( this.stages.pathTracer?.interactionMode ) this._requestRenderScale( this._interactionRenderScale() );
3589
3601
 
3590
3602
  },
3603
+ onCameraProjection: ( value ) => this.cameraManager.applyProjection( value ),
3591
3604
  } );
3592
3605
 
3593
3606
  this.renderer.toneMappingExposure = this.settings.get( 'exposure' ) ?? 1.0;
@@ -3643,6 +3656,8 @@ export class PathTracerApp extends EventDispatcher {
3643
3656
 
3644
3657
  const cameras = [ this.cameraManager.camera, ...( event.cameras || [] ) ];
3645
3658
  this.cameraManager.setCameras( cameras );
3659
+ // Keys are poses in the scene being replaced, like the cameras saved in it.
3660
+ this.timeline.clear();
3646
3661
 
3647
3662
  if ( this.interactionManager ) {
3648
3663
 
@@ -3821,8 +3836,10 @@ export class PathTracerApp extends EventDispatcher {
3821
3836
 
3822
3837
  _syncControlsAfterLoad() {
3823
3838
 
3839
+ this.cameraManager.fitOrthographic();
3824
3840
  this.cameraManager.controls.saveState();
3825
3841
  this.cameraManager.controls.update();
3842
+ this.cameraManager.walkControls.fitSpeed( this.assetLoader.getSceneScale() );
3826
3843
 
3827
3844
  }
3828
3845
 
@@ -97,6 +97,7 @@ export class AssetLoader extends EventDispatcher {
97
97
  this.targetModel = null;
98
98
  this.floorPlane = null;
99
99
  this.sceneScale = 1.0;
100
+ this.sceneSize = new Vector3( 1, 1, 1 );
100
101
  this.loaderCache = {};
101
102
  this.uploadedFileInfo = null;
102
103
  this.animations = [];
@@ -2000,6 +2001,7 @@ export class AssetLoader extends EventDispatcher {
2000
2001
 
2001
2002
  // Calculate scene scale factor based on model size
2002
2003
  const sceneScale = maxDim;
2004
+ this.sceneSize.copy( size );
2003
2005
 
2004
2006
  // Rebuild path tracing
2005
2007
  buildTimer.start( 'setupPathTracing' );
@@ -2216,6 +2218,13 @@ export class AssetLoader extends EventDispatcher {
2216
2218
 
2217
2219
  }
2218
2220
 
2221
+ /** The loaded model's bounding-box size along x, y and z, in scene units. */
2222
+ getSceneSize() {
2223
+
2224
+ return this.sceneSize.clone();
2225
+
2226
+ }
2227
+
2219
2228
  getTargetModel() {
2220
2229
 
2221
2230
  return this.targetModel;
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Output: { group, camera, environment, animations, warnings }
5
5
  * - group: THREE.Group of meshes (fed to PathTracerApp.loadObject3D)
6
- * - camera: PerspectiveCamera matching the pbrt Camera/LookAt, parented
6
+ * - camera: Perspective- or OrthographicCamera matching the pbrt Camera/LookAt, parented
7
7
  * into the group so AssetLoader.extractCamerasFromModel finds it
8
8
  * - environment: { texture } | null — set by the caller as scene.environment
9
9
  * - animations: [ AnimationClip ] when the IR carries motion (see PBRTAnimation.js), else []
@@ -15,7 +15,7 @@
15
15
  */
16
16
 
17
17
  import {
18
- Group, Mesh, InstancedMesh, PerspectiveCamera, Matrix4, Vector3, Quaternion,
18
+ Group, Mesh, InstancedMesh, PerspectiveCamera, OrthographicCamera, Matrix4, Vector3, Quaternion,
19
19
  BufferGeometry, Float32BufferAttribute, Uint32BufferAttribute, SphereGeometry,
20
20
  DataTexture, FloatType, RGBAFormat, LinearFilter, EquirectangularReflectionMapping,
21
21
  SRGBColorSpace, AnimationClip, VectorKeyframeTrack, QuaternionKeyframeTrack,
@@ -184,7 +184,7 @@ export class PBRTSceneBuilder {
184
184
 
185
185
  /**
186
186
  * @param {object} ir - output of PBRTParser
187
- * @returns {Promise<{group:Group, camera:PerspectiveCamera|null, environment:object|null, warnings:string[]}>}
187
+ * @returns {Promise<{group:Group, camera:PerspectiveCamera|OrthographicCamera|null, environment:object|null, warnings:string[]}>}
188
188
  */
189
189
  async build( ir ) {
190
190
 
@@ -494,7 +494,7 @@ export class PBRTSceneBuilder {
494
494
 
495
495
  } );
496
496
 
497
- if ( camMotion.fov ) {
497
+ if ( camMotion.fov && camera.isPerspectiveCamera ) {
498
498
 
499
499
  const fovs = Array.from( camMotion.fov, f => this._verticalFov( { fov: { type: 'float', value: [ f ] } }, camera.aspect ) );
500
500
  tracks.push( new NumberKeyframeTrack( `${camera.name}.fov`, camMotion.times.slice(), fovs ) );
@@ -1175,9 +1175,21 @@ export class PBRTSceneBuilder {
1175
1175
  _buildCamera( cam, film ) {
1176
1176
 
1177
1177
  const aspect = film && film.yresolution ? film.xresolution / film.yresolution : 16 / 9;
1178
- const fov = this._verticalFov( cam.params, aspect );
1179
1178
 
1180
- const camera = new PerspectiveCamera( fov, aspect, 0.01, 10000 );
1179
+ let camera;
1180
+ if ( cam.type === 'orthographic' ) {
1181
+
1182
+ // pbrt's default screen window spans [-1, 1] along the shorter image axis.
1183
+ const window = cam.params?.screenwindow?.value;
1184
+ const half = window?.length === 4 ? ( window[ 3 ] - window[ 2 ] ) / 2 : Math.max( 1, 1 / aspect );
1185
+ camera = new OrthographicCamera( - half * aspect, half * aspect, half, - half, 0.01, 10000 );
1186
+
1187
+ } else {
1188
+
1189
+ camera = new PerspectiveCamera( this._verticalFov( cam.params, aspect ), aspect, 0.01, 10000 );
1190
+
1191
+ }
1192
+
1181
1193
  camera.name = 'PBRT Camera';
1182
1194
  this._poseCamera( camera, cam.cameraToWorld );
1183
1195
  camera.updateMatrixWorld( true );
@@ -1,5 +1,5 @@
1
1
  import { EventDispatcher, Color, Vector2, MathUtils } from 'three';
2
- import { ENGINE_DEFAULTS } from './EngineDefaults.js';
2
+ import { ENGINE_DEFAULTS, CAMERA_PROJECTION_IDS } from './EngineDefaults.js';
3
3
  import { EngineEvents } from './EngineEvents.js';
4
4
  import { ISSUE_CODES } from './EngineIssues.js';
5
5
 
@@ -40,6 +40,9 @@ const SETTING_ROUTES = {
40
40
  aperture: { uniform: 'aperture', reset: true },
41
41
  apertureScale: { uniform: 'apertureScale', reset: true },
42
42
  anamorphicRatio: { uniform: 'anamorphicRatio', reset: true },
43
+ unitsPerMetre: { uniform: 'unitsPerMetre', reset: true },
44
+ dofMode: { handler: 'handleDofMode', reset: true },
45
+ dofBlur: { uniform: 'dofBlur', reset: true },
43
46
  samplingTechnique: { uniform: 'samplingTechnique', reset: true },
44
47
  fireflyThreshold: { uniform: 'fireflyThreshold', reset: true },
45
48
  shadowTerminatorOffset: { uniform: 'shadowTerminatorOffset', reset: true },
@@ -149,8 +152,8 @@ export class RenderSettings extends EventDispatcher {
149
152
  * @param {Function} params.resetCallback - Called to reset accumulation
150
153
  * @param {Function} [params.reconcileCompletion] - Called when completion limits change
151
154
  * @param {Object} [params.denoisingManager] - Needed to force ASVGF off under panorama
152
- * @param {Object} [params.cameraManager] - Needed to force auto-focus manual under panorama
153
155
  * @param {Function} [params.onInteractionRenderScale] - Applies a new moving-camera render scale
156
+ * @param {Function} [params.onCameraProjection] - The camera's side of a `cameraProjection` change
154
157
  */
155
158
  bind( params ) {
156
159
 
@@ -165,7 +168,7 @@ export class RenderSettings extends EventDispatcher {
165
168
  * Builds handler functions for multi-stage settings that can't
166
169
  * be routed with a simple uniform forward.
167
170
  */
168
- _buildHandlers( { stages, renderer, resetCallback, reconcileCompletion, denoisingManager, cameraManager, onInteractionRenderScale } ) {
171
+ _buildHandlers( { stages, renderer, resetCallback, reconcileCompletion, denoisingManager, onInteractionRenderScale, onCameraProjection } ) {
169
172
 
170
173
  // UniformManager copies into the existing node, so one scratch vector serves every write.
171
174
  const panoScratch = new Vector2();
@@ -180,20 +183,20 @@ export class RenderSettings extends EventDispatcher {
180
183
 
181
184
  handleCameraProjection: ( value ) => {
182
185
 
183
- const isPanorama = value === 'equirectangular';
184
- stages.pathTracer?.setUniform( 'cameraProjection', isPanorama ? 1 : 0 );
186
+ stages.pathTracer?.setUniform( 'cameraProjection', CAMERA_PROJECTION_IDS[ value ] ?? CAMERA_PROJECTION_IDS.perspective );
187
+ onCameraProjection?.( value );
185
188
 
186
- if ( ! isPanorama ) return;
189
+ if ( value !== 'equirectangular' ) return;
187
190
 
188
191
  // MotionVector unprojects through projectionMatrixInverse, which is meaningless once
189
192
  // every pixel is its own direction. Fall back to the spatial-only denoiser rather
190
193
  // than leaving no strategy.
191
194
  if ( denoisingManager?.requiresMotionVectors ) denoisingManager.setDenoiserStrategy( 'edgeaware' );
192
- // Auto-focus raycasts via Raycaster.setFromCamera, which only knows the frustum.
193
- cameraManager?.setAutoFocusMode( 'manual' );
194
195
 
195
196
  },
196
197
 
198
+ handleDofMode: ( value ) => stages.pathTracer?.setUniform( 'dofMode', value === 'look' ? 1 : 0 ),
199
+
197
200
  handlePanoramaLonRange: ( value ) => setPanoRange( 'panoLonRange', value ),
198
201
 
199
202
  handlePanoramaLatRange: ( value ) => setPanoRange( 'panoLatRange', value ),
@@ -1,9 +1,10 @@
1
- import { Fn, vec2, vec3, vec4, float, int, uint, ivec2, uvec2, uniform, If, normalize, mat3,
1
+ import { Fn, vec2, vec3, vec4, float, int, uint, ivec2, uvec2, uniform, If,
2
2
  textureLoad, textureStore, workgroupId, localId } from 'three/tsl';
3
3
  import { RenderTarget, TextureNode, StorageTexture } from 'three/webgpu';
4
4
  import { HalfFloatType, RGBAFormat, NearestFilter, Matrix4, Box2, Vector2 } from 'three';
5
5
  import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
6
6
  import { MAX_STORAGE_TEXTURE_SIZE } from '../EngineDefaults.js';
7
+ import { cameraRayPoint, cameraRayUniforms } from '../TSL/CameraRay.js';
7
8
 
8
9
  /**
9
10
  * WebGPU Motion Vector Stage (Compute Shader)
@@ -20,8 +21,8 @@ import { MAX_STORAGE_TEXTURE_SIZE } from '../EngineDefaults.js';
20
21
  *
21
22
  * Algorithm:
22
23
  * 1. Read normalDepth from NormalDepth (linear depth in alpha)
23
- * 2. Reconstruct camera ray from pixel coords via inverse projection
24
- * 3. World position = cameraPos + normalize(rayDir) * linearDepth
24
+ * 2. Reconstruct the camera ray NormalDepth traced for the pixel
25
+ * 3. World position = rayOrigin + rayDir * linearDepth
25
26
  * 4. Project to previous frame: prevVP * worldPos
26
27
  * 5. Motion = currentUV - prevUV
27
28
  *
@@ -78,6 +79,7 @@ export class MotionVector extends RenderStage {
78
79
  // Synced from PathTracer each frame (same source as NormalDepth)
79
80
  this.cameraWorldMatrix = uniform( new Matrix4(), 'mat4' );
80
81
  this.cameraProjectionMatrixInverse = uniform( new Matrix4(), 'mat4' );
82
+ this.cameraRay = cameraRayUniforms();
81
83
  this.prevVP = uniform( new Matrix4(), 'mat4' );
82
84
  this.isFirstFrameU = uniform( 1.0 ); // 1.0 = true, 0.0 = false
83
85
  this.deltaTime = uniform( 1.0 / 60.0 );
@@ -175,29 +177,7 @@ export class MotionVector extends RenderStage {
175
177
  // Sky / background (depth >= 65504 sentinel) — no motion
176
178
  If( linearDepth.lessThan( float( 6e4 ) ), () => {
177
179
 
178
- // Pixel coordinate → NDC
179
- // Negate Y to match PathTracer convention
180
- const ndcX = float( gx ).add( 0.5 ).div( resW ).mul( 2.0 ).sub( 1.0 );
181
- const ndcY = float( gy ).add( 0.5 ).div( resH ).mul( 2.0 ).sub( 1.0 ).negate();
182
- const ndcPos = vec3( ndcX, ndcY, 1.0 );
183
-
184
- // Camera-space ray direction via inverse projection
185
- const rayDirCS = cpi.mul( vec4( ndcPos, 1.0 ) );
186
-
187
- // Transform to world space (rotation only, via mat3 of world matrix)
188
- const rayDirWorld = normalize(
189
- mat3(
190
- cwm[ 0 ].xyz,
191
- cwm[ 1 ].xyz,
192
- cwm[ 2 ].xyz
193
- ).mul( rayDirCS.xyz.div( rayDirCS.w ) )
194
- );
195
-
196
- // Camera position (translation column of world matrix)
197
- const camPos = vec3( cwm[ 3 ] );
198
-
199
- // World position = camera origin + ray direction * linear depth
200
- const worldPos = camPos.add( rayDirWorld.mul( linearDepth ) );
180
+ const worldPos = cameraRayPoint( currentUV, linearDepth, cwm, cpi, this.cameraRay );
201
181
 
202
182
  // Project to previous frame
203
183
  const prevClip = prevVP.mul( vec4( worldPos, 1.0 ) );
@@ -278,27 +258,12 @@ export class MotionVector extends RenderStage {
278
258
  // Skip first frame and sky (depth >= 65504 sentinel)
279
259
  If( isFirstFrameU.lessThan( 0.5 ).and( linearDepth.lessThan( float( 6e4 ) ) ), () => {
280
260
 
281
- // Pixel coordinate → NDC
282
- // Negate Y to match PathTracer convention
283
- const ndcX = float( gx ).add( 0.5 ).div( resW ).mul( 2.0 ).sub( 1.0 );
284
- const ndcY = float( gy ).add( 0.5 ).div( resH ).mul( 2.0 ).sub( 1.0 ).negate();
285
- const ndcPos = vec3( ndcX, ndcY, 1.0 );
286
- const rayDirCS = cpi.mul( vec4( ndcPos, 1.0 ) );
287
- const rayDirWorld = normalize(
288
- mat3(
289
- cwm[ 0 ].xyz,
290
- cwm[ 1 ].xyz,
291
- cwm[ 2 ].xyz
292
- ).mul( rayDirCS.xyz.div( rayDirCS.w ) )
293
- );
294
- const camPos = vec3( cwm[ 3 ] );
295
- const worldPos = camPos.add( rayDirWorld.mul( linearDepth ) );
296
-
297
261
  // Current pixel UV
298
262
  const currentUV = vec2(
299
263
  float( gx ).add( 0.5 ).div( resW ),
300
264
  float( gy ).add( 0.5 ).div( resH )
301
265
  );
266
+ const worldPos = cameraRayPoint( currentUV, linearDepth, cwm, cpi, this.cameraRay );
302
267
 
303
268
  // Project to previous frame
304
269
  const prevClip = prevVP.mul( vec4( worldPos, 1.0 ) );
@@ -370,6 +335,7 @@ export class MotionVector extends RenderStage {
370
335
  if ( pt && pt.uniforms ) {
371
336
 
372
337
  // Sync from PathTracer (same source as NormalDepth)
338
+ this.cameraRay.sync( pt.uniforms );
373
339
  worldMatrix = pt.uniforms.get( 'cameraWorldMatrix' ).value;
374
340
  viewMatrix = pt.uniforms.get( 'cameraViewMatrix' ).value;
375
341
  projMatrix = pt.uniforms.get( 'cameraProjectionMatrix' ).value;