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.
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * VideoRenderManager — Drives offline frame-by-frame animation rendering.
3
3
  *
4
- * Seeks the animation to each frame time, accumulates SPP until convergence,
5
- * optionally denoises, captures the output canvas, and delivers each frame
6
- * via a callback for encoding.
4
+ * Seeks the animation clip and the timeline, when given one, to each frame time, accumulates SPP
5
+ * until convergence, optionally denoises, captures the output canvas, and delivers each frame via a
6
+ * callback for encoding.
7
7
  */
8
8
 
9
9
  import { EngineEvents } from '../EngineEvents.js';
@@ -30,6 +30,8 @@ export class VideoRenderManager {
30
30
  * @param {boolean} [options.enableOIDN=true] - Run OIDN denoiser per frame
31
31
  * @param {number} [options.speed=1] - Playback speed multiplier (maps video time to animation time)
32
32
  * @param {number} [options.totalFrames] - Override total frame count (for looped animations)
33
+ * @param {import('./timeline/TimelineManager.js').TimelineManager} [options.timeline] - Keyframes to
34
+ * play in video time; without a clip the video lasts as long as they do
33
35
  * @param {Function} [options.onFrame] - async (ImageBitmap, frameIndex, totalFrames) => void
34
36
  * @param {Function} [options.onProgress] - ({ frame, totalFrames, percent }) => void
35
37
  * @param {Function} [options.onComplete] - (success: boolean) => void
@@ -42,6 +44,7 @@ export class VideoRenderManager {
42
44
  speed = 1,
43
45
  samplesPerFrame = PRODUCTION_RENDER_CONFIG.maxSamples,
44
46
  enableOIDN = true,
47
+ timeline = null,
45
48
  onFrame,
46
49
  onProgress,
47
50
  onComplete,
@@ -49,32 +52,32 @@ export class VideoRenderManager {
49
52
 
50
53
  const app = this._app;
51
54
 
52
- if ( ! app.animationManager?.hasAnimations ) {
55
+ // Keyframes alone make a video too.
56
+ const clip = app.animationManager?.hasAnimations ? app.animationManager.clips[ clipIndex ] : null;
57
+ if ( ! clip && ! timeline?.animates ) {
53
58
 
54
- console.warn( 'VideoRenderManager: No animation clips available' );
59
+ console.warn( app.animationManager?.hasAnimations ? `VideoRenderManager: Invalid clip index ${clipIndex}` : 'VideoRenderManager: No animation clips or keyframes' );
55
60
  onComplete?.( false );
56
61
  return;
57
62
 
58
63
  }
59
64
 
60
- const clip = app.animationManager.clips[ clipIndex ];
61
- if ( ! clip ) {
65
+ const effectiveDuration = clip ? clip.duration / ( speed || 1 ) : timeline.duration;
66
+ const totalFrames = options.totalFrames || Math.ceil( effectiveDuration * fps );
67
+ const frameDuration = 1 / fps;
68
+ if ( ! ( totalFrames > 0 ) ) {
62
69
 
63
- console.warn( `VideoRenderManager: Invalid clip index ${clipIndex}` );
70
+ console.warn( 'VideoRenderManager: No frames to render' );
64
71
  onComplete?.( false );
65
72
  return;
66
73
 
67
74
  }
68
75
 
69
- const effectiveDuration = clip.duration / ( speed || 1 );
70
- const totalFrames = options.totalFrames || Math.ceil( effectiveDuration * fps );
71
- const frameDuration = 1 / fps;
72
-
73
76
  this._cancelled = false;
74
77
  this._rendering = true;
75
78
 
76
79
  // Save current engine state
77
- const savedState = this._saveState();
80
+ const savedState = this._saveState( { view: !! timeline } );
78
81
 
79
82
  // Stop the rAF loop — we drive rendering manually
80
83
  app.stopAnimation();
@@ -99,7 +102,16 @@ export class VideoRenderManager {
99
102
  const animationTime = i * frameDuration * speed;
100
103
 
101
104
  // 1. Seek animation to frame time
102
- const positions = app.animationManager.seekTo( animationTime, clipIndex );
105
+ const positions = clip ? app.animationManager.seekTo( animationTime, clipIndex ) : null;
106
+
107
+ // After the clip, which moves a followed model camera.
108
+ if ( timeline?.seek( i * frameDuration ) ) {
109
+
110
+ // A moving camera changes what is under the focus point.
111
+ app.cameraManager.resetAutoFocus();
112
+ app.cameraManager.updateAutoFocus();
113
+
114
+ }
103
115
 
104
116
  // 2. Refit BVH with deformed positions (also calls reset())
105
117
  if ( positions ) {
@@ -268,16 +280,21 @@ export class VideoRenderManager {
268
280
 
269
281
  /**
270
282
  * Save engine state that we'll modify during video render.
283
+ * @param {Object} options
284
+ * @param {boolean} options.view - a timeline will move the camera, and auto-focus with it
271
285
  * @private
272
286
  */
273
- _saveState() {
287
+ _saveState( { view } ) {
274
288
 
275
289
  const app = this._app;
276
290
  const effective = app.settings.getEffective();
291
+ const keys = Object.keys( modePresetSettings( PRODUCTION_RENDER_CONFIG ) );
292
+ if ( view ) keys.push( 'focusDistance' );
277
293
 
278
294
  return {
295
+ view: view ? app.cameraManager.captureView() : null,
279
296
  // Everything configureForMode( 'production' ) overwrites, with who set it.
280
- settings: Object.keys( modePresetSettings( PRODUCTION_RENDER_CONFIG ) ).map( key => [ key, effective[ key ] ] ),
297
+ settings: keys.map( key => [ key, effective[ key ] ] ),
281
298
  renderMode: app.stages.pathTracer?.renderMode?.value,
282
299
  controlsEnabled: app.cameraManager.controls?.enabled,
283
300
  oidnEnabled: app.denoisingManager?.finalDenoise,
@@ -321,6 +338,8 @@ export class VideoRenderManager {
321
338
 
322
339
  app.pauseRendering = state.pauseRendering ?? false;
323
340
 
341
+ if ( state.view ) app.cameraManager.applyPose( state.view );
342
+
324
343
  // configureForMode( 'production' ) suspended the live-view refresh and nothing here switches
325
344
  // back, so the viewport would stay un-denoised until the host changed tab.
326
345
  app.denoisingManager?.setCadenceSuspended( false );
@@ -0,0 +1,77 @@
1
+ import { OrthographicCamera, PerspectiveCamera } from 'three';
2
+
3
+ /**
4
+ * The view's camera: a perspective camera that can also project orthographically.
5
+ *
6
+ * It switches in place, so everything holding it (the controls, picking, the gizmo, the overlays
7
+ * and the path tracer) follows. three.js reads the camera's type from the two flags below, and an
8
+ * orthographic camera's frustum from `top`, `bottom`, `left` and `right`.
9
+ */
10
+ export class ViewCamera extends PerspectiveCamera {
11
+
12
+ constructor( fov, aspect, near, far ) {
13
+
14
+ super( fov, aspect, near, far );
15
+
16
+ this.orthographic = false;
17
+ /** Half the view's height in world units while orthographic, before `zoom`. */
18
+ this.orthoHalfHeight = 1;
19
+
20
+ }
21
+
22
+ get isPerspectiveCamera() {
23
+
24
+ return ! this.orthographic;
25
+
26
+ }
27
+
28
+ // PerspectiveCamera's constructor assigns the flag.
29
+ set isPerspectiveCamera( value ) {}
30
+
31
+ get isOrthographicCamera() {
32
+
33
+ return this.orthographic;
34
+
35
+ }
36
+
37
+ get top() {
38
+
39
+ return this.orthoHalfHeight;
40
+
41
+ }
42
+
43
+ get bottom() {
44
+
45
+ return - this.orthoHalfHeight;
46
+
47
+ }
48
+
49
+ get right() {
50
+
51
+ return this.orthoHalfHeight * this.aspect;
52
+
53
+ }
54
+
55
+ get left() {
56
+
57
+ return - this.orthoHalfHeight * this.aspect;
58
+
59
+ }
60
+
61
+ updateProjectionMatrix() {
62
+
63
+ if ( this.orthographic ) OrthographicCamera.prototype.updateProjectionMatrix.call( this );
64
+ else super.updateProjectionMatrix();
65
+
66
+ }
67
+
68
+ copy( source, recursive ) {
69
+
70
+ super.copy( source, recursive );
71
+ this.orthographic = source.orthographic === true;
72
+ this.orthoHalfHeight = source.orthoHalfHeight ?? this.orthoHalfHeight;
73
+ return this;
74
+
75
+ }
76
+
77
+ }
@@ -0,0 +1,262 @@
1
+ import { Controls, MathUtils, Quaternion, Vector3 } from 'three';
2
+
3
+ const CROSSING_SECONDS = 8;
4
+ const FAST = 4;
5
+ const SLOW = 0.25;
6
+ const MAX_PITCH = MathUtils.degToRad( 89 );
7
+ const MAX_STEP_SECONDS = 0.25;
8
+
9
+ // Right, forward, up.
10
+ const KEY_DIRECTIONS = {
11
+ KeyD: [ 1, 0, 0 ], ArrowRight: [ 1, 0, 0 ],
12
+ KeyA: [ - 1, 0, 0 ], ArrowLeft: [ - 1, 0, 0 ],
13
+ KeyW: [ 0, 1, 0 ], ArrowUp: [ 0, 1, 0 ],
14
+ KeyS: [ 0, - 1, 0 ], ArrowDown: [ 0, - 1, 0 ],
15
+ KeyE: [ 0, 0, 1 ],
16
+ KeyQ: [ 0, 0, - 1 ],
17
+ };
18
+
19
+ // Focus that types, or opens a list, keeps its keys. A widget that handled a key prevents its default.
20
+ const TYPES_KEYS = 'input, textarea, select, [contenteditable]:not([contenteditable="false"]), '
21
+ + '[role=textbox], [role=combobox], [role=listbox], [role=menu], [role=menuitem]';
22
+
23
+ const speedFactor = event => event.shiftKey ? FAST : event.altKey ? SLOW : 1;
24
+
25
+ const _changeEvent = { type: 'change' };
26
+ const _startEvent = { type: 'start' };
27
+ const _forward = new Vector3();
28
+ const _right = new Vector3();
29
+ const _move = new Vector3();
30
+ const _axis = new Vector3();
31
+ const _q = new Quaternion();
32
+
33
+ /**
34
+ * First-person navigation: drag to look, W A S D or the arrows to walk level, E and Q to rise
35
+ * and sink, Shift faster, Alt slower.
36
+ *
37
+ * Rides on the orbit controls: moves only while they are enabled, and keeps their target ahead
38
+ * of the camera, since they re-aim the camera at it every frame.
39
+ */
40
+ export class WalkControls extends Controls {
41
+
42
+ /**
43
+ * @param {import('three').PerspectiveCamera} camera
44
+ * @param {HTMLElement} domElement
45
+ * @param {import('three/addons/controls/OrbitControls.js').OrbitControls} orbit
46
+ */
47
+ constructor( camera, domElement, orbit ) {
48
+
49
+ super( camera, domElement );
50
+
51
+ this.orbit = orbit;
52
+ this.enabled = false;
53
+ /** Scene units per second. */
54
+ this.speed = 1 / CROSSING_SECONDS;
55
+
56
+ this._held = new Set();
57
+ this._factor = 1;
58
+ this._lastTime = 0;
59
+ this._drag = null;
60
+
61
+ this._onKeyDown = this._onKeyDown.bind( this );
62
+ this._onKeyUp = this._onKeyUp.bind( this );
63
+ this.release = this.release.bind( this );
64
+ this._onPointerDown = this._onPointerDown.bind( this );
65
+ this._onPointerMove = this._onPointerMove.bind( this );
66
+ this._onPointerUp = this._onPointerUp.bind( this );
67
+
68
+ if ( domElement ) this.connect( domElement );
69
+
70
+ }
71
+
72
+ get active() {
73
+
74
+ return this.enabled && this.orbit.enabled;
75
+
76
+ }
77
+
78
+ /** Cross a scene this long in about eight seconds. */
79
+ fitSpeed( sceneSize ) {
80
+
81
+ this.speed = ( sceneSize > 0 ? sceneSize : 1 ) / CROSSING_SECONDS;
82
+
83
+ }
84
+
85
+ connect( element ) {
86
+
87
+ super.connect( element );
88
+ const doc = element.ownerDocument;
89
+ doc?.addEventListener( 'keydown', this._onKeyDown );
90
+ doc?.addEventListener( 'keyup', this._onKeyUp );
91
+ doc?.addEventListener( 'visibilitychange', this.release );
92
+ doc?.defaultView?.addEventListener( 'blur', this.release );
93
+ element.addEventListener( 'pointerdown', this._onPointerDown );
94
+ element.addEventListener( 'pointermove', this._onPointerMove );
95
+ element.addEventListener( 'pointerup', this._onPointerUp );
96
+ element.addEventListener( 'pointercancel', this._onPointerUp );
97
+
98
+ }
99
+
100
+ disconnect() {
101
+
102
+ const element = this.domElement;
103
+ if ( ! element ) return;
104
+ const doc = element.ownerDocument;
105
+ doc?.removeEventListener( 'keydown', this._onKeyDown );
106
+ doc?.removeEventListener( 'keyup', this._onKeyUp );
107
+ doc?.removeEventListener( 'visibilitychange', this.release );
108
+ doc?.defaultView?.removeEventListener( 'blur', this.release );
109
+ element.removeEventListener( 'pointerdown', this._onPointerDown );
110
+ element.removeEventListener( 'pointermove', this._onPointerMove );
111
+ element.removeEventListener( 'pointerup', this._onPointerUp );
112
+ element.removeEventListener( 'pointercancel', this._onPointerUp );
113
+
114
+ }
115
+
116
+ dispose() {
117
+
118
+ this.disconnect();
119
+
120
+ }
121
+
122
+ update( delta = this._elapsed() ) {
123
+
124
+ if ( ! this._held.size || ! this.active ) return false;
125
+
126
+ let right = 0, forward = 0, up = 0;
127
+ for ( const code of this._held ) {
128
+
129
+ const [ r, f, u ] = KEY_DIRECTIONS[ code ];
130
+ right += r;
131
+ forward += f;
132
+ up += u;
133
+
134
+ }
135
+
136
+ if ( ! right && ! forward && ! up ) return false;
137
+
138
+ const camera = this.object;
139
+ // Level axes from the camera's right, which stays level however far it looks down.
140
+ _right.set( 1, 0, 0 ).applyQuaternion( camera.quaternion );
141
+ _forward.crossVectors( camera.up, _right ).normalize();
142
+ _right.crossVectors( _forward, camera.up );
143
+
144
+ _move.set( 0, 0, 0 )
145
+ .addScaledVector( _right, right )
146
+ .addScaledVector( _forward, forward )
147
+ .addScaledVector( camera.up, up )
148
+ .normalize()
149
+ .multiplyScalar( this.speed * this._factor * delta );
150
+ camera.position.add( _move );
151
+ this.orbit.target.add( _move );
152
+ camera.updateMatrixWorld();
153
+
154
+ this.dispatchEvent( _changeEvent );
155
+ return true;
156
+
157
+ }
158
+
159
+ _elapsed() {
160
+
161
+ const now = performance.now();
162
+ const delta = Math.min( ( now - this._lastTime ) / 1000, MAX_STEP_SECONDS );
163
+ this._lastTime = now;
164
+ return delta;
165
+
166
+ }
167
+
168
+ /** Turn by a drag of dx, dy pixels, so the scene follows the pointer. */
169
+ _look( dx, dy ) {
170
+
171
+ const camera = this.object;
172
+ const target = this.orbit.target;
173
+ const distance = camera.position.distanceTo( target ) || 1;
174
+ const radiansPerPixel = MathUtils.degToRad( camera.fov ) / ( this.domElement.clientHeight || 1 );
175
+
176
+ camera.quaternion.premultiply( _q.setFromAxisAngle( camera.up, dx * radiansPerPixel ) );
177
+
178
+ camera.getWorldDirection( _forward );
179
+ const pitch = Math.asin( MathUtils.clamp( _forward.dot( camera.up ), - 1, 1 ) );
180
+ const nextPitch = MathUtils.clamp( pitch + dy * radiansPerPixel, - MAX_PITCH, MAX_PITCH );
181
+ camera.quaternion.multiply( _q.setFromAxisAngle( _axis.set( 1, 0, 0 ), nextPitch - pitch ) );
182
+ camera.updateMatrixWorld();
183
+
184
+ target.copy( camera.position ).addScaledVector( camera.getWorldDirection( _forward ), distance );
185
+ this.dispatchEvent( _changeEvent );
186
+
187
+ }
188
+
189
+ _onKeyDown( event ) {
190
+
191
+ if ( ! this.active || event.defaultPrevented || event.target?.closest?.( TYPES_KEYS ) ) return;
192
+
193
+ // Browser shortcuts pass through, and macOS drops the key-up of a key pressed while Cmd is down.
194
+ if ( event.metaKey || event.ctrlKey ) {
195
+
196
+ this.release();
197
+ return;
198
+
199
+ }
200
+
201
+ this._factor = speedFactor( event );
202
+ if ( ! KEY_DIRECTIONS[ event.code ] ) return;
203
+
204
+ event.preventDefault();
205
+ if ( this._held.has( event.code ) ) return;
206
+ this._held.add( event.code );
207
+
208
+ // After the key is held: a woken loop runs its first frame inside this dispatch.
209
+ if ( this._held.size === 1 ) {
210
+
211
+ this._lastTime = performance.now();
212
+ this.dispatchEvent( _startEvent );
213
+
214
+ }
215
+
216
+ }
217
+
218
+ _onKeyUp( event ) {
219
+
220
+ this._factor = speedFactor( event );
221
+ this._held.delete( event.code );
222
+
223
+ }
224
+
225
+ /** Let go of every held key. */
226
+ release() {
227
+
228
+ this._held.clear();
229
+ this._factor = 1;
230
+
231
+ }
232
+
233
+ _onPointerDown( event ) {
234
+
235
+ if ( ! this.active || event.button !== 0 || ! event.isPrimary ) return;
236
+ this._drag = { id: event.pointerId, x: event.clientX, y: event.clientY };
237
+ this.domElement.setPointerCapture?.( event.pointerId );
238
+
239
+ }
240
+
241
+ _onPointerMove( event ) {
242
+
243
+ const drag = this._drag;
244
+ if ( ! drag || event.pointerId !== drag.id ) return;
245
+
246
+ const dx = event.clientX - drag.x;
247
+ const dy = event.clientY - drag.y;
248
+ drag.x = event.clientX;
249
+ drag.y = event.clientY;
250
+ if ( this.active && ( dx || dy ) ) this._look( dx, dy );
251
+
252
+ }
253
+
254
+ _onPointerUp( event ) {
255
+
256
+ if ( this._drag?.id !== event.pointerId ) return;
257
+ this._drag = null;
258
+ this.domElement.releasePointerCapture?.( event.pointerId );
259
+
260
+ }
261
+
262
+ }
@@ -28,12 +28,22 @@ export class OutlineHelper {
28
28
 
29
29
  this.layer = 'scene';
30
30
  this._enabled = true;
31
+ this._scene = scene;
32
+ this._camera = camera;
33
+ this._build( [] );
34
+
35
+ }
36
+
37
+ // OutlineNode reads the camera's type once, as it builds its depth test.
38
+ _build( selectedObjects ) {
39
+
40
+ this._orthographic = this._camera.isOrthographicCamera === true;
31
41
 
32
42
  // Outline node (handles its own multi-pass rendering via updateBefore).
33
43
  // It sizes its render targets from the drawing buffer of whatever renderer
34
44
  // draws it — the view overlay surface — so no size override is needed.
35
- this._outlineNode = outline( scene, camera, {
36
- selectedObjects: [],
45
+ this._outlineNode = outline( this._scene, this._camera, {
46
+ selectedObjects,
37
47
  edgeThickness: uniform( 1.0 ),
38
48
  edgeGlow: uniform( 0.0 ),
39
49
  } );
@@ -98,6 +108,16 @@ export class OutlineHelper {
98
108
 
99
109
  if ( ! this.visible ) return;
100
110
 
111
+ if ( this._camera.isOrthographicCamera !== this._orthographic ) {
112
+
113
+ // dispose() empties the list it was given.
114
+ const selected = [ ...this._outlineNode.selectedObjects ];
115
+ this._outlineNode.dispose();
116
+ this._material.dispose();
117
+ this._build( selected );
118
+
119
+ }
120
+
101
121
  const prevAutoClear = renderer.autoClear;
102
122
  renderer.autoClear = false;
103
123
  renderer.setRenderTarget( null );