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,217 @@
|
|
|
1
|
+
import { SpillStore } from './SpillStore.js';
|
|
2
|
+
|
|
3
|
+
const PACK_BYTES = 32 * 1024 * 1024;
|
|
4
|
+
const OWN_WRITE_BYTES = PACK_BYTES / 4;
|
|
5
|
+
const READ_WINDOW = 64 * 1024 * 1024;
|
|
6
|
+
const EMPTY = new Map();
|
|
7
|
+
|
|
8
|
+
function empty( Type ) {
|
|
9
|
+
|
|
10
|
+
let array = EMPTY.get( Type );
|
|
11
|
+
if ( ! array ) EMPTY.set( Type, array = new Type( 0 ) );
|
|
12
|
+
return array;
|
|
13
|
+
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Holds three.js geometry on disk while a build has no use for it, and puts it back afterwards.
|
|
18
|
+
* Small arrays are packed into shared writes; each comes back in a buffer of its own. An array
|
|
19
|
+
* whose write fails stays in memory and is restored from there.
|
|
20
|
+
*/
|
|
21
|
+
export class GeometrySpill {
|
|
22
|
+
|
|
23
|
+
/** @returns {Promise<?GeometrySpill>} null without storage, or with no room */
|
|
24
|
+
static async create( storage, key, expectedBytes = 0 ) {
|
|
25
|
+
|
|
26
|
+
const store = await SpillStore.create( storage, key, 0, { label: 'Scene geometry (while building)', expectedBytes } );
|
|
27
|
+
return store ? new GeometrySpill( store ) : null;
|
|
28
|
+
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Whether every attribute is a plain BufferAttribute, the only kind whose array can be swapped. */
|
|
32
|
+
static canTake( geometry ) {
|
|
33
|
+
|
|
34
|
+
if ( Object.keys( geometry.morphAttributes ?? {} ).length ) return false;
|
|
35
|
+
for ( const name in geometry.attributes ) if ( ! geometry.attributes[ name ].isBufferAttribute ) return false;
|
|
36
|
+
return ! geometry.index || geometry.index.isBufferAttribute === true;
|
|
37
|
+
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
constructor( store ) {
|
|
41
|
+
|
|
42
|
+
this._store = store;
|
|
43
|
+
this._records = [];
|
|
44
|
+
this._geometries = [];
|
|
45
|
+
this._end = 0;
|
|
46
|
+
this._pack = null;
|
|
47
|
+
this._packAt = 0;
|
|
48
|
+
this._packUsed = 0;
|
|
49
|
+
this._packRecords = [];
|
|
50
|
+
this._chain = Promise.resolve();
|
|
51
|
+
this._queued = 0;
|
|
52
|
+
this.bytes = 0;
|
|
53
|
+
this.error = null;
|
|
54
|
+
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Bytes handed over and not yet on disk. */
|
|
58
|
+
get queuedBytes() {
|
|
59
|
+
|
|
60
|
+
return this._queued;
|
|
61
|
+
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Moves a geometry's arrays to disk, leaving empty arrays of the same type in their place. */
|
|
65
|
+
add( geometry ) {
|
|
66
|
+
|
|
67
|
+
const byArray = new Map();
|
|
68
|
+
const attributes = Object.values( geometry.attributes );
|
|
69
|
+
if ( geometry.index ) attributes.push( geometry.index );
|
|
70
|
+
|
|
71
|
+
for ( const attribute of attributes ) {
|
|
72
|
+
|
|
73
|
+
const array = attribute.array;
|
|
74
|
+
if ( ! array?.byteLength ) continue;
|
|
75
|
+
let record = byArray.get( array );
|
|
76
|
+
if ( ! record ) byArray.set( array, record = this._write( array ) );
|
|
77
|
+
record.attributes.push( attribute );
|
|
78
|
+
attribute.array = empty( array.constructor );
|
|
79
|
+
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Bounds computed from an empty array would outlive the restore.
|
|
83
|
+
this._geometries.push( { geometry, box: geometry.boundingBox !== null, sphere: geometry.boundingSphere !== null } );
|
|
84
|
+
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** @private */
|
|
88
|
+
_write( array ) {
|
|
89
|
+
|
|
90
|
+
const bytes = new Uint8Array( array.buffer, array.byteOffset, array.byteLength );
|
|
91
|
+
const record = { at: 0, byteLength: bytes.byteLength, Type: array.constructor, attributes: [], array: null, pack: null, offset: 0 };
|
|
92
|
+
this._records.push( record );
|
|
93
|
+
this.bytes += bytes.byteLength;
|
|
94
|
+
|
|
95
|
+
if ( bytes.byteLength >= OWN_WRITE_BYTES ) {
|
|
96
|
+
|
|
97
|
+
record.at = this._end;
|
|
98
|
+
record.array = array;
|
|
99
|
+
this._end += bytes.byteLength;
|
|
100
|
+
this._queue( record.at, bytes, [ record ] );
|
|
101
|
+
return record;
|
|
102
|
+
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if ( this._pack && this._packUsed + bytes.byteLength > PACK_BYTES ) this._flushPack();
|
|
106
|
+
if ( ! this._pack ) {
|
|
107
|
+
|
|
108
|
+
this._pack = new Uint8Array( PACK_BYTES );
|
|
109
|
+
this._packAt = this._end;
|
|
110
|
+
this._packUsed = 0;
|
|
111
|
+
this._end += PACK_BYTES;
|
|
112
|
+
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this._pack.set( bytes, this._packUsed );
|
|
116
|
+
record.at = this._packAt + this._packUsed;
|
|
117
|
+
record.pack = this._pack;
|
|
118
|
+
record.offset = this._packUsed;
|
|
119
|
+
this._packUsed += bytes.byteLength;
|
|
120
|
+
this._packRecords.push( record );
|
|
121
|
+
return record;
|
|
122
|
+
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** @private */
|
|
126
|
+
_flushPack() {
|
|
127
|
+
|
|
128
|
+
if ( ! this._pack ) return;
|
|
129
|
+
this._queue( this._packAt, this._pack.subarray( 0, this._packUsed ), this._packRecords );
|
|
130
|
+
this._pack = null;
|
|
131
|
+
this._packRecords = [];
|
|
132
|
+
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** @private */
|
|
136
|
+
_queue( at, bytes, records ) {
|
|
137
|
+
|
|
138
|
+
this._queued += bytes.byteLength;
|
|
139
|
+
this._chain = this._chain
|
|
140
|
+
.then( async () => {
|
|
141
|
+
|
|
142
|
+
if ( this.error ) return;
|
|
143
|
+
await this._store.writeAt( at, bytes );
|
|
144
|
+
for ( const record of records ) record.array = record.pack = null;
|
|
145
|
+
|
|
146
|
+
} )
|
|
147
|
+
.catch( error => void ( this.error ??= error ) )
|
|
148
|
+
.finally( () => void ( this._queued -= bytes.byteLength ) );
|
|
149
|
+
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Resolves once everything handed over so far is written (or has failed to be). */
|
|
153
|
+
written() {
|
|
154
|
+
|
|
155
|
+
this._flushPack();
|
|
156
|
+
return this._chain;
|
|
157
|
+
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Puts every array back where it was taken from, reading the file in large windows. */
|
|
161
|
+
async restore() {
|
|
162
|
+
|
|
163
|
+
await this.written();
|
|
164
|
+
const records = this._records.sort( ( a, b ) => a.at - b.at );
|
|
165
|
+
if ( records.some( r => ! r.array && ! r.pack ) ) await this._store.flush();
|
|
166
|
+
|
|
167
|
+
let window = null;
|
|
168
|
+
let windowAt = 0;
|
|
169
|
+
for ( const record of records ) {
|
|
170
|
+
|
|
171
|
+
let array;
|
|
172
|
+
if ( record.array ) array = record.array;
|
|
173
|
+
else if ( record.pack ) array = new record.Type( record.pack.slice( record.offset, record.offset + record.byteLength ).buffer );
|
|
174
|
+
else if ( record.byteLength > READ_WINDOW ) array = new record.Type( await this._store.readAt( record.at, record.byteLength ) );
|
|
175
|
+
else {
|
|
176
|
+
|
|
177
|
+
if ( ! window || record.at + record.byteLength > windowAt + window.byteLength ) {
|
|
178
|
+
|
|
179
|
+
windowAt = record.at;
|
|
180
|
+
window = await this._store.readAt( record.at, READ_WINDOW );
|
|
181
|
+
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
array = new record.Type( window.slice( record.at - windowAt, record.at - windowAt + record.byteLength ) );
|
|
185
|
+
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if ( array.byteLength !== record.byteLength ) throw new Error( 'geometry spill: an array came back short' );
|
|
189
|
+
for ( const attribute of record.attributes ) attribute.array = array;
|
|
190
|
+
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
for ( const { geometry, box, sphere } of this._geometries ) {
|
|
194
|
+
|
|
195
|
+
if ( ! box ) geometry.boundingBox = null;
|
|
196
|
+
if ( ! sphere ) geometry.boundingSphere = null;
|
|
197
|
+
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
this._records = [];
|
|
201
|
+
this._geometries = [];
|
|
202
|
+
this.bytes = 0;
|
|
203
|
+
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async dispose() {
|
|
207
|
+
|
|
208
|
+
await this._chain;
|
|
209
|
+
this._records = [];
|
|
210
|
+
this._geometries = [];
|
|
211
|
+
this._pack = null;
|
|
212
|
+
this._packRecords = [];
|
|
213
|
+
await this._store.dispose();
|
|
214
|
+
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
}
|
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binary round trip for the three.js scenes the pbrt builder produces, so a reopened archive
|
|
3
|
+
* skips parsing. Geometry, instance matrices and generated textures go to one binary file as
|
|
4
|
+
* raw sections; the tree, materials and animation go to JSON. Textures that came from the
|
|
5
|
+
* archive are stored as their path and decoded from it again.
|
|
6
|
+
*
|
|
7
|
+
* Deliberately narrow: an object type it does not know makes `encodeSceneGraph` throw
|
|
8
|
+
* `SceneGraphUnsupported`, and the scene is simply not cached.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
AnimationClip, BufferAttribute, BufferGeometry, DataTexture, Group, InstancedBufferAttribute, InstancedMesh,
|
|
13
|
+
MaterialLoader, Mesh, Object3D, OrthographicCamera, PerspectiveCamera,
|
|
14
|
+
} from 'three';
|
|
15
|
+
|
|
16
|
+
export const SCENE_GRAPH_FORMAT = 2;
|
|
17
|
+
export const ARCHIVE_PATH = '__rayzeeArchivePath';
|
|
18
|
+
export const ARCHIVE_LOADER = '__rayzeeArchiveLoader';
|
|
19
|
+
|
|
20
|
+
const ALIGN = 64;
|
|
21
|
+
const PIECE_BYTES = 32 << 20;
|
|
22
|
+
const READ_WINDOW = 64 << 20;
|
|
23
|
+
|
|
24
|
+
const ARRAYS = { Float32Array, Float64Array, Int8Array, Int16Array, Int32Array, Uint8Array, Uint16Array, Uint32Array, Uint8ClampedArray };
|
|
25
|
+
|
|
26
|
+
const TEXTURE_PROPS = [
|
|
27
|
+
'name', 'mapping', 'channel', 'wrapS', 'wrapT', 'magFilter', 'minFilter', 'anisotropy', 'format', 'type',
|
|
28
|
+
'flipY', 'generateMipmaps', 'premultiplyAlpha', 'unpackAlignment', 'colorSpace',
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const OBJECT_PROPS = [ 'name', 'visible', 'castShadow', 'receiveShadow', 'frustumCulled', 'renderOrder', 'matrixAutoUpdate' ];
|
|
32
|
+
|
|
33
|
+
export class SceneGraphUnsupported extends Error {
|
|
34
|
+
|
|
35
|
+
constructor( message ) {
|
|
36
|
+
|
|
37
|
+
super( message );
|
|
38
|
+
this.name = 'SceneGraphUnsupported';
|
|
39
|
+
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function kindOf( object ) {
|
|
45
|
+
|
|
46
|
+
if ( object.isInstancedMesh ) return 'InstancedMesh';
|
|
47
|
+
if ( object.isSkinnedMesh || object.isBatchedMesh ) return null;
|
|
48
|
+
if ( object.isMesh ) return 'Mesh';
|
|
49
|
+
if ( object.isPerspectiveCamera ) return 'PerspectiveCamera';
|
|
50
|
+
if ( object.isOrthographicCamera ) return 'OrthographicCamera';
|
|
51
|
+
if ( object.isLight || object.isPoints || object.isLine || object.isSprite || object.isLOD ) return null;
|
|
52
|
+
if ( object.isGroup ) return 'Group';
|
|
53
|
+
return object.constructor === Object3D ? 'Object3D' : null;
|
|
54
|
+
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function portableUserData( userData, where ) {
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
|
|
61
|
+
const clone = JSON.parse( JSON.stringify( userData ?? {} ) );
|
|
62
|
+
delete clone[ ARCHIVE_PATH ];
|
|
63
|
+
delete clone[ ARCHIVE_LOADER ];
|
|
64
|
+
return clone;
|
|
65
|
+
|
|
66
|
+
} catch {
|
|
67
|
+
|
|
68
|
+
throw new SceneGraphUnsupported( `${where}: userData is not plain JSON` );
|
|
69
|
+
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
class SectionWriter {
|
|
75
|
+
|
|
76
|
+
constructor() {
|
|
77
|
+
|
|
78
|
+
this.sections = [];
|
|
79
|
+
this.length = 0;
|
|
80
|
+
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
add( array ) {
|
|
84
|
+
|
|
85
|
+
const offset = Math.ceil( this.length / ALIGN ) * ALIGN;
|
|
86
|
+
this.sections.push( { offset, array } );
|
|
87
|
+
this.length = offset + array.byteLength;
|
|
88
|
+
return { offset, byteLength: array.byteLength, type: array.constructor.name, length: array.length };
|
|
89
|
+
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function encodeAttribute( attribute, sections, where ) {
|
|
95
|
+
|
|
96
|
+
if ( attribute.isInterleavedBufferAttribute || ! ArrayBuffer.isView( attribute.array ) ) {
|
|
97
|
+
|
|
98
|
+
throw new SceneGraphUnsupported( `${where}: interleaved or non-typed attribute` );
|
|
99
|
+
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
section: sections.add( attribute.array ),
|
|
104
|
+
itemSize: attribute.itemSize,
|
|
105
|
+
normalized: attribute.normalized === true,
|
|
106
|
+
name: attribute.name ?? '',
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function encodeGeometry( geometry, sections ) {
|
|
112
|
+
|
|
113
|
+
const where = `geometry ${geometry.name || geometry.uuid}`;
|
|
114
|
+
if ( Object.keys( geometry.morphAttributes ?? {} ).length ) throw new SceneGraphUnsupported( `${where}: morph targets` );
|
|
115
|
+
|
|
116
|
+
const attributes = {};
|
|
117
|
+
for ( const [ name, attribute ] of Object.entries( geometry.attributes ) ) attributes[ name ] = encodeAttribute( attribute, sections, where );
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
uuid: geometry.uuid,
|
|
121
|
+
name: geometry.name,
|
|
122
|
+
attributes,
|
|
123
|
+
index: geometry.index ? encodeAttribute( geometry.index, sections, where ) : null,
|
|
124
|
+
groups: geometry.groups.map( ( g ) => ( { ...g } ) ),
|
|
125
|
+
drawRange: { ...geometry.drawRange },
|
|
126
|
+
userData: portableUserData( geometry.userData, where ),
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function encodeTexture( texture, sections ) {
|
|
132
|
+
|
|
133
|
+
const record = { uuid: texture.uuid, userData: portableUserData( texture.userData, `texture ${texture.uuid}` ) };
|
|
134
|
+
for ( const prop of TEXTURE_PROPS ) record[ prop ] = texture[ prop ];
|
|
135
|
+
for ( const prop of [ 'repeat', 'offset', 'center' ] ) record[ prop ] = texture[ prop ].toArray();
|
|
136
|
+
record.rotation = texture.rotation;
|
|
137
|
+
|
|
138
|
+
const path = texture.userData?.[ ARCHIVE_PATH ];
|
|
139
|
+
if ( path ) {
|
|
140
|
+
|
|
141
|
+
record.path = path;
|
|
142
|
+
record.loader = texture.userData[ ARCHIVE_LOADER ] ?? 'image';
|
|
143
|
+
|
|
144
|
+
} else if ( texture.isDataTexture && ArrayBuffer.isView( texture.image?.data ) ) {
|
|
145
|
+
|
|
146
|
+
record.data = { section: sections.add( texture.image.data ), width: texture.image.width, height: texture.image.height };
|
|
147
|
+
|
|
148
|
+
} else {
|
|
149
|
+
|
|
150
|
+
throw new SceneGraphUnsupported( `texture ${texture.name || texture.uuid}: no archive path and no pixel data to keep` );
|
|
151
|
+
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return record;
|
|
155
|
+
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Material.toJSON keeps colours as 8-bit sRGB hex, which moves every albedo a little: keep the floats.
|
|
159
|
+
function encodeMaterial( material, meta ) {
|
|
160
|
+
|
|
161
|
+
const json = material.toJSON( meta );
|
|
162
|
+
json.exactColors = {};
|
|
163
|
+
for ( const [ key, value ] of Object.entries( material ) ) if ( value?.isColor ) json.exactColors[ key ] = [ value.r, value.g, value.b ];
|
|
164
|
+
// MaterialLoader sets `reflectivity` after `ior`, and that setter rewrites ior with rounding.
|
|
165
|
+
if ( material.ior !== undefined ) json.exactIor = material.ior;
|
|
166
|
+
return json;
|
|
167
|
+
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function collectTextures( material, out ) {
|
|
171
|
+
|
|
172
|
+
for ( const value of Object.values( material ) ) if ( value?.isTexture ) out.set( value.uuid, value );
|
|
173
|
+
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* @param {import('three').Object3D} root
|
|
178
|
+
* @param {{environment?: import('three').Texture, animations?: AnimationClip[], stats?: Object}} [extra]
|
|
179
|
+
* @returns {{manifest: Object, sections: Array<{offset:number, array: ArrayBufferView}>, byteLength: number}}
|
|
180
|
+
* @throws {SceneGraphUnsupported}
|
|
181
|
+
*/
|
|
182
|
+
export function encodeSceneGraph( root, { environment = null, animations = [], stats = {} } = {} ) {
|
|
183
|
+
|
|
184
|
+
const sections = new SectionWriter();
|
|
185
|
+
const nodes = [];
|
|
186
|
+
const geometries = new Map();
|
|
187
|
+
const materials = new Map();
|
|
188
|
+
const textures = new Map();
|
|
189
|
+
|
|
190
|
+
const visit = ( object, parent ) => {
|
|
191
|
+
|
|
192
|
+
const type = kindOf( object );
|
|
193
|
+
if ( ! type ) throw new SceneGraphUnsupported( `${object.type} "${object.name}" cannot be cached` );
|
|
194
|
+
|
|
195
|
+
const node = { type, uuid: object.uuid, parent, userData: portableUserData( object.userData, `${type} "${object.name}"` ) };
|
|
196
|
+
for ( const prop of OBJECT_PROPS ) node[ prop ] = object[ prop ];
|
|
197
|
+
node.position = object.position.toArray();
|
|
198
|
+
node.quaternion = object.quaternion.toArray();
|
|
199
|
+
node.scale = object.scale.toArray();
|
|
200
|
+
node.layers = object.layers.mask;
|
|
201
|
+
if ( ! object.matrixAutoUpdate ) node.matrix = object.matrix.toArray();
|
|
202
|
+
|
|
203
|
+
if ( object.isMesh ) {
|
|
204
|
+
|
|
205
|
+
geometries.set( object.geometry.uuid, object.geometry );
|
|
206
|
+
const list = Array.isArray( object.material ) ? object.material : [ object.material ];
|
|
207
|
+
for ( const material of list ) {
|
|
208
|
+
|
|
209
|
+
materials.set( material.uuid, material );
|
|
210
|
+
collectTextures( material, textures );
|
|
211
|
+
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
node.geometry = object.geometry.uuid;
|
|
215
|
+
node.material = Array.isArray( object.material ) ? list.map( ( m ) => m.uuid ) : object.material.uuid;
|
|
216
|
+
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if ( object.isInstancedMesh ) {
|
|
220
|
+
|
|
221
|
+
node.count = object.count;
|
|
222
|
+
node.instanceMatrix = sections.add( object.instanceMatrix.array );
|
|
223
|
+
node.instanceColor = object.instanceColor ? sections.add( object.instanceColor.array ) : null;
|
|
224
|
+
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if ( object.isCamera ) {
|
|
228
|
+
|
|
229
|
+
for ( const prop of [ 'fov', 'aspect', 'near', 'far', 'zoom', 'focus', 'filmGauge', 'filmOffset', 'left', 'right', 'top', 'bottom' ] ) {
|
|
230
|
+
|
|
231
|
+
if ( object[ prop ] !== undefined ) node[ prop ] = object[ prop ];
|
|
232
|
+
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const index = nodes.push( node ) - 1;
|
|
238
|
+
for ( const child of object.children ) visit( child, index );
|
|
239
|
+
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
visit( root, - 1 );
|
|
243
|
+
|
|
244
|
+
if ( environment ) textures.set( environment.uuid, environment );
|
|
245
|
+
|
|
246
|
+
const meta = { textures: {}, images: {} };
|
|
247
|
+
for ( const uuid of textures.keys() ) meta.textures[ uuid ] = { uuid };
|
|
248
|
+
|
|
249
|
+
const manifest = {
|
|
250
|
+
v: SCENE_GRAPH_FORMAT,
|
|
251
|
+
nodes,
|
|
252
|
+
geometries: [ ...geometries.values() ].map( ( g ) => encodeGeometry( g, sections ) ),
|
|
253
|
+
textures: [ ...textures.values() ].map( ( t ) => encodeTexture( t, sections ) ),
|
|
254
|
+
materials: [ ...materials.values() ].map( ( m ) => encodeMaterial( m, meta ) ),
|
|
255
|
+
animations: animations.map( ( clip ) => AnimationClip.toJSON( clip ) ),
|
|
256
|
+
environment: environment ? environment.uuid : null,
|
|
257
|
+
stats,
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
return { manifest, sections: sections.sections, byteLength: sections.length };
|
|
261
|
+
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Writes an encoded graph into a storage entry: `graph.json` and `data.bin`.
|
|
266
|
+
* @param {{release?: boolean}} [options] - drop each section's array once written, so a graph
|
|
267
|
+
* written during a build does not keep arrays the build has since replaced
|
|
268
|
+
*/
|
|
269
|
+
export async function writeSceneGraph( writer, { manifest, sections }, { release = false } = {} ) {
|
|
270
|
+
|
|
271
|
+
for ( const section of sections ) {
|
|
272
|
+
|
|
273
|
+
const { offset, array } = section;
|
|
274
|
+
const bytes = new Uint8Array( array.buffer, array.byteOffset, array.byteLength );
|
|
275
|
+
for ( let at = 0; at < bytes.length; at += PIECE_BYTES ) {
|
|
276
|
+
|
|
277
|
+
await writer.write( 'data.bin', bytes.subarray( at, Math.min( bytes.length, at + PIECE_BYTES ) ), { at: offset + at } );
|
|
278
|
+
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if ( release ) section.array = null;
|
|
282
|
+
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if ( sections.length === 0 ) await writer.write( 'data.bin', new Uint8Array( 0 ) );
|
|
286
|
+
await writer.writeJSON( 'graph.json', manifest );
|
|
287
|
+
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function arrayType( type ) {
|
|
291
|
+
|
|
292
|
+
const ArrayType = ARRAYS[ type ];
|
|
293
|
+
if ( ! ArrayType ) throw new Error( `scene graph: unknown array type ${type}` );
|
|
294
|
+
return ArrayType;
|
|
295
|
+
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Every section of the file, read in one forward pass of large windows — thousands of small
|
|
300
|
+
* reads cost ~0.5 ms each through File.slice, which made decoding slower than parsing.
|
|
301
|
+
* @returns {Promise<Map<number, ArrayBufferView>>} keyed by offset
|
|
302
|
+
*/
|
|
303
|
+
async function readSections( file, sections ) {
|
|
304
|
+
|
|
305
|
+
const out = new Map();
|
|
306
|
+
const ordered = [ ...sections ].sort( ( a, b ) => a.offset - b.offset );
|
|
307
|
+
let window = null;
|
|
308
|
+
let windowStart = 0;
|
|
309
|
+
|
|
310
|
+
for ( const section of ordered ) {
|
|
311
|
+
|
|
312
|
+
const { offset, byteLength, type } = section;
|
|
313
|
+
const ArrayType = arrayType( type );
|
|
314
|
+
const end = offset + byteLength;
|
|
315
|
+
|
|
316
|
+
if ( byteLength > READ_WINDOW ) {
|
|
317
|
+
|
|
318
|
+
out.set( offset, new ArrayType( await file.slice( offset, end ).arrayBuffer() ) );
|
|
319
|
+
continue;
|
|
320
|
+
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if ( ! window || offset < windowStart || end > windowStart + window.byteLength ) {
|
|
324
|
+
|
|
325
|
+
windowStart = offset;
|
|
326
|
+
window = await file.slice( offset, Math.min( file.size, offset + READ_WINDOW ) ).arrayBuffer();
|
|
327
|
+
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
out.set( offset, new ArrayType( window.slice( offset - windowStart, end - windowStart ) ) );
|
|
331
|
+
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return out;
|
|
335
|
+
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function sectionsOf( manifest ) {
|
|
339
|
+
|
|
340
|
+
const all = [];
|
|
341
|
+
for ( const node of manifest.nodes ) {
|
|
342
|
+
|
|
343
|
+
if ( node.instanceMatrix ) all.push( node.instanceMatrix );
|
|
344
|
+
if ( node.instanceColor ) all.push( node.instanceColor );
|
|
345
|
+
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
for ( const g of manifest.geometries ) {
|
|
349
|
+
|
|
350
|
+
for ( const a of Object.values( g.attributes ) ) all.push( a.section );
|
|
351
|
+
if ( g.index ) all.push( g.index.section );
|
|
352
|
+
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
for ( const t of manifest.textures ) if ( t.data ) all.push( t.data.section );
|
|
356
|
+
return all;
|
|
357
|
+
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function decodeAttribute( arrays, record ) {
|
|
361
|
+
|
|
362
|
+
const attribute = new BufferAttribute( arrays.get( record.section.offset ), record.itemSize, record.normalized );
|
|
363
|
+
attribute.name = record.name;
|
|
364
|
+
return attribute;
|
|
365
|
+
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function applyTextureProps( texture, record ) {
|
|
369
|
+
|
|
370
|
+
for ( const prop of TEXTURE_PROPS ) if ( record[ prop ] !== undefined ) texture[ prop ] = record[ prop ];
|
|
371
|
+
texture.repeat.fromArray( record.repeat );
|
|
372
|
+
texture.offset.fromArray( record.offset );
|
|
373
|
+
texture.center.fromArray( record.center );
|
|
374
|
+
texture.rotation = record.rotation;
|
|
375
|
+
texture.uuid = record.uuid;
|
|
376
|
+
Object.assign( texture.userData, record.userData );
|
|
377
|
+
texture.needsUpdate = true;
|
|
378
|
+
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* @param {Object} manifest - `graph.json`
|
|
383
|
+
* @param {Blob} data - `data.bin`
|
|
384
|
+
* @param {{loadTexture: function(string, string): Promise<?import('three').Texture>}} io - decodes an
|
|
385
|
+
* archive path with the loader it was first read with ('image' or 'environment')
|
|
386
|
+
* @returns {Promise<{root: Object3D, environment: ?import('three').Texture, animations: AnimationClip[], stats: Object}>}
|
|
387
|
+
*/
|
|
388
|
+
export async function decodeSceneGraph( manifest, data, { loadTexture } ) {
|
|
389
|
+
|
|
390
|
+
if ( manifest?.v !== SCENE_GRAPH_FORMAT ) throw new Error( 'scene graph: unknown format' );
|
|
391
|
+
|
|
392
|
+
const arrays = await readSections( data, sectionsOf( manifest ) );
|
|
393
|
+
const section = ( record ) => arrays.get( record.offset );
|
|
394
|
+
|
|
395
|
+
const textures = {};
|
|
396
|
+
for ( const record of manifest.textures ) {
|
|
397
|
+
|
|
398
|
+
let texture;
|
|
399
|
+
if ( record.path ) {
|
|
400
|
+
|
|
401
|
+
texture = await loadTexture( record.path, record.loader );
|
|
402
|
+
if ( ! texture ) throw new Error( `scene graph: "${record.path}" is missing from the archive` );
|
|
403
|
+
texture.userData[ ARCHIVE_PATH ] = record.path;
|
|
404
|
+
texture.userData[ ARCHIVE_LOADER ] = record.loader;
|
|
405
|
+
|
|
406
|
+
} else {
|
|
407
|
+
|
|
408
|
+
texture = new DataTexture( section( record.data.section ), record.data.width, record.data.height );
|
|
409
|
+
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
applyTextureProps( texture, record );
|
|
413
|
+
textures[ record.uuid ] = texture;
|
|
414
|
+
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const materialLoader = new MaterialLoader().setTextures( textures );
|
|
418
|
+
const materials = {};
|
|
419
|
+
for ( const json of manifest.materials ) {
|
|
420
|
+
|
|
421
|
+
const material = materialLoader.parse( json );
|
|
422
|
+
for ( const [ key, rgb ] of Object.entries( json.exactColors ?? {} ) ) material[ key ]?.setRGB( rgb[ 0 ], rgb[ 1 ], rgb[ 2 ] );
|
|
423
|
+
if ( json.exactIor !== undefined ) material.ior = json.exactIor;
|
|
424
|
+
materials[ json.uuid ] = material;
|
|
425
|
+
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const geometries = {};
|
|
429
|
+
for ( const record of manifest.geometries ) {
|
|
430
|
+
|
|
431
|
+
const geometry = new BufferGeometry();
|
|
432
|
+
geometry.uuid = record.uuid;
|
|
433
|
+
geometry.name = record.name;
|
|
434
|
+
for ( const [ name, attribute ] of Object.entries( record.attributes ) ) geometry.setAttribute( name, decodeAttribute( arrays, attribute ) );
|
|
435
|
+
if ( record.index ) geometry.setIndex( decodeAttribute( arrays, record.index ) );
|
|
436
|
+
for ( const g of record.groups ) geometry.addGroup( g.start, g.count, g.materialIndex );
|
|
437
|
+
geometry.setDrawRange( record.drawRange.start, record.drawRange.count );
|
|
438
|
+
geometry.userData = record.userData;
|
|
439
|
+
geometries[ record.uuid ] = geometry;
|
|
440
|
+
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const built = [];
|
|
444
|
+
for ( const node of manifest.nodes ) {
|
|
445
|
+
|
|
446
|
+
const material = Array.isArray( node.material ) ? node.material.map( ( u ) => materials[ u ] ) : materials[ node.material ];
|
|
447
|
+
let object;
|
|
448
|
+
switch ( node.type ) {
|
|
449
|
+
|
|
450
|
+
case 'InstancedMesh': {
|
|
451
|
+
|
|
452
|
+
// Built for one instance, then given the stored matrices: no second full-size array.
|
|
453
|
+
object = new InstancedMesh( geometries[ node.geometry ], material, 1 );
|
|
454
|
+
object.instanceMatrix = new InstancedBufferAttribute( section( node.instanceMatrix ), 16 );
|
|
455
|
+
object.count = node.count;
|
|
456
|
+
if ( node.instanceColor ) object.instanceColor = new InstancedBufferAttribute( section( node.instanceColor ), 3 );
|
|
457
|
+
break;
|
|
458
|
+
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
case 'Mesh': object = new Mesh( geometries[ node.geometry ], material ); break;
|
|
462
|
+
case 'PerspectiveCamera': object = new PerspectiveCamera(); break;
|
|
463
|
+
case 'OrthographicCamera': object = new OrthographicCamera(); break;
|
|
464
|
+
case 'Group': object = new Group(); break;
|
|
465
|
+
default: object = new Object3D();
|
|
466
|
+
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
object.uuid = node.uuid;
|
|
470
|
+
for ( const prop of OBJECT_PROPS ) object[ prop ] = node[ prop ];
|
|
471
|
+
object.position.fromArray( node.position );
|
|
472
|
+
object.quaternion.fromArray( node.quaternion );
|
|
473
|
+
object.scale.fromArray( node.scale );
|
|
474
|
+
object.layers.mask = node.layers;
|
|
475
|
+
object.userData = node.userData;
|
|
476
|
+
if ( node.matrix ) object.matrix.fromArray( node.matrix );
|
|
477
|
+
|
|
478
|
+
if ( object.isCamera ) {
|
|
479
|
+
|
|
480
|
+
for ( const prop of [ 'fov', 'aspect', 'near', 'far', 'zoom', 'focus', 'filmGauge', 'filmOffset', 'left', 'right', 'top', 'bottom' ] ) {
|
|
481
|
+
|
|
482
|
+
if ( node[ prop ] !== undefined ) object[ prop ] = node[ prop ];
|
|
483
|
+
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
object.updateProjectionMatrix();
|
|
487
|
+
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
built.push( object );
|
|
491
|
+
if ( node.parent >= 0 ) built[ node.parent ].add( object );
|
|
492
|
+
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return {
|
|
496
|
+
root: built[ 0 ],
|
|
497
|
+
environment: manifest.environment ? textures[ manifest.environment ] : null,
|
|
498
|
+
animations: manifest.animations.map( ( json ) => AnimationClip.parse( json ) ),
|
|
499
|
+
stats: manifest.stats,
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
}
|
|
503
|
+
|