rayzee 8.8.2 → 9.1.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/README.md +210 -20
- package/dist/{NeuralRetouch-CYOJ21Wn.js → NeuralRetouch-BRquqfmZ.js} +1 -1
- package/dist/{NeuralSuperRes-Dc6jRYke.js → NeuralSuperRes-CbNvQjCw.js} +5 -2
- package/dist/{ToneMapGPU-BVE852iN.js → ToneMapGPU-DpUZDMbQ.js} +2 -1
- package/dist/rayzee.es.js +10795 -5324
- package/dist/rayzee.umd.js +279 -268
- package/package.json +1 -1
- package/src/AssetConfig.js +4 -0
- package/src/EngineDefaults.js +60 -76
- package/src/EngineEvents.js +7 -0
- package/src/EngineIssues.js +6 -0
- package/src/Headless.js +3 -1
- package/src/Passes/AIUpscaler.js +2 -1
- package/src/Passes/OIDNDenoiser.js +3 -6
- package/src/Passes/OIDNTemporalHistory.js +25 -7
- package/src/PathTracerApp.js +313 -25
- package/src/Processor/ArchiveCache.js +120 -0
- package/src/Processor/ArchiveReader.js +179 -32
- package/src/Processor/AssetLoader.js +523 -76
- package/src/Processor/AtmosphereModel.js +465 -0
- package/src/Processor/BVHBuilder.js +4 -1
- package/src/Processor/BVHLeafFold.js +100 -0
- package/src/Processor/BVHRefitter.js +98 -214
- package/src/Processor/ChunkedRecords.js +189 -4
- package/src/Processor/EmissiveTriangleBuilder.js +8 -0
- package/src/Processor/GeometryExtractor.js +238 -40
- package/src/Processor/InstanceTable.js +55 -37
- package/src/Processor/PBRT/PBRTCurves.js +104 -27
- package/src/Processor/PBRT/PBRTParser.js +132 -22
- package/src/Processor/PBRT/PBRTSceneBuilder.js +210 -55
- package/src/Processor/PBRT/buffers.js +27 -0
- package/src/Processor/PBRT/index.js +55 -15
- package/src/Processor/PackedRayBuffer.js +10 -13
- package/src/Processor/ParallelBVHBuilder.js +36 -15
- package/src/Processor/PhysicalSky.js +308 -0
- package/src/Processor/QueueManager.js +1 -0
- package/src/Processor/SceneProcessor.js +1132 -181
- package/src/Processor/SplitBLAS.js +249 -0
- package/src/Processor/SunPosition.js +41 -0
- package/src/Processor/TextureCreator.js +21 -0
- package/src/Processor/Workers/AIUpscalerWorker.js +79 -52
- package/src/Processor/Workers/BVHWorker.js +17 -13
- package/src/Processor/ZipReader.js +207 -0
- package/src/RenderSettings.js +49 -8
- package/src/SceneState/SceneState.js +367 -0
- package/src/SceneState/portable.js +52 -0
- package/src/Stages/MotionVector.js +8 -42
- package/src/Stages/NRD.js +30 -21
- package/src/Stages/NormalDepth.js +12 -23
- package/src/Stages/PathTracer.js +114 -6
- package/src/Stages/PathTracerStage.js +251 -104
- package/src/Storage/BLASCache.js +190 -0
- package/src/Storage/CDFCache.js +77 -0
- package/src/Storage/DownloadCache.js +266 -0
- package/src/Storage/GeometrySpill.js +217 -0
- package/src/Storage/SceneGraphCodec.js +503 -0
- package/src/Storage/SpillStore.js +112 -0
- package/src/Storage/StorageManager.js +934 -0
- package/src/Storage/StorageOps.js +137 -0
- package/src/Storage/StorageWorker.js +37 -0
- package/src/Storage/events.js +29 -0
- package/src/Storage/identity.js +77 -0
- package/src/Storage/index.js +9 -0
- package/src/Storage/inlineTransport.js +24 -0
- package/src/Storage/locks.js +99 -0
- package/src/Storage/openStorage.js +87 -0
- package/src/Storage/sceneCachePolicy.js +12 -0
- package/src/Storage/shared.js +9 -0
- package/src/Storage/transport.js +62 -0
- package/src/TSL/Atmosphere.js +684 -0
- package/src/TSL/BVHTraversal.js +288 -239
- package/src/TSL/CameraRay.js +99 -14
- package/src/TSL/Common.js +11 -2
- package/src/TSL/DebugKernel.js +2 -2
- package/src/TSL/Displacement.js +6 -6
- package/src/TSL/EmissiveSampling.js +5 -5
- package/src/TSL/EnvironmentCDF.js +176 -0
- package/src/TSL/GenerateKernel.js +5 -5
- package/src/TSL/HitFacet.js +4 -4
- package/src/TSL/LightsDirect.js +8 -8
- package/src/TSL/LightsSampling.js +55 -37
- package/src/TSL/Random.js +1 -0
- package/src/TSL/ShadeKernel.js +75 -19
- package/src/TSL/Sun.js +40 -0
- package/src/TSL/TextureSampling.js +6 -7
- package/src/index.js +22 -0
- package/src/managers/CameraManager.js +412 -56
- package/src/managers/DenoisingManager.js +4 -3
- package/src/managers/EnvironmentManager.js +263 -33
- package/src/managers/GoboManager.js +19 -8
- package/src/managers/IESManager.js +3 -3
- package/src/managers/InteractionManager.js +30 -2
- package/src/managers/LightManager.js +98 -5
- package/src/managers/MaterialDataManager.js +22 -2
- package/src/managers/TransformManager.js +26 -0
- package/src/managers/UniformManager.js +11 -7
- package/src/managers/VideoRenderManager.js +38 -17
- package/src/managers/ViewCamera.js +77 -0
- package/src/managers/WalkControls.js +262 -0
- package/src/managers/helpers/OutlineHelper.js +22 -2
- package/src/managers/timeline/CameraTrack.js +235 -0
- package/src/managers/timeline/TimelineManager.js +146 -0
- package/src/version.js +3 -0
- package/src/Processor/ProceduralSky.js +0 -291
|
@@ -2,6 +2,13 @@ import {
|
|
|
2
2
|
EventDispatcher, DirectionalLight, PointLight, SpotLight, RectAreaLight,
|
|
3
3
|
Object3D, MathUtils
|
|
4
4
|
} from 'three';
|
|
5
|
+
import { toPortable, fromPortable } from '../SceneState/portable.js';
|
|
6
|
+
|
|
7
|
+
// Set on lights a host added, which live in the light scene alone: a rebuild re-transfers the
|
|
8
|
+
// model's lights and keeps these.
|
|
9
|
+
const USER_LIGHT = '__rayzeeUserLight';
|
|
10
|
+
|
|
11
|
+
const LIGHT_TYPES = { DirectionalLight, PointLight, SpotLight, RectAreaLight };
|
|
5
12
|
|
|
6
13
|
/**
|
|
7
14
|
* Manages scene lights: add, remove, transfer from mesh scene to WebGPU
|
|
@@ -84,6 +91,8 @@ export class LightManager extends EventDispatcher {
|
|
|
84
91
|
|
|
85
92
|
}
|
|
86
93
|
|
|
94
|
+
light.userData[ USER_LIGHT ] = true;
|
|
95
|
+
|
|
87
96
|
// Blender-style emission controls common to every light type.
|
|
88
97
|
light.userData.temperature = 6500;
|
|
89
98
|
light.userData.useTemperature = false;
|
|
@@ -158,10 +167,11 @@ export class LightManager extends EventDispatcher {
|
|
|
158
167
|
* Clones lights from the mesh scene into the WebGPU light scene,
|
|
159
168
|
* then updates GPU uniform buffers.
|
|
160
169
|
* @param {import('three').Scene} meshScene
|
|
170
|
+
* @param {{keepUserLights?: boolean}} [options] - keep the lights a host added (an incremental rebuild)
|
|
161
171
|
*/
|
|
162
|
-
transferSceneLights( meshScene ) {
|
|
172
|
+
transferSceneLights( meshScene, { keepUserLights = false } = {} ) {
|
|
163
173
|
|
|
164
|
-
this._removeAllLights();
|
|
174
|
+
this._removeAllLights( keepUserLights ? light => ! light.userData?.[ USER_LIGHT ] : undefined );
|
|
165
175
|
|
|
166
176
|
const sourceLights = meshScene.getObjectsByProperty( 'isLight', true );
|
|
167
177
|
|
|
@@ -228,6 +238,89 @@ export class LightManager extends EventDispatcher {
|
|
|
228
238
|
|
|
229
239
|
}
|
|
230
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Every light as plain data, for a saved session: the model's (with any edits) and the host's.
|
|
243
|
+
* @returns {Object[]}
|
|
244
|
+
*/
|
|
245
|
+
serialize() {
|
|
246
|
+
|
|
247
|
+
return this.scene.getObjectsByProperty( 'isLight', true ).map( light => {
|
|
248
|
+
|
|
249
|
+
const record = {
|
|
250
|
+
type: light.type,
|
|
251
|
+
name: light.name,
|
|
252
|
+
visible: light.visible,
|
|
253
|
+
color: [ light.color.r, light.color.g, light.color.b ],
|
|
254
|
+
intensity: light.intensity,
|
|
255
|
+
position: light.position.toArray(),
|
|
256
|
+
quaternion: light.quaternion.toArray(),
|
|
257
|
+
scale: light.scale.toArray(),
|
|
258
|
+
userData: toPortable( light.userData ) ?? {},
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
if ( light.isSpotLight ) Object.assign( record, { angle: light.angle, penumbra: light.penumbra, distance: light.distance, decay: light.decay } );
|
|
262
|
+
if ( light.isPointLight ) Object.assign( record, { distance: light.distance, decay: light.decay } );
|
|
263
|
+
if ( light.isRectAreaLight ) Object.assign( record, { width: light.width, height: light.height } );
|
|
264
|
+
if ( light.target && ( light.isSpotLight || light.isDirectionalLight ) ) record.target = light.target.position.toArray();
|
|
265
|
+
return record;
|
|
266
|
+
|
|
267
|
+
} );
|
|
268
|
+
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Replaces every light with the ones {@link serialize} described.
|
|
273
|
+
* @param {Object[]} records
|
|
274
|
+
* @returns {import('three').Light[]} the new lights, in record order (unknown types skipped)
|
|
275
|
+
*/
|
|
276
|
+
restore( records ) {
|
|
277
|
+
|
|
278
|
+
this.sceneHelpers?.clear();
|
|
279
|
+
this._removeAllLights();
|
|
280
|
+
|
|
281
|
+
const lights = [];
|
|
282
|
+
for ( const record of records ?? [] ) {
|
|
283
|
+
|
|
284
|
+
const Type = LIGHT_TYPES[ record.type ];
|
|
285
|
+
if ( ! Type ) continue;
|
|
286
|
+
|
|
287
|
+
const light = new Type();
|
|
288
|
+
light.name = record.name ?? '';
|
|
289
|
+
light.visible = record.visible !== false;
|
|
290
|
+
light.color.setRGB( ...record.color );
|
|
291
|
+
light.intensity = record.intensity;
|
|
292
|
+
light.position.fromArray( record.position );
|
|
293
|
+
light.quaternion.fromArray( record.quaternion );
|
|
294
|
+
light.scale.fromArray( record.scale ?? [ 1, 1, 1 ] );
|
|
295
|
+
light.userData = fromPortable( record.userData ) ?? {};
|
|
296
|
+
|
|
297
|
+
for ( const key of [ 'angle', 'penumbra', 'distance', 'decay', 'width', 'height' ] ) {
|
|
298
|
+
|
|
299
|
+
if ( typeof record[ key ] === 'number' ) light[ key ] = record[ key ];
|
|
300
|
+
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if ( record.target ) {
|
|
304
|
+
|
|
305
|
+
const target = new Object3D();
|
|
306
|
+
target.position.fromArray( record.target );
|
|
307
|
+
this.scene.add( target );
|
|
308
|
+
light.target = target;
|
|
309
|
+
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
this.scene.add( light );
|
|
313
|
+
lights.push( light );
|
|
314
|
+
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
this.updateLights();
|
|
318
|
+
this._syncHelpers();
|
|
319
|
+
this._onReset?.();
|
|
320
|
+
return lights;
|
|
321
|
+
|
|
322
|
+
}
|
|
323
|
+
|
|
231
324
|
// ── Aliases (match Sub-API surface for zero-churn migration) ──
|
|
232
325
|
|
|
233
326
|
/** @see addLight */
|
|
@@ -295,10 +388,10 @@ export class LightManager extends EventDispatcher {
|
|
|
295
388
|
|
|
296
389
|
// ── Private ───────────────────────────────────────────────────
|
|
297
390
|
|
|
298
|
-
/**
|
|
299
|
-
_removeAllLights() {
|
|
391
|
+
/** @param {function(import('three').Light): boolean} [which] - all lights when omitted */
|
|
392
|
+
_removeAllLights( which ) {
|
|
300
393
|
|
|
301
|
-
this.scene.getObjectsByProperty( 'isLight', true ).forEach( light => {
|
|
394
|
+
this.scene.getObjectsByProperty( 'isLight', true ).filter( light => ! which || which( light ) ).forEach( light => {
|
|
302
395
|
|
|
303
396
|
this._onLightRemoved?.( light );
|
|
304
397
|
if ( light.target ) this.scene.remove( light.target );
|
|
@@ -17,6 +17,7 @@ import { packMaterial, UNIT_RANGE_PROPERTIES, clampUnit } from '../Processor/Mat
|
|
|
17
17
|
import { resolveMaterialTextures, MATERIAL_VALUE_SOURCE } from '../Processor/GeometryExtractor.js';
|
|
18
18
|
import { convertLinearTriple, convertLinearTriples, getWorkingMatrixSpace } from '../Color/WorkingMatrix.js';
|
|
19
19
|
import { createLogger, fmt } from '../utils/Logger.js';
|
|
20
|
+
import { toPortable } from '../SceneState/portable.js';
|
|
20
21
|
|
|
21
22
|
/** Material buffers already moved into the working space, so a re-init cannot convert twice. */
|
|
22
23
|
const convertedBuffers = new WeakSet();
|
|
@@ -30,6 +31,9 @@ const TRI_FLAGS_OFFSET = T.MATERIAL_FLAGS_OFFSET;
|
|
|
30
31
|
// Material properties that affect the shadow-ray opaque-blocker flag.
|
|
31
32
|
const BLOCKER_PROPS = new Set( [ 'transmission', 'transparent', 'opacity', 'alphaMode' ] );
|
|
32
33
|
|
|
34
|
+
/** Material properties whose edit rewrites triangle records (side, shadow blocker, emitters). */
|
|
35
|
+
export const TRIANGLE_PATCH_PROPERTIES = new Set( [ 'side', ...BLOCKER_PROPS, 'emissive', 'emissiveIntensity' ] );
|
|
36
|
+
|
|
33
37
|
// Map slot → sRGB pool (true) or linear pool.
|
|
34
38
|
const TEXTURE_POOLS = [
|
|
35
39
|
[ 'map', true ], [ 'emissiveMap', true ], [ 'sheenColorMap', true ], [ 'specularColorMap', true ],
|
|
@@ -98,7 +102,7 @@ export class MaterialDataManager {
|
|
|
98
102
|
*/
|
|
99
103
|
this.callbacks = {};
|
|
100
104
|
|
|
101
|
-
// Per material index: createMaterialObject's sources, and properties a host set since.
|
|
105
|
+
// Per material index: createMaterialObject's sources, and properties a host set since (name → portable value).
|
|
102
106
|
this._sources = [];
|
|
103
107
|
this._hostSet = [];
|
|
104
108
|
|
|
@@ -310,6 +314,22 @@ export class MaterialDataManager {
|
|
|
310
314
|
|
|
311
315
|
}
|
|
312
316
|
|
|
317
|
+
/**
|
|
318
|
+
* Every property a host set, per material, as portable values (see SceneState/portable.js).
|
|
319
|
+
* @returns {Array<{index: number, props: Object<string, *>}>}
|
|
320
|
+
*/
|
|
321
|
+
serializeHostEdits() {
|
|
322
|
+
|
|
323
|
+
const out = [];
|
|
324
|
+
this._hostSet.forEach( ( props, index ) => {
|
|
325
|
+
|
|
326
|
+
if ( props?.size ) out.push( { index, props: Object.fromEntries( props ) } );
|
|
327
|
+
|
|
328
|
+
} );
|
|
329
|
+
return out;
|
|
330
|
+
|
|
331
|
+
}
|
|
332
|
+
|
|
313
333
|
/**
|
|
314
334
|
* Update a single material property in the storage buffer.
|
|
315
335
|
* @param {number} materialIndex
|
|
@@ -481,7 +501,7 @@ export class MaterialDataManager {
|
|
|
481
501
|
}
|
|
482
502
|
|
|
483
503
|
this.materialStorageAttr.needsUpdate = true;
|
|
484
|
-
( this._hostSet[ materialIndex ] ??= new
|
|
504
|
+
( this._hostSet[ materialIndex ] ??= new Map() ).set( property, toPortable( value ) );
|
|
485
505
|
|
|
486
506
|
// Recompute triangle-data opaque-blocker flag when any input to it changes.
|
|
487
507
|
if ( BLOCKER_PROPS.has( property ) ) {
|
|
@@ -14,6 +14,13 @@ import { EngineEvents } from '../EngineEvents.js';
|
|
|
14
14
|
// does not mutate its arguments, so this can be a shared constant.
|
|
15
15
|
const FORWARD = new Vector3( 0, 0, - 1 );
|
|
16
16
|
|
|
17
|
+
const rootScene = object => {
|
|
18
|
+
|
|
19
|
+
while ( object.parent ) object = object.parent;
|
|
20
|
+
return object.isScene ? object : null;
|
|
21
|
+
|
|
22
|
+
};
|
|
23
|
+
|
|
17
24
|
export class TransformManager {
|
|
18
25
|
|
|
19
26
|
constructor( { camera, canvas, orbitControls, app } ) {
|
|
@@ -33,6 +40,8 @@ export class TransformManager {
|
|
|
33
40
|
this._meshes = null;
|
|
34
41
|
this._refitInFlight = false;
|
|
35
42
|
this._baselineComputed = false;
|
|
43
|
+
// Objects the gizmo moved, so a saved session can put them back.
|
|
44
|
+
this._moved = new Set();
|
|
36
45
|
|
|
37
46
|
// Light transform state — spot/directional lights aim via a separate
|
|
38
47
|
// `.target` Object3D rather than their own rotation (see attach()).
|
|
@@ -56,6 +65,21 @@ export class TransformManager {
|
|
|
56
65
|
setMeshData( meshes ) {
|
|
57
66
|
|
|
58
67
|
this._meshes = meshes;
|
|
68
|
+
for ( const object of this._moved ) if ( ! rootScene( object ) ) this._moved.delete( object );
|
|
69
|
+
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Objects the gizmo has moved that are still in the scene (lights are saved with the lights). */
|
|
73
|
+
get movedObjects() {
|
|
74
|
+
|
|
75
|
+
return [ ...this._moved ].filter( object => rootScene( object ) );
|
|
76
|
+
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Counts an object as moved, as a gizmo drag would — for a restored session. */
|
|
80
|
+
noteMoved( object ) {
|
|
81
|
+
|
|
82
|
+
if ( object && ! object.isLight ) this._moved.add( object );
|
|
59
83
|
|
|
60
84
|
}
|
|
61
85
|
|
|
@@ -335,6 +359,7 @@ export class TransformManager {
|
|
|
335
359
|
// instead costs a pass over every vertex, and corrupts any other object sharing
|
|
336
360
|
// this geometry.
|
|
337
361
|
this._app.updateMeshTransforms( affectedIndices );
|
|
362
|
+
this._moved.add( this._attached );
|
|
338
363
|
|
|
339
364
|
} catch ( err ) {
|
|
340
365
|
|
|
@@ -393,6 +418,7 @@ export class TransformManager {
|
|
|
393
418
|
this._controls.dispose();
|
|
394
419
|
|
|
395
420
|
this._meshes = null;
|
|
421
|
+
this._moved.clear();
|
|
396
422
|
this._baselineComputed = false;
|
|
397
423
|
this._tempForward = null;
|
|
398
424
|
this._lightTargetDistance = null;
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { uniform, uniformArray } from 'three/tsl';
|
|
9
|
-
import { Vector2, Matrix4, Vector3, Color, MathUtils } from 'three';
|
|
9
|
+
import { Vector2, Matrix4, Vector3, Vector4, Color, MathUtils } from 'three';
|
|
10
10
|
import { samplingTechniqueUniform } from '../TSL/Random.js';
|
|
11
|
-
import { ENGINE_DEFAULTS as DEFAULT_STATE } from '../EngineDefaults.js';
|
|
11
|
+
import { ENGINE_DEFAULTS as DEFAULT_STATE, CAMERA_PROJECTION_IDS } from '../EngineDefaults.js';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Map of uniform names to their WGSL shader names (where different).
|
|
@@ -249,9 +249,11 @@ export class UniformManager {
|
|
|
249
249
|
ub( 'enableGroundCatcher', DEFAULT_STATE.enableGroundCatcher );
|
|
250
250
|
u( 'groundCatcherHeight', DEFAULT_STATE.groundCatcherHeight, 'float' );
|
|
251
251
|
|
|
252
|
-
// Sun
|
|
252
|
+
// The physical sky's sun (TSL/Sun.js): world direction, disc-average radiance, and
|
|
253
|
+
// ( cos half-angle, solid angle, 1 / sin² half-angle, horizon dip sin )
|
|
253
254
|
u( 'sunDirection', new Vector3( 0, 1, 0 ), 'vec3' );
|
|
254
|
-
u( '
|
|
255
|
+
u( 'sunRadiance', new Vector3(), 'vec3' );
|
|
256
|
+
u( 'sunParams', new Vector4( 1, 0, 0, 0 ), 'vec4' );
|
|
255
257
|
ub( 'hasSun', false );
|
|
256
258
|
|
|
257
259
|
// Lighting
|
|
@@ -278,9 +280,9 @@ export class UniformManager {
|
|
|
278
280
|
u( 'cameraViewMatrix', new Matrix4(), 'mat4' );
|
|
279
281
|
u( 'cameraProjectionMatrix', new Matrix4(), 'mat4' );
|
|
280
282
|
|
|
281
|
-
// Projection:
|
|
283
|
+
// Projection: CAMERA_PROJECTION_IDS. Ranges are radians (UI carries degrees).
|
|
282
284
|
const radRange = ( [ min, max ] ) => new Vector2( min, max ).multiplyScalar( MathUtils.DEG2RAD );
|
|
283
|
-
u( 'cameraProjection', DEFAULT_STATE.cameraProjection
|
|
285
|
+
u( 'cameraProjection', CAMERA_PROJECTION_IDS[ DEFAULT_STATE.cameraProjection ], 'int' );
|
|
284
286
|
u( 'panoLonRange', radRange( DEFAULT_STATE.panoramaLonRange ), 'vec2' );
|
|
285
287
|
u( 'panoLatRange', radRange( DEFAULT_STATE.panoramaLatRange ), 'vec2' );
|
|
286
288
|
ub( 'panoLevelHorizon', DEFAULT_STATE.panoramaLevelHorizon );
|
|
@@ -292,7 +294,9 @@ export class UniformManager {
|
|
|
292
294
|
u( 'aperture', DEFAULT_STATE.aperture, 'float' );
|
|
293
295
|
u( 'apertureScale', 1.0, 'float' );
|
|
294
296
|
u( 'anamorphicRatio', DEFAULT_STATE.anamorphicRatio ?? 1.0, 'float' );
|
|
295
|
-
u( '
|
|
297
|
+
u( 'unitsPerMetre', DEFAULT_STATE.unitsPerMetre, 'float' );
|
|
298
|
+
u( 'dofMode', DEFAULT_STATE.dofMode === 'look' ? 1 : 0, 'int' );
|
|
299
|
+
u( 'dofBlur', DEFAULT_STATE.dofBlur, 'float' );
|
|
296
300
|
|
|
297
301
|
// Sampling — use the module-level uniform from Random.js so TSL sees the same node
|
|
298
302
|
this._uniforms.set( 'samplingTechnique', samplingTechniqueUniform );
|
|
@@ -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
|
|
5
|
-
* optionally denoises, captures the output canvas, and delivers each frame
|
|
6
|
-
*
|
|
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,9 @@ 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 {number} [options.startFrame=0] - First frame to render: a resumed export skips those it has
|
|
34
|
+
* @param {import('./timeline/TimelineManager.js').TimelineManager} [options.timeline] - Keyframes to
|
|
35
|
+
* play in video time; without a clip the video lasts as long as they do
|
|
33
36
|
* @param {Function} [options.onFrame] - async (ImageBitmap, frameIndex, totalFrames) => void
|
|
34
37
|
* @param {Function} [options.onProgress] - ({ frame, totalFrames, percent }) => void
|
|
35
38
|
* @param {Function} [options.onComplete] - (success: boolean) => void
|
|
@@ -42,6 +45,8 @@ export class VideoRenderManager {
|
|
|
42
45
|
speed = 1,
|
|
43
46
|
samplesPerFrame = PRODUCTION_RENDER_CONFIG.maxSamples,
|
|
44
47
|
enableOIDN = true,
|
|
48
|
+
timeline = null,
|
|
49
|
+
startFrame = 0,
|
|
45
50
|
onFrame,
|
|
46
51
|
onProgress,
|
|
47
52
|
onComplete,
|
|
@@ -49,32 +54,32 @@ export class VideoRenderManager {
|
|
|
49
54
|
|
|
50
55
|
const app = this._app;
|
|
51
56
|
|
|
52
|
-
|
|
57
|
+
// Keyframes alone make a video too.
|
|
58
|
+
const clip = app.animationManager?.hasAnimations ? app.animationManager.clips[ clipIndex ] : null;
|
|
59
|
+
if ( ! clip && ! timeline?.animates ) {
|
|
53
60
|
|
|
54
|
-
console.warn( 'VideoRenderManager: No animation clips
|
|
61
|
+
console.warn( app.animationManager?.hasAnimations ? `VideoRenderManager: Invalid clip index ${clipIndex}` : 'VideoRenderManager: No animation clips or keyframes' );
|
|
55
62
|
onComplete?.( false );
|
|
56
63
|
return;
|
|
57
64
|
|
|
58
65
|
}
|
|
59
66
|
|
|
60
|
-
const
|
|
61
|
-
|
|
67
|
+
const effectiveDuration = clip ? clip.duration / ( speed || 1 ) : timeline.duration;
|
|
68
|
+
const totalFrames = options.totalFrames || Math.ceil( effectiveDuration * fps );
|
|
69
|
+
const frameDuration = 1 / fps;
|
|
70
|
+
if ( ! ( totalFrames > 0 ) ) {
|
|
62
71
|
|
|
63
|
-
console.warn(
|
|
72
|
+
console.warn( 'VideoRenderManager: No frames to render' );
|
|
64
73
|
onComplete?.( false );
|
|
65
74
|
return;
|
|
66
75
|
|
|
67
76
|
}
|
|
68
77
|
|
|
69
|
-
const effectiveDuration = clip.duration / ( speed || 1 );
|
|
70
|
-
const totalFrames = options.totalFrames || Math.ceil( effectiveDuration * fps );
|
|
71
|
-
const frameDuration = 1 / fps;
|
|
72
|
-
|
|
73
78
|
this._cancelled = false;
|
|
74
79
|
this._rendering = true;
|
|
75
80
|
|
|
76
81
|
// Save current engine state
|
|
77
|
-
const savedState = this._saveState();
|
|
82
|
+
const savedState = this._saveState( { view: !! timeline } );
|
|
78
83
|
|
|
79
84
|
// Stop the rAF loop — we drive rendering manually
|
|
80
85
|
app.stopAnimation();
|
|
@@ -91,7 +96,7 @@ export class VideoRenderManager {
|
|
|
91
96
|
|
|
92
97
|
try {
|
|
93
98
|
|
|
94
|
-
for ( let i = 0; i < totalFrames; i ++ ) {
|
|
99
|
+
for ( let i = Math.max( 0, startFrame ); i < totalFrames; i ++ ) {
|
|
95
100
|
|
|
96
101
|
if ( this._cancelled ) break;
|
|
97
102
|
|
|
@@ -99,7 +104,16 @@ export class VideoRenderManager {
|
|
|
99
104
|
const animationTime = i * frameDuration * speed;
|
|
100
105
|
|
|
101
106
|
// 1. Seek animation to frame time
|
|
102
|
-
const positions = app.animationManager.seekTo( animationTime, clipIndex );
|
|
107
|
+
const positions = clip ? app.animationManager.seekTo( animationTime, clipIndex ) : null;
|
|
108
|
+
|
|
109
|
+
// After the clip, which moves a followed model camera.
|
|
110
|
+
if ( timeline?.seek( i * frameDuration ) ) {
|
|
111
|
+
|
|
112
|
+
// A moving camera changes what is under the focus point.
|
|
113
|
+
app.cameraManager.resetAutoFocus();
|
|
114
|
+
app.cameraManager.updateAutoFocus();
|
|
115
|
+
|
|
116
|
+
}
|
|
103
117
|
|
|
104
118
|
// 2. Refit BVH with deformed positions (also calls reset())
|
|
105
119
|
if ( positions ) {
|
|
@@ -268,16 +282,21 @@ export class VideoRenderManager {
|
|
|
268
282
|
|
|
269
283
|
/**
|
|
270
284
|
* Save engine state that we'll modify during video render.
|
|
285
|
+
* @param {Object} options
|
|
286
|
+
* @param {boolean} options.view - a timeline will move the camera, and auto-focus with it
|
|
271
287
|
* @private
|
|
272
288
|
*/
|
|
273
|
-
_saveState() {
|
|
289
|
+
_saveState( { view } ) {
|
|
274
290
|
|
|
275
291
|
const app = this._app;
|
|
276
292
|
const effective = app.settings.getEffective();
|
|
293
|
+
const keys = Object.keys( modePresetSettings( PRODUCTION_RENDER_CONFIG ) );
|
|
294
|
+
if ( view ) keys.push( 'focusDistance' );
|
|
277
295
|
|
|
278
296
|
return {
|
|
297
|
+
view: view ? app.cameraManager.captureView() : null,
|
|
279
298
|
// Everything configureForMode( 'production' ) overwrites, with who set it.
|
|
280
|
-
settings:
|
|
299
|
+
settings: keys.map( key => [ key, effective[ key ] ] ),
|
|
281
300
|
renderMode: app.stages.pathTracer?.renderMode?.value,
|
|
282
301
|
controlsEnabled: app.cameraManager.controls?.enabled,
|
|
283
302
|
oidnEnabled: app.denoisingManager?.finalDenoise,
|
|
@@ -321,6 +340,8 @@ export class VideoRenderManager {
|
|
|
321
340
|
|
|
322
341
|
app.pauseRendering = state.pauseRendering ?? false;
|
|
323
342
|
|
|
343
|
+
if ( state.view ) app.cameraManager.applyPose( state.view );
|
|
344
|
+
|
|
324
345
|
// configureForMode( 'production' ) suspended the live-view refresh and nothing here switches
|
|
325
346
|
// back, so the viewport would stay un-denoised until the host changed tab.
|
|
326
347
|
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
|
+
}
|