rayzee 9.0.0 → 9.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +139 -8
- package/dist/{NeuralRetouch-CYOJ21Wn.js → NeuralRetouch-BRquqfmZ.js} +1 -1
- package/dist/{NeuralSuperRes-CQLCLpcC.js → NeuralSuperRes-CbNvQjCw.js} +3 -2
- package/dist/{ToneMapGPU-BVE852iN.js → ToneMapGPU-DpUZDMbQ.js} +2 -1
- package/dist/rayzee.es.js +10353 -5337
- package/dist/rayzee.umd.js +265 -265
- package/package.json +1 -1
- package/src/AssetConfig.js +4 -0
- package/src/EngineDefaults.js +38 -56
- package/src/EngineEvents.js +3 -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/PathTracerApp.js +290 -19
- package/src/Processor/ArchiveCache.js +120 -0
- package/src/Processor/ArchiveReader.js +179 -32
- package/src/Processor/AssetLoader.js +514 -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/KernelManager.js +4 -1
- package/src/Processor/PBRT/PBRTCurves.js +104 -27
- package/src/Processor/PBRT/PBRTParser.js +132 -22
- package/src/Processor/PBRT/PBRTSceneBuilder.js +192 -49
- 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 +38 -0
- package/src/SceneState/SceneState.js +367 -0
- package/src/SceneState/portable.js +52 -0
- package/src/Stages/NormalDepth.js +7 -6
- package/src/Stages/PathTracer.js +108 -4
- 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/Common.js +11 -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 +3 -3
- 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 +18 -0
- package/src/managers/CameraManager.js +169 -1
- package/src/managers/EnvironmentManager.js +263 -33
- package/src/managers/GoboManager.js +19 -8
- package/src/managers/IESManager.js +3 -3
- 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 +5 -3
- package/src/managers/VideoRenderManager.js +3 -1
- package/src/managers/timeline/CameraTrack.js +22 -0
- package/src/managers/timeline/TimelineManager.js +14 -0
- package/src/version.js +3 -0
- package/src/Processor/ProceduralSky.js +0 -291
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads a .zip in place: the central directory at the end names every entry and where it
|
|
3
|
+
* starts, so one entry is read and inflated at a time and the archive is never resident.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { inflateSync } from 'three/addons/libs/fflate.module.js';
|
|
7
|
+
|
|
8
|
+
const EOCD = 0x06054b50;
|
|
9
|
+
const ZIP64_LOCATOR = 0x07064b50;
|
|
10
|
+
const ZIP64_EOCD = 0x06064b50;
|
|
11
|
+
const CENTRAL = 0x02014b50;
|
|
12
|
+
const LOCAL = 0x04034b50;
|
|
13
|
+
const EOCD_MAX_SEARCH = 65535 + 22;
|
|
14
|
+
|
|
15
|
+
const utf8 = new TextDecoder();
|
|
16
|
+
|
|
17
|
+
async function bytes( blob, start, end ) {
|
|
18
|
+
|
|
19
|
+
return new Uint8Array( await blob.slice( start, end ).arrayBuffer() );
|
|
20
|
+
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function u64( view, offset ) {
|
|
24
|
+
|
|
25
|
+
return view.getUint32( offset + 4, true ) * 0x100000000 + view.getUint32( offset, true );
|
|
26
|
+
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function latin1( b ) {
|
|
30
|
+
|
|
31
|
+
let out = '';
|
|
32
|
+
for ( let i = 0; i < b.length; i += 8192 ) out += String.fromCharCode( ...b.subarray( i, i + 8192 ) );
|
|
33
|
+
return out;
|
|
34
|
+
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {Blob|File} blob
|
|
39
|
+
* @returns {Promise<Array<{name:string, size:number, compSize:number, method:number, local:number, encrypted:boolean}>>}
|
|
40
|
+
*/
|
|
41
|
+
export async function readZipDirectory( blob ) {
|
|
42
|
+
|
|
43
|
+
const size = blob.size;
|
|
44
|
+
const tailStart = Math.max( 0, size - EOCD_MAX_SEARCH - 20 );
|
|
45
|
+
const tail = await bytes( blob, tailStart, size );
|
|
46
|
+
const tv = new DataView( tail.buffer );
|
|
47
|
+
|
|
48
|
+
let eocd = - 1;
|
|
49
|
+
for ( let i = tail.length - 22; i >= 0; i -- ) {
|
|
50
|
+
|
|
51
|
+
if ( tv.getUint32( i, true ) === EOCD ) {
|
|
52
|
+
|
|
53
|
+
eocd = i;
|
|
54
|
+
break;
|
|
55
|
+
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if ( eocd < 0 ) throw new Error( 'zip: end of central directory not found' );
|
|
61
|
+
|
|
62
|
+
let count = tv.getUint16( eocd + 10, true );
|
|
63
|
+
let cdSize = tv.getUint32( eocd + 12, true );
|
|
64
|
+
let cdOffset = tv.getUint32( eocd + 16, true );
|
|
65
|
+
|
|
66
|
+
if ( count === 0xffff || cdSize === 0xffffffff || cdOffset === 0xffffffff ) {
|
|
67
|
+
|
|
68
|
+
const locator = eocd - 20;
|
|
69
|
+
if ( locator < 0 || tv.getUint32( locator, true ) !== ZIP64_LOCATOR ) throw new Error( 'zip: ZIP64 locator missing' );
|
|
70
|
+
const recordOffset = u64( tv, locator + 8 );
|
|
71
|
+
const record = new DataView( ( await bytes( blob, recordOffset, recordOffset + 56 ) ).buffer );
|
|
72
|
+
if ( record.getUint32( 0, true ) !== ZIP64_EOCD ) throw new Error( 'zip: ZIP64 end record missing' );
|
|
73
|
+
count = u64( record, 32 );
|
|
74
|
+
cdSize = u64( record, 40 );
|
|
75
|
+
cdOffset = u64( record, 48 );
|
|
76
|
+
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const cd = await bytes( blob, cdOffset, cdOffset + cdSize );
|
|
80
|
+
const v = new DataView( cd.buffer );
|
|
81
|
+
const entries = [];
|
|
82
|
+
|
|
83
|
+
let p = 0;
|
|
84
|
+
for ( let i = 0; i < count && p + 46 <= cd.length; i ++ ) {
|
|
85
|
+
|
|
86
|
+
if ( v.getUint32( p, true ) !== CENTRAL ) throw new Error( 'zip: corrupt central directory' );
|
|
87
|
+
|
|
88
|
+
const flags = v.getUint16( p + 8, true );
|
|
89
|
+
const method = v.getUint16( p + 10, true );
|
|
90
|
+
let compSize = v.getUint32( p + 20, true );
|
|
91
|
+
let entrySize = v.getUint32( p + 24, true );
|
|
92
|
+
const nameLen = v.getUint16( p + 28, true );
|
|
93
|
+
const extraLen = v.getUint16( p + 30, true );
|
|
94
|
+
const commentLen = v.getUint16( p + 32, true );
|
|
95
|
+
let local = v.getUint32( p + 42, true );
|
|
96
|
+
|
|
97
|
+
const rawName = cd.subarray( p + 46, p + 46 + nameLen );
|
|
98
|
+
const name = flags & 0x800 ? utf8.decode( rawName ) : latin1( rawName );
|
|
99
|
+
|
|
100
|
+
// ZIP64 extra field: only the saturated 32-bit fields are present, in this order.
|
|
101
|
+
for ( let e = p + 46 + nameLen, end = e + extraLen; e + 4 <= end; ) {
|
|
102
|
+
|
|
103
|
+
const id = v.getUint16( e, true );
|
|
104
|
+
const len = v.getUint16( e + 2, true );
|
|
105
|
+
if ( id === 0x0001 ) {
|
|
106
|
+
|
|
107
|
+
let q = e + 4;
|
|
108
|
+
if ( entrySize === 0xffffffff ) {
|
|
109
|
+
|
|
110
|
+
entrySize = u64( v, q );
|
|
111
|
+
q += 8;
|
|
112
|
+
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if ( compSize === 0xffffffff ) {
|
|
116
|
+
|
|
117
|
+
compSize = u64( v, q );
|
|
118
|
+
q += 8;
|
|
119
|
+
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if ( local === 0xffffffff ) local = u64( v, q );
|
|
123
|
+
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
e += 4 + len;
|
|
127
|
+
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if ( ! name.endsWith( '/' ) ) entries.push( { name, size: entrySize, compSize, method, local, encrypted: ( flags & 1 ) !== 0 } );
|
|
131
|
+
p += 46 + nameLen + extraLen + commentLen;
|
|
132
|
+
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return entries;
|
|
136
|
+
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function dataStart( blob, entry ) {
|
|
140
|
+
|
|
141
|
+
if ( entry.encrypted ) throw new Error( `zip: "${entry.name}" is encrypted` );
|
|
142
|
+
|
|
143
|
+
const head = new DataView( ( await bytes( blob, entry.local, entry.local + 30 ) ).buffer );
|
|
144
|
+
if ( head.getUint32( 0, true ) !== LOCAL ) throw new Error( `zip: bad local header for "${entry.name}"` );
|
|
145
|
+
return entry.local + 30 + head.getUint16( 26, true ) + head.getUint16( 28, true );
|
|
146
|
+
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function readEntry( blob, entry ) {
|
|
150
|
+
|
|
151
|
+
const start = await dataStart( blob, entry );
|
|
152
|
+
const raw = await bytes( blob, start, start + entry.compSize );
|
|
153
|
+
|
|
154
|
+
if ( entry.method === 0 ) return raw;
|
|
155
|
+
if ( entry.method === 8 ) return entry.size === 0 ? new Uint8Array( 0 ) : inflateSync( raw, { out: new Uint8Array( entry.size ) } );
|
|
156
|
+
throw new Error( `zip: "${entry.name}" uses compression method ${entry.method}, which is not supported` );
|
|
157
|
+
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Same shape as `openTar`: `listing` holds every entry, with an `offset` on those `filter` keeps,
|
|
162
|
+
* and `read( path )` inflates one on demand; `slice( path )` gives it as a Blob. Paths are the archive's own, as fflate's unzipSync keys them.
|
|
163
|
+
* @param {Blob|File} blob
|
|
164
|
+
* @param {{filter?: (path:string, size:number) => boolean}} [options]
|
|
165
|
+
*/
|
|
166
|
+
export async function openZip( blob, { filter = null } = {} ) {
|
|
167
|
+
|
|
168
|
+
const directory = await readZipDirectory( blob );
|
|
169
|
+
const byPath = new Map();
|
|
170
|
+
|
|
171
|
+
const listing = directory.map( e => {
|
|
172
|
+
|
|
173
|
+
if ( filter && ! filter( e.name, e.size ) ) return { path: e.name, size: e.size };
|
|
174
|
+
byPath.set( e.name, e );
|
|
175
|
+
return { path: e.name, size: e.size, offset: e.local };
|
|
176
|
+
|
|
177
|
+
} );
|
|
178
|
+
|
|
179
|
+
const read = async ( path ) => {
|
|
180
|
+
|
|
181
|
+
const entry = byPath.get( path );
|
|
182
|
+
return entry ? readEntry( blob, entry ) : null;
|
|
183
|
+
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// A stored entry is a slice of the archive itself, so a large one never passes through memory.
|
|
187
|
+
const slice = async ( path ) => {
|
|
188
|
+
|
|
189
|
+
const entry = byPath.get( path );
|
|
190
|
+
if ( ! entry ) return null;
|
|
191
|
+
if ( entry.method !== 0 ) return new Blob( [ await readEntry( blob, entry ) ] );
|
|
192
|
+
const start = await dataStart( blob, entry );
|
|
193
|
+
return blob.slice( start, start + entry.compSize );
|
|
194
|
+
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// A stored entry's head is a slice; a deflated one is inflated whole, as a read would.
|
|
198
|
+
const readHead = async ( path, bytes ) => {
|
|
199
|
+
|
|
200
|
+
const part = await slice( path );
|
|
201
|
+
return part ? new Uint8Array( await part.slice( 0, bytes ).arrayBuffer() ) : null;
|
|
202
|
+
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
return { entries: Object.create( null ), listing, read, readHead, slice, retainedBytes: 0, truncated: false, indexed: byPath.size };
|
|
206
|
+
|
|
207
|
+
}
|
package/src/RenderSettings.js
CHANGED
|
@@ -2,6 +2,7 @@ import { EventDispatcher, Color, Vector2, MathUtils } from 'three';
|
|
|
2
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
|
+
import { toPortable, fromPortable } from './SceneState/portable.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Routing table: maps each setting key to its target stage/handler.
|
|
@@ -424,6 +425,43 @@ export class RenderSettings extends EventDispatcher {
|
|
|
424
425
|
|
|
425
426
|
}
|
|
426
427
|
|
|
428
|
+
/** The settings a host set, as portable values (see SceneState/portable.js): what a session restores. */
|
|
429
|
+
serialize() {
|
|
430
|
+
|
|
431
|
+
const out = {};
|
|
432
|
+
for ( const [ key, value ] of this._values ) {
|
|
433
|
+
|
|
434
|
+
if ( this._sources.get( key ) !== SETTING_SOURCE.HOST || ! SETTING_ROUTES[ key ] ) continue;
|
|
435
|
+
const portable = toPortable( value );
|
|
436
|
+
if ( portable !== undefined ) out[ key ] = portable;
|
|
437
|
+
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return out;
|
|
441
|
+
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Applies {@link serialize}'s values as host settings, once reset at the end. Keys this engine
|
|
446
|
+
* no longer routes are left out rather than recorded as unknown.
|
|
447
|
+
* @returns {string[]} the keys left out
|
|
448
|
+
*/
|
|
449
|
+
restore( values ) {
|
|
450
|
+
|
|
451
|
+
const known = {};
|
|
452
|
+
const unknown = [];
|
|
453
|
+
for ( const [ key, value ] of Object.entries( values ?? {} ) ) {
|
|
454
|
+
|
|
455
|
+
if ( SETTING_ROUTES[ key ] ) known[ key ] = fromPortable( value );
|
|
456
|
+
else unknown.push( key );
|
|
457
|
+
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
this.setMany( known );
|
|
461
|
+
return unknown;
|
|
462
|
+
|
|
463
|
+
}
|
|
464
|
+
|
|
427
465
|
/**
|
|
428
466
|
* Pushes all stored values to their target stages.
|
|
429
467
|
* Called after loadSceneData() to ensure GPU uniforms match stored values.
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A scene's state after it loaded: everything a host or user changed, as plain data. Saved
|
|
3
|
+
* sessions and project files carry it; the model itself is the host's to reload first.
|
|
4
|
+
*
|
|
5
|
+
* Objects, materials and cameras are matched by position in this load plus a name check, never by
|
|
6
|
+
* UUID — those change on every load.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { VERSION } from '../version.js';
|
|
10
|
+
import { fromPortable } from './portable.js';
|
|
11
|
+
|
|
12
|
+
export const SCENE_STATE_VERSION = 1;
|
|
13
|
+
|
|
14
|
+
const isUrl = source => typeof source === 'string' && /^https?:\/\//.test( source );
|
|
15
|
+
|
|
16
|
+
function pathOf( object, root ) {
|
|
17
|
+
|
|
18
|
+
const path = [];
|
|
19
|
+
while ( object && object !== root ) {
|
|
20
|
+
|
|
21
|
+
const parent = object.parent;
|
|
22
|
+
if ( ! parent ) return null;
|
|
23
|
+
path.unshift( parent.children.indexOf( object ) );
|
|
24
|
+
object = parent;
|
|
25
|
+
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return object === root ? path : null;
|
|
29
|
+
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function objectAt( root, path ) {
|
|
33
|
+
|
|
34
|
+
let object = root;
|
|
35
|
+
for ( const index of path ) {
|
|
36
|
+
|
|
37
|
+
object = object?.children[ index ];
|
|
38
|
+
if ( ! object ) return null;
|
|
39
|
+
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return object;
|
|
43
|
+
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function signature( app ) {
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
key: app.assetLoader?.sceneSourceKey ?? null,
|
|
50
|
+
name: app.sceneModel?.name ?? null,
|
|
51
|
+
meshes: app.sceneMeshes.length,
|
|
52
|
+
materials: app.stages.pathTracer?.materialData.materialCount ?? 0,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function materialsByIndex( app ) {
|
|
58
|
+
|
|
59
|
+
const out = new Map();
|
|
60
|
+
for ( const mesh of app.sceneMeshes ) {
|
|
61
|
+
|
|
62
|
+
const index = mesh.userData?.materialIndex;
|
|
63
|
+
if ( index !== undefined && ! out.has( index ) ) out.set( index, Array.isArray( mesh.material ) ? mesh.material[ 0 ] : mesh.material );
|
|
64
|
+
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return out;
|
|
68
|
+
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function colorState( cm ) {
|
|
72
|
+
|
|
73
|
+
if ( ! cm ) return null;
|
|
74
|
+
const status = cm.status();
|
|
75
|
+
const view = status.activeView;
|
|
76
|
+
const configId = status.config?.id ?? view?.configId ?? null;
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
config: configId ? { id: configId } : null,
|
|
80
|
+
view: view ? { display: view.display, view: view.view, look: view.look ?? null } : null,
|
|
81
|
+
transform: view ? null : status.activeTransform?.id ?? null,
|
|
82
|
+
workingSpace: status.workingSpaceAdopted ? status.workingSpace : null,
|
|
83
|
+
context: status.context ?? null,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @param {import('../PathTracerApp.js').PathTracerApp} app
|
|
90
|
+
* @returns {Object} JSON-safe
|
|
91
|
+
*/
|
|
92
|
+
export function captureSceneState( app ) {
|
|
93
|
+
|
|
94
|
+
const root = app.meshScene;
|
|
95
|
+
const materials = materialsByIndex( app );
|
|
96
|
+
|
|
97
|
+
const hidden = [];
|
|
98
|
+
root.traverse( object => {
|
|
99
|
+
|
|
100
|
+
if ( object !== root && object.visible === false ) hidden.push( pathOf( object, root ) );
|
|
101
|
+
|
|
102
|
+
} );
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
v: SCENE_STATE_VERSION,
|
|
106
|
+
engine: VERSION,
|
|
107
|
+
savedAt: Date.now(),
|
|
108
|
+
scene: signature( app ),
|
|
109
|
+
appended: root.children
|
|
110
|
+
.filter( c => c !== app.sceneModel && c.userData?.__rayzeeSceneObject )
|
|
111
|
+
.map( c => ( { name: c.userData.__rayzeeName ?? c.name, url: c.userData.__rayzeeSourceUrl ?? null, cacheKey: c.userData.__rayzeeCacheKey ?? null } ) ),
|
|
112
|
+
settings: app.settings.serialize(),
|
|
113
|
+
environment: app.environmentManager?.serialize() ?? null,
|
|
114
|
+
color: colorState( app.color ),
|
|
115
|
+
lights: app.lightManager.serialize(),
|
|
116
|
+
cameras: app.cameraManager.serialize(),
|
|
117
|
+
timeline: app.timeline?.serialize() ?? null,
|
|
118
|
+
materials: ( app.stages.pathTracer?.materialData.serializeHostEdits() ?? [] ).map( edit => ( { ...edit, name: materials.get( edit.index )?.name ?? '' } ) ),
|
|
119
|
+
hidden,
|
|
120
|
+
moved: ( app.transformManager?.movedObjects ?? [] ).map( object => ( {
|
|
121
|
+
path: pathOf( object, root ),
|
|
122
|
+
name: object.name,
|
|
123
|
+
position: object.position.toArray(),
|
|
124
|
+
quaternion: object.quaternion.toArray(),
|
|
125
|
+
scale: object.scale.toArray(),
|
|
126
|
+
} ) ).filter( m => m.path ),
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function applyMaterialValue( material, property, value ) {
|
|
132
|
+
|
|
133
|
+
const current = material[ property ];
|
|
134
|
+
if ( current?.copy && ( value?.isColor || value?.isVector2 || value?.isVector3 ) ) current.copy( value );
|
|
135
|
+
else material[ property ] = value;
|
|
136
|
+
material.needsUpdate = true;
|
|
137
|
+
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function restoreEnvironment( app, env, resolve, skip ) {
|
|
141
|
+
|
|
142
|
+
if ( ! env ) return;
|
|
143
|
+
const manager = app.environmentManager;
|
|
144
|
+
|
|
145
|
+
if ( env.hdri && env.hdri !== manager.hdriSource ) {
|
|
146
|
+
|
|
147
|
+
const input = isUrl( env.hdri ) ? env.hdri : await resolve( { kind: 'environment', source: env.hdri } );
|
|
148
|
+
try {
|
|
149
|
+
|
|
150
|
+
if ( typeof input === 'string' ) await app.loadEnvironment( input );
|
|
151
|
+
else if ( input ) await app.loadFile( input );
|
|
152
|
+
else skip( 'environment', 'source unavailable', { source: env.hdri } );
|
|
153
|
+
|
|
154
|
+
} catch ( error ) {
|
|
155
|
+
|
|
156
|
+
skip( 'environment', error.message, { source: env.hdri } );
|
|
157
|
+
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
await manager.restore( env );
|
|
163
|
+
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function restoreColor( app, color, resolve, skip ) {
|
|
167
|
+
|
|
168
|
+
const cm = app.color;
|
|
169
|
+
if ( ! color || ! cm ) return;
|
|
170
|
+
|
|
171
|
+
const status = cm.status();
|
|
172
|
+
const active = status.activeView;
|
|
173
|
+
const sameView = !! color.view && !! active
|
|
174
|
+
&& active.display === color.view.display && active.view === color.view.view && ( active.look ?? null ) === color.view.look;
|
|
175
|
+
const loadedId = status.config?.id ?? null;
|
|
176
|
+
const shownId = loadedId ?? active?.configId ?? null;
|
|
177
|
+
const settled = ( color.config?.id ?? null ) === shownId && ! color.workingSpace && ! color.context
|
|
178
|
+
&& ( color.view ? sameView : ! active && ( ! color.transform || color.transform === status.activeTransform?.id ) );
|
|
179
|
+
if ( settled ) return;
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
|
|
183
|
+
if ( color.config && color.config.id !== loadedId ) {
|
|
184
|
+
|
|
185
|
+
if ( ! await resolve( { kind: 'colorConfig', config: color.config } ) ) {
|
|
186
|
+
|
|
187
|
+
const builtins = await cm.constructor.listBuiltinConfigs().catch( () => [] );
|
|
188
|
+
const known = builtins.some( b => ( b.name ?? b ) === color.config.id || `ocio://${b.name ?? b}` === color.config.id );
|
|
189
|
+
if ( ! known ) {
|
|
190
|
+
|
|
191
|
+
skip( 'color', 'config unavailable', { config: color.config.id } );
|
|
192
|
+
return;
|
|
193
|
+
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
await app.loadColorConfig( { builtin: color.config.id, registerViews: false } );
|
|
197
|
+
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
} else if ( ! color.config && loadedId ) {
|
|
201
|
+
|
|
202
|
+
await app.unloadColorConfig();
|
|
203
|
+
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if ( color.context ) cm.setContext( color.context );
|
|
207
|
+
if ( color.view ) cm.setView( color.view );
|
|
208
|
+
else if ( color.transform ) cm.setActiveView( color.transform );
|
|
209
|
+
|
|
210
|
+
if ( ( color.workingSpace ?? null ) !== ( cm.workingSpaceAdopted ? cm.workingSpace : null ) ) {
|
|
211
|
+
|
|
212
|
+
cm.setWorkingSpace( color.workingSpace );
|
|
213
|
+
await app.applyColorWorkingSpace();
|
|
214
|
+
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
} catch ( error ) {
|
|
218
|
+
|
|
219
|
+
skip( 'color', error.message, { config: color.config?.id ?? null } );
|
|
220
|
+
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Applies a captured state to the scene now loaded. Call once the model has loaded.
|
|
227
|
+
*
|
|
228
|
+
* @param {import('../PathTracerApp.js').PathTracerApp} app
|
|
229
|
+
* @param {Object} state - from {@link captureSceneState}
|
|
230
|
+
* @param {Object} [options]
|
|
231
|
+
* @param {function(Object): Promise<*>} [options.resolve] - the host's answer for an input the
|
|
232
|
+
* engine cannot reach itself: `{ kind: 'environment', source }` → a File, a URL or null;
|
|
233
|
+
* `{ kind: 'colorConfig', config }` → true once the host has loaded it
|
|
234
|
+
* @returns {Promise<{skipped: Array<{section: string, reason: string}>}>} what could not be put back
|
|
235
|
+
*/
|
|
236
|
+
export async function applySceneState( app, state, { resolve = async () => null } = {} ) {
|
|
237
|
+
|
|
238
|
+
if ( state?.v !== SCENE_STATE_VERSION ) throw new Error( `scene state version ${state?.v} is not ${SCENE_STATE_VERSION}` );
|
|
239
|
+
|
|
240
|
+
const skipped = [];
|
|
241
|
+
const skip = ( section, reason, detail = {} ) => skipped.push( { section, reason, ...detail } );
|
|
242
|
+
|
|
243
|
+
for ( const model of state.appended ?? [] ) {
|
|
244
|
+
|
|
245
|
+
if ( ! model.url ) {
|
|
246
|
+
|
|
247
|
+
skip( 'appended', 'added from memory, not a file', { name: model.name } );
|
|
248
|
+
continue;
|
|
249
|
+
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
|
|
254
|
+
await app.addModel( model.url, { name: model.name, cacheKey: model.cacheKey ?? undefined } );
|
|
255
|
+
|
|
256
|
+
} catch ( error ) {
|
|
257
|
+
|
|
258
|
+
skip( 'appended', error.message, { name: model.name } );
|
|
259
|
+
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const now = signature( app );
|
|
265
|
+
const saved = state.scene ?? {};
|
|
266
|
+
const sameScene = saved.meshes === now.meshes && saved.materials === now.materials;
|
|
267
|
+
if ( ! sameScene ) skip( 'scene', 'the loaded scene is not the one saved: objects and materials left as loaded', { saved, now } );
|
|
268
|
+
|
|
269
|
+
await restoreEnvironment( app, state.environment, resolve, skip );
|
|
270
|
+
await restoreColor( app, state.color, resolve, skip );
|
|
271
|
+
|
|
272
|
+
const unknown = app.settings.restore( state.settings );
|
|
273
|
+
if ( unknown.length ) skip( 'settings', 'not settings in this engine', { keys: unknown } );
|
|
274
|
+
|
|
275
|
+
const lights = app.lightManager.restore( state.lights );
|
|
276
|
+
for ( const light of lights ) {
|
|
277
|
+
|
|
278
|
+
const { gobo, ies } = light.userData;
|
|
279
|
+
if ( gobo && app.goboManager?.entries?.length ) app.goboManager.setLightGobo( light.uuid, gobo.name, gobo );
|
|
280
|
+
if ( ies && app.iesManager?.entries?.length ) app.iesManager.setSpotLightProfile( light.uuid, ies.name, ies.intensity, { applyAutoCone: false } );
|
|
281
|
+
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if ( sameScene ) {
|
|
285
|
+
|
|
286
|
+
const materials = materialsByIndex( app );
|
|
287
|
+
for ( const edit of state.materials ?? [] ) {
|
|
288
|
+
|
|
289
|
+
const material = materials.get( edit.index );
|
|
290
|
+
if ( ! material || ( material.name ?? '' ) !== edit.name ) {
|
|
291
|
+
|
|
292
|
+
skip( 'materials', 'no such material', { index: edit.index, name: edit.name } );
|
|
293
|
+
continue;
|
|
294
|
+
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
for ( const [ property, portable ] of Object.entries( edit.props ) ) {
|
|
298
|
+
|
|
299
|
+
const value = fromPortable( portable );
|
|
300
|
+
applyMaterialValue( material, property, value );
|
|
301
|
+
app.setMaterialProperty( edit.index, property, value );
|
|
302
|
+
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const root = app.meshScene;
|
|
308
|
+
const hidden = new Set( ( state.hidden ?? [] ).map( path => objectAt( root, path ) ).filter( Boolean ) );
|
|
309
|
+
root.traverse( object => {
|
|
310
|
+
|
|
311
|
+
if ( object !== root && ! object.isLight ) object.visible = ! hidden.has( object );
|
|
312
|
+
|
|
313
|
+
} );
|
|
314
|
+
app.updateAllMeshVisibility();
|
|
315
|
+
|
|
316
|
+
const moved = [];
|
|
317
|
+
for ( const entry of state.moved ?? [] ) {
|
|
318
|
+
|
|
319
|
+
const object = objectAt( root, entry.path );
|
|
320
|
+
if ( ! object || object.name !== entry.name ) {
|
|
321
|
+
|
|
322
|
+
skip( 'moved', 'no such object', { name: entry.name } );
|
|
323
|
+
continue;
|
|
324
|
+
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
object.position.fromArray( entry.position );
|
|
328
|
+
object.quaternion.fromArray( entry.quaternion );
|
|
329
|
+
object.scale.fromArray( entry.scale );
|
|
330
|
+
object.updateMatrixWorld( true );
|
|
331
|
+
app.transformManager?.noteMoved( object );
|
|
332
|
+
moved.push( object );
|
|
333
|
+
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if ( moved.length ) {
|
|
337
|
+
|
|
338
|
+
const affected = [];
|
|
339
|
+
app.sceneMeshes.forEach( ( mesh, index ) => {
|
|
340
|
+
|
|
341
|
+
for ( let o = mesh; o; o = o.parent ) {
|
|
342
|
+
|
|
343
|
+
if ( moved.includes( o ) ) {
|
|
344
|
+
|
|
345
|
+
affected.push( index );
|
|
346
|
+
break;
|
|
347
|
+
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
} );
|
|
353
|
+
if ( affected.length ) app.updateMeshTransforms( affected );
|
|
354
|
+
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const { mismatched } = app.cameraManager.restore( state.cameras );
|
|
360
|
+
if ( mismatched.length ) skip( 'cameras', 'model cameras changed', { names: mismatched } );
|
|
361
|
+
app._dispatchCamerasUpdated();
|
|
362
|
+
app.timeline?.restore( state.timeline );
|
|
363
|
+
|
|
364
|
+
app.reset();
|
|
365
|
+
return { skipped };
|
|
366
|
+
|
|
367
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Color, Quaternion, Vector2, Vector3, Vector4 } from 'three';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* JSON-safe copies of setting and material values: colours, vectors and non-finite numbers are
|
|
5
|
+
* tagged so {@link fromPortable} gives back the same types. Anything else that is not plain data
|
|
6
|
+
* comes back undefined, so a caller can leave it out.
|
|
7
|
+
*/
|
|
8
|
+
export function toPortable( value ) {
|
|
9
|
+
|
|
10
|
+
if ( value === null || typeof value === 'boolean' || typeof value === 'string' ) return value;
|
|
11
|
+
if ( typeof value === 'number' ) return Number.isFinite( value ) ? value : { $num: String( value ) };
|
|
12
|
+
if ( typeof value !== 'object' ) return undefined;
|
|
13
|
+
if ( value.isColor ) return { $color: [ value.r, value.g, value.b ] };
|
|
14
|
+
if ( value.isQuaternion ) return { $quat: [ value.x, value.y, value.z, value.w ] };
|
|
15
|
+
if ( value.isVector2 || value.isVector3 || value.isVector4 ) return { $vec: value.toArray() };
|
|
16
|
+
if ( ArrayBuffer.isView( value ) ) return Array.from( value );
|
|
17
|
+
if ( Array.isArray( value ) ) return value.map( v => toPortable( v ) ?? null );
|
|
18
|
+
|
|
19
|
+
const proto = Object.getPrototypeOf( value );
|
|
20
|
+
if ( proto !== Object.prototype && proto !== null ) return undefined;
|
|
21
|
+
|
|
22
|
+
const out = {};
|
|
23
|
+
for ( const [ key, v ] of Object.entries( value ) ) {
|
|
24
|
+
|
|
25
|
+
const p = toPortable( v );
|
|
26
|
+
if ( p !== undefined ) out[ key ] = p;
|
|
27
|
+
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return out;
|
|
31
|
+
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function fromPortable( value ) {
|
|
35
|
+
|
|
36
|
+
if ( value === null || typeof value !== 'object' ) return value;
|
|
37
|
+
if ( Array.isArray( value ) ) return value.map( fromPortable );
|
|
38
|
+
if ( '$num' in value ) return Number( value.$num );
|
|
39
|
+
if ( value.$color ) return new Color( ...value.$color );
|
|
40
|
+
if ( value.$quat ) return new Quaternion( ...value.$quat );
|
|
41
|
+
if ( value.$vec ) {
|
|
42
|
+
|
|
43
|
+
const Vector = [ null, null, Vector2, Vector3, Vector4 ][ value.$vec.length ];
|
|
44
|
+
return new Vector( ...value.$vec );
|
|
45
|
+
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const out = {};
|
|
49
|
+
for ( const [ key, v ] of Object.entries( value ) ) out[ key ] = fromPortable( v );
|
|
50
|
+
return out;
|
|
51
|
+
|
|
52
|
+
}
|