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
|
@@ -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
|
@@ -1,7 +1,8 @@
|
|
|
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
|
+
import { toPortable, fromPortable } from './SceneState/portable.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Routing table: maps each setting key to its target stage/handler.
|
|
@@ -40,6 +41,9 @@ const SETTING_ROUTES = {
|
|
|
40
41
|
aperture: { uniform: 'aperture', reset: true },
|
|
41
42
|
apertureScale: { uniform: 'apertureScale', reset: true },
|
|
42
43
|
anamorphicRatio: { uniform: 'anamorphicRatio', reset: true },
|
|
44
|
+
unitsPerMetre: { uniform: 'unitsPerMetre', reset: true },
|
|
45
|
+
dofMode: { handler: 'handleDofMode', reset: true },
|
|
46
|
+
dofBlur: { uniform: 'dofBlur', reset: true },
|
|
43
47
|
samplingTechnique: { uniform: 'samplingTechnique', reset: true },
|
|
44
48
|
fireflyThreshold: { uniform: 'fireflyThreshold', reset: true },
|
|
45
49
|
shadowTerminatorOffset: { uniform: 'shadowTerminatorOffset', reset: true },
|
|
@@ -149,8 +153,8 @@ export class RenderSettings extends EventDispatcher {
|
|
|
149
153
|
* @param {Function} params.resetCallback - Called to reset accumulation
|
|
150
154
|
* @param {Function} [params.reconcileCompletion] - Called when completion limits change
|
|
151
155
|
* @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
156
|
* @param {Function} [params.onInteractionRenderScale] - Applies a new moving-camera render scale
|
|
157
|
+
* @param {Function} [params.onCameraProjection] - The camera's side of a `cameraProjection` change
|
|
154
158
|
*/
|
|
155
159
|
bind( params ) {
|
|
156
160
|
|
|
@@ -165,7 +169,7 @@ export class RenderSettings extends EventDispatcher {
|
|
|
165
169
|
* Builds handler functions for multi-stage settings that can't
|
|
166
170
|
* be routed with a simple uniform forward.
|
|
167
171
|
*/
|
|
168
|
-
_buildHandlers( { stages, renderer, resetCallback, reconcileCompletion, denoisingManager,
|
|
172
|
+
_buildHandlers( { stages, renderer, resetCallback, reconcileCompletion, denoisingManager, onInteractionRenderScale, onCameraProjection } ) {
|
|
169
173
|
|
|
170
174
|
// UniformManager copies into the existing node, so one scratch vector serves every write.
|
|
171
175
|
const panoScratch = new Vector2();
|
|
@@ -180,20 +184,20 @@ export class RenderSettings extends EventDispatcher {
|
|
|
180
184
|
|
|
181
185
|
handleCameraProjection: ( value ) => {
|
|
182
186
|
|
|
183
|
-
|
|
184
|
-
|
|
187
|
+
stages.pathTracer?.setUniform( 'cameraProjection', CAMERA_PROJECTION_IDS[ value ] ?? CAMERA_PROJECTION_IDS.perspective );
|
|
188
|
+
onCameraProjection?.( value );
|
|
185
189
|
|
|
186
|
-
if (
|
|
190
|
+
if ( value !== 'equirectangular' ) return;
|
|
187
191
|
|
|
188
192
|
// MotionVector unprojects through projectionMatrixInverse, which is meaningless once
|
|
189
193
|
// every pixel is its own direction. Fall back to the spatial-only denoiser rather
|
|
190
194
|
// than leaving no strategy.
|
|
191
195
|
if ( denoisingManager?.requiresMotionVectors ) denoisingManager.setDenoiserStrategy( 'edgeaware' );
|
|
192
|
-
// Auto-focus raycasts via Raycaster.setFromCamera, which only knows the frustum.
|
|
193
|
-
cameraManager?.setAutoFocusMode( 'manual' );
|
|
194
196
|
|
|
195
197
|
},
|
|
196
198
|
|
|
199
|
+
handleDofMode: ( value ) => stages.pathTracer?.setUniform( 'dofMode', value === 'look' ? 1 : 0 ),
|
|
200
|
+
|
|
197
201
|
handlePanoramaLonRange: ( value ) => setPanoRange( 'panoLonRange', value ),
|
|
198
202
|
|
|
199
203
|
handlePanoramaLatRange: ( value ) => setPanoRange( 'panoLatRange', value ),
|
|
@@ -421,6 +425,43 @@ export class RenderSettings extends EventDispatcher {
|
|
|
421
425
|
|
|
422
426
|
}
|
|
423
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
|
+
|
|
424
465
|
/**
|
|
425
466
|
* Pushes all stored values to their target stages.
|
|
426
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
|
+
}
|