rayzee 7.25.0 → 7.26.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 +7 -4
- package/dist/rayzee.es.js +1929 -1779
- package/dist/rayzee.umd.js +58 -58
- package/package.json +1 -1
- package/src/PathTracerApp.js +236 -29
- package/src/Processor/AssetLoader.js +113 -37
- package/src/Processor/SceneMetadata.js +123 -0
- package/src/Processor/utils.js +11 -0
- package/src/index.js +3 -0
- package/src/managers/EnvironmentManager.js +34 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Box3, Vector3, RectAreaLight, Color, FloatType, LinearFilter, EquirectangularReflectionMapping,
|
|
1
|
+
import { Box3, BufferGeometry, Vector3, RectAreaLight, Color, FloatType, LinearFilter, EquirectangularReflectionMapping,
|
|
2
2
|
TextureLoader, Texture, SRGBColorSpace, RepeatWrapping, Mesh, MeshStandardMaterial, MeshPhysicalMaterial,
|
|
3
3
|
CircleGeometry, Points, PointsMaterial, LoadingManager, EventDispatcher
|
|
4
4
|
} from 'three';
|
|
@@ -8,12 +8,14 @@ import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
|
|
|
8
8
|
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
|
|
9
9
|
import { EXRLoader } from 'three/addons/loaders/EXRLoader.js';
|
|
10
10
|
import { createMeshesFromMultiMaterialMesh } from 'three/addons/utils/SceneUtils.js';
|
|
11
|
+
import { clone as cloneWithSkeletons } from 'three/addons/utils/SkeletonUtils.js';
|
|
11
12
|
import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js';
|
|
12
13
|
import { unzipSync, zipSync, strFromU8 } from 'three/addons/libs/fflate.module.js';
|
|
13
|
-
import { disposeObjectFromMemory, updateLoading } from './utils';
|
|
14
|
+
import { disposeEngineOwnedResources, disposeObjectFromMemory, updateLoading } from './utils';
|
|
14
15
|
import { BuildTimer } from './BuildTimer.js';
|
|
15
16
|
import { getAssetConfig } from '../AssetConfig.js';
|
|
16
17
|
import { loadPBRTScene, pickEntryPath } from './PBRT/index.js';
|
|
18
|
+
import { extractSceneMetadata } from './SceneMetadata.js';
|
|
17
19
|
|
|
18
20
|
// Define supported file formats
|
|
19
21
|
const SUPPORTED_FORMATS = {
|
|
@@ -35,6 +37,18 @@ const SUPPORTED_FORMATS = {
|
|
|
35
37
|
// layers and the image assets they reference.
|
|
36
38
|
const USD_LAYER_RE = /\.(usd|usda|usdc)$/i;
|
|
37
39
|
const USD_IMAGE_RE = /\.(png|jpg|jpeg|avif)$/i;
|
|
40
|
+
// A throwaway stand-in for a geometry the engine must not mutate: the split's mergeGroups()
|
|
41
|
+
// reorders and disposes what it is given, but never writes the attributes.
|
|
42
|
+
function standInForSplit( source ) {
|
|
43
|
+
|
|
44
|
+
const geometry = new BufferGeometry();
|
|
45
|
+
for ( const name in source.attributes ) geometry.setAttribute( name, source.attributes[ name ] );
|
|
46
|
+
if ( source.index ) geometry.setIndex( source.index );
|
|
47
|
+
for ( const group of source.groups ) geometry.addGroup( group.start, group.count, group.materialIndex );
|
|
48
|
+
return geometry;
|
|
49
|
+
|
|
50
|
+
}
|
|
51
|
+
|
|
38
52
|
/**
|
|
39
53
|
* AssetLoader class - handles loading of 3D models, environment maps, and archives
|
|
40
54
|
*/
|
|
@@ -47,7 +61,6 @@ export class AssetLoader extends EventDispatcher {
|
|
|
47
61
|
this.camera = camera;
|
|
48
62
|
this.controls = controls;
|
|
49
63
|
this.targetModel = null;
|
|
50
|
-
this._externalModel = null;
|
|
51
64
|
this.floorPlane = null;
|
|
52
65
|
this.sceneScale = 1.0;
|
|
53
66
|
this.loaderCache = {};
|
|
@@ -55,6 +68,10 @@ export class AssetLoader extends EventDispatcher {
|
|
|
55
68
|
this.animations = [];
|
|
56
69
|
this.renderer = null;
|
|
57
70
|
|
|
71
|
+
// Scene-level authoring metadata from the current model (glTF `extras`), or null.
|
|
72
|
+
// See SceneMetadata.js. Cleared by releaseTargetModel() on every replace-load.
|
|
73
|
+
this.sceneMetadata = null;
|
|
74
|
+
|
|
58
75
|
// Shared across every loader so cancelActiveLoad() can abort whichever
|
|
59
76
|
// fetch is in flight (three r185 FileLoader wires the manager's abort
|
|
60
77
|
// signal into its fetch). One load runs at a time (guarded upstream).
|
|
@@ -127,27 +144,58 @@ export class AssetLoader extends EventDispatcher {
|
|
|
127
144
|
}
|
|
128
145
|
|
|
129
146
|
/**
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
147
|
+
* Deep-clones a caller-owned Object3D so the engine never mutates the host's tree.
|
|
148
|
+
* Geometry/material/texture ride along by reference, so release frees only what the engine
|
|
149
|
+
* allocated — see removeModelRoot().
|
|
150
|
+
*
|
|
151
|
+
* @param {import('three').Object3D} object3d - the caller's object; left untouched.
|
|
152
|
+
* @returns {import('three').Object3D} the engine-owned copy.
|
|
134
153
|
*/
|
|
135
|
-
|
|
154
|
+
_adoptExternalObject( object3d ) {
|
|
136
155
|
|
|
137
|
-
|
|
156
|
+
// Not Object3D.clone(): that leaves SkinnedMeshes bound to the source's bones.
|
|
157
|
+
let model;
|
|
158
|
+
try {
|
|
138
159
|
|
|
139
|
-
|
|
160
|
+
model = cloneWithSkeletons( object3d );
|
|
140
161
|
|
|
141
|
-
|
|
162
|
+
} catch ( error ) {
|
|
142
163
|
|
|
143
|
-
|
|
164
|
+
// Object3D.copy() round-trips userData through JSON, so a back-reference throws.
|
|
165
|
+
throw new Error(
|
|
166
|
+
`Cannot render "${object3d.name || object3d.type}": its userData must be JSON-serializable.`,
|
|
167
|
+
{ cause: error }
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
model.userData.__rayzeeExternal = true;
|
|
173
|
+
|
|
174
|
+
// Carried through as the scene-object id, unless a second copy already took it.
|
|
175
|
+
if ( ! this.scene?.children.some( c => c.uuid === object3d.uuid ) ) model.uuid = object3d.uuid;
|
|
144
176
|
|
|
145
|
-
|
|
177
|
+
// The copy sits under the engine's identity root, which keeps only a local transform.
|
|
178
|
+
if ( object3d.parent ) {
|
|
179
|
+
|
|
180
|
+
object3d.parent.updateWorldMatrix( true, false );
|
|
181
|
+
model.applyMatrix4( object3d.parent.matrixWorld );
|
|
146
182
|
|
|
147
183
|
}
|
|
148
184
|
|
|
185
|
+
return model;
|
|
186
|
+
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Releases the current targetModel. See removeModelRoot() for what gets freed. */
|
|
190
|
+
releaseTargetModel() {
|
|
191
|
+
|
|
192
|
+
this.sceneMetadata = null;
|
|
193
|
+
|
|
194
|
+
if ( ! this.targetModel ) return;
|
|
195
|
+
|
|
196
|
+
this.removeModelRoot( this.targetModel );
|
|
197
|
+
|
|
149
198
|
this.targetModel = null;
|
|
150
|
-
this._externalModel = null;
|
|
151
199
|
// Drop the released model's animation clips so a later rebuild doesn't rebind
|
|
152
200
|
// a mixer to disposed nodes. Every load path re-populates this.animations after.
|
|
153
201
|
this.animations = [];
|
|
@@ -298,7 +346,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
298
346
|
texture.generateMipmaps = true;
|
|
299
347
|
|
|
300
348
|
this.applyEnvironmentToScene( texture );
|
|
301
|
-
this.dispatchEvent( { type: 'load', texture } );
|
|
349
|
+
this.dispatchEvent( { type: 'load', texture, url: envUrl, filename: envUrl.split( /[?#]/ )[ 0 ].split( '/' ).pop() } );
|
|
302
350
|
return texture;
|
|
303
351
|
|
|
304
352
|
} catch ( error ) {
|
|
@@ -804,6 +852,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
804
852
|
|
|
805
853
|
this.releaseTargetModel();
|
|
806
854
|
this.targetModel = gltf.scene;
|
|
855
|
+
this.sceneMetadata = extractSceneMetadata( gltf );
|
|
807
856
|
this.onModelLoad( this.targetModel ).then( () => resolve( gltf ) );
|
|
808
857
|
|
|
809
858
|
},
|
|
@@ -1104,6 +1153,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1104
1153
|
|
|
1105
1154
|
this.targetModel = data.scene;
|
|
1106
1155
|
this.animations = data.animations || [];
|
|
1156
|
+
this.sceneMetadata = extractSceneMetadata( data );
|
|
1107
1157
|
await this.onModelLoad( this.targetModel );
|
|
1108
1158
|
this.dispatchEvent( { type: 'load', model: data.scene, filename: modelUrl.split( '/' ).pop() } );
|
|
1109
1159
|
return data;
|
|
@@ -1171,21 +1221,32 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1171
1221
|
|
|
1172
1222
|
}
|
|
1173
1223
|
|
|
1174
|
-
// Append a caller-owned Object3D without releasing prior models or reframing.
|
|
1224
|
+
// Append a copy of a caller-owned Object3D without releasing prior models or reframing.
|
|
1175
1225
|
appendObject3D( object3d, name = 'object3d' ) {
|
|
1176
1226
|
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1227
|
+
const root = this._adoptExternalObject( object3d );
|
|
1228
|
+
root.name = object3d.name || name;
|
|
1229
|
+
this._processAndParent( root );
|
|
1230
|
+
return { root };
|
|
1180
1231
|
|
|
1181
1232
|
}
|
|
1182
1233
|
|
|
1183
|
-
// Detach + dispose
|
|
1184
|
-
|
|
1234
|
+
// Detach + dispose a model root. An adopted root's geometry/materials belong to the
|
|
1235
|
+
// caller, so only the engine's own allocations go.
|
|
1236
|
+
removeModelRoot( root ) {
|
|
1185
1237
|
|
|
1186
1238
|
if ( ! root ) return;
|
|
1187
|
-
|
|
1188
|
-
|
|
1239
|
+
|
|
1240
|
+
if ( root.userData.__rayzeeExternal ) {
|
|
1241
|
+
|
|
1242
|
+
disposeEngineOwnedResources( root );
|
|
1243
|
+
root.parent?.remove( root );
|
|
1244
|
+
|
|
1245
|
+
} else {
|
|
1246
|
+
|
|
1247
|
+
disposeObjectFromMemory( root );
|
|
1248
|
+
|
|
1249
|
+
}
|
|
1189
1250
|
|
|
1190
1251
|
}
|
|
1191
1252
|
|
|
@@ -1204,6 +1265,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1204
1265
|
|
|
1205
1266
|
this.targetModel = data.scene;
|
|
1206
1267
|
this.animations = data.animations || [];
|
|
1268
|
+
this.sceneMetadata = extractSceneMetadata( data );
|
|
1207
1269
|
updateLoading( { isLoading: true, status: "Processing Data...", progress: 10 } );
|
|
1208
1270
|
await this.onModelLoad( this.targetModel );
|
|
1209
1271
|
|
|
@@ -1500,17 +1562,18 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1500
1562
|
|
|
1501
1563
|
async loadObject3D( object3d, name = 'object3d' ) {
|
|
1502
1564
|
|
|
1503
|
-
object3d.name = object3d.name || name;
|
|
1504
|
-
|
|
1505
1565
|
this.releaseTargetModel();
|
|
1506
|
-
|
|
1507
|
-
this.
|
|
1566
|
+
|
|
1567
|
+
const model = this._adoptExternalObject( object3d );
|
|
1568
|
+
model.name = object3d.name || name;
|
|
1569
|
+
|
|
1570
|
+
this.targetModel = model;
|
|
1508
1571
|
|
|
1509
1572
|
updateLoading( { isLoading: true, status: "Processing Data...", progress: 10 } );
|
|
1510
1573
|
await this.onModelLoad( this.targetModel );
|
|
1511
1574
|
|
|
1512
|
-
this.dispatchEvent( { type: 'load', model
|
|
1513
|
-
return
|
|
1575
|
+
this.dispatchEvent( { type: 'load', model, filename: name } );
|
|
1576
|
+
return model;
|
|
1514
1577
|
|
|
1515
1578
|
}
|
|
1516
1579
|
|
|
@@ -1646,6 +1709,9 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1646
1709
|
processModelObjects( model ) {
|
|
1647
1710
|
|
|
1648
1711
|
let visitedAreaLights = [];
|
|
1712
|
+
// Split after the walk: traverse() caches children.length, so splitting in place
|
|
1713
|
+
// shifts later siblings down a slot and skips one.
|
|
1714
|
+
const multiMaterialMeshes = [];
|
|
1649
1715
|
model.traverse( ( object ) => {
|
|
1650
1716
|
|
|
1651
1717
|
const userData = object.userData;
|
|
@@ -1699,19 +1765,29 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1699
1765
|
// Handle multi-material meshes
|
|
1700
1766
|
if ( object.isMesh && Array.isArray( object.material ) ) {
|
|
1701
1767
|
|
|
1702
|
-
|
|
1703
|
-
const group = createMeshesFromMultiMaterialMesh( object );
|
|
1768
|
+
multiMaterialMeshes.push( object );
|
|
1704
1769
|
|
|
1705
|
-
|
|
1770
|
+
}
|
|
1706
1771
|
|
|
1707
|
-
|
|
1708
|
-
object.parent.remove( object );
|
|
1772
|
+
} );
|
|
1709
1773
|
|
|
1710
|
-
|
|
1774
|
+
const shared = model.userData.__rayzeeExternal === true;
|
|
1711
1775
|
|
|
1712
|
-
|
|
1776
|
+
for ( const object of multiMaterialMeshes ) {
|
|
1713
1777
|
|
|
1714
|
-
|
|
1778
|
+
if ( ! object.parent ) continue;
|
|
1779
|
+
|
|
1780
|
+
console.log( 'Found multi-material mesh:', object.name );
|
|
1781
|
+
if ( shared ) object.geometry = standInForSplit( object.geometry );
|
|
1782
|
+
|
|
1783
|
+
const group = createMeshesFromMultiMaterialMesh( object );
|
|
1784
|
+
// Fresh geometry per group; tag it so an adopted model's release can free it.
|
|
1785
|
+
for ( const child of group.children ) child.geometry.userData.__rayzeeOwned = true;
|
|
1786
|
+
|
|
1787
|
+
object.parent.add( group );
|
|
1788
|
+
object.parent.remove( object );
|
|
1789
|
+
|
|
1790
|
+
}
|
|
1715
1791
|
|
|
1716
1792
|
}
|
|
1717
1793
|
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SceneMetadata.js
|
|
3
|
+
* Scene-level authoring metadata carried inside an asset file.
|
|
4
|
+
*
|
|
5
|
+
* glTF/GLB carries it in `extras`, which three's GLTFLoader surfaces in three
|
|
6
|
+
* places depending on where the exporter wrote it: top-level `extras` becomes
|
|
7
|
+
* `gltf.userData`, `asset.extras` stays on `gltf.asset.extras`, and a scene's
|
|
8
|
+
* `extras` becomes `gltf.scene.userData`. All three are scanned, and the payload
|
|
9
|
+
* may sit at the root or be namespaced under a `rayzee` key.
|
|
10
|
+
*
|
|
11
|
+
* Recognised payload:
|
|
12
|
+
* {
|
|
13
|
+
* "environment": {
|
|
14
|
+
* "sourceFile": "https://.../brown_photostudio_02_1k.hdr",
|
|
15
|
+
* "rotation": 0, // degrees around Y, matches the environmentRotation setting
|
|
16
|
+
* "intensity": 1 // drives both environmentIntensity and backgroundIntensity
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* Values may also arrive as JSON strings — DCC custom-property round-trips often
|
|
21
|
+
* stringify nested objects — so every container is parsed leniently.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const ENV_URL_KEYS = [ 'sourceFile', 'url', 'file' ];
|
|
25
|
+
|
|
26
|
+
function asObject( value ) {
|
|
27
|
+
|
|
28
|
+
if ( typeof value === 'string' ) {
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
|
|
32
|
+
const parsed = JSON.parse( value );
|
|
33
|
+
return ( parsed && typeof parsed === 'object' ) ? parsed : null;
|
|
34
|
+
|
|
35
|
+
} catch {
|
|
36
|
+
|
|
37
|
+
return null;
|
|
38
|
+
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return ( value && typeof value === 'object' ) ? value : null;
|
|
44
|
+
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function asNumber( value ) {
|
|
48
|
+
|
|
49
|
+
const n = typeof value === 'string' ? parseFloat( value ) : value;
|
|
50
|
+
return Number.isFinite( n ) ? n : undefined;
|
|
51
|
+
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function pickPayload( container ) {
|
|
55
|
+
|
|
56
|
+
const obj = asObject( container );
|
|
57
|
+
if ( ! obj ) return null;
|
|
58
|
+
|
|
59
|
+
const scoped = asObject( obj.rayzee );
|
|
60
|
+
if ( scoped && scoped.environment !== undefined ) return scoped;
|
|
61
|
+
|
|
62
|
+
return obj.environment !== undefined ? obj : null;
|
|
63
|
+
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeEnvironment( raw ) {
|
|
67
|
+
|
|
68
|
+
const env = asObject( raw );
|
|
69
|
+
if ( ! env ) return null;
|
|
70
|
+
|
|
71
|
+
const sourceFile = ENV_URL_KEYS
|
|
72
|
+
.map( key => env[ key ] )
|
|
73
|
+
.find( value => typeof value === 'string' && value.trim() );
|
|
74
|
+
if ( ! sourceFile ) return null;
|
|
75
|
+
|
|
76
|
+
const rotation = asNumber( env.rotation );
|
|
77
|
+
const intensity = asNumber( env.intensity );
|
|
78
|
+
|
|
79
|
+
const normalized = { sourceFile: sourceFile.trim() };
|
|
80
|
+
if ( rotation !== undefined ) normalized.rotation = rotation;
|
|
81
|
+
if ( intensity !== undefined ) normalized.intensity = Math.max( 0, intensity );
|
|
82
|
+
|
|
83
|
+
return normalized;
|
|
84
|
+
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Reads scene metadata out of one `extras`-shaped container.
|
|
89
|
+
* @param {Object|string|null} container
|
|
90
|
+
* @returns {{ environment?: { sourceFile: string, rotation?: number, intensity?: number } }|null}
|
|
91
|
+
*/
|
|
92
|
+
export function parseSceneMetadata( container ) {
|
|
93
|
+
|
|
94
|
+
const payload = pickPayload( container );
|
|
95
|
+
if ( ! payload ) return null;
|
|
96
|
+
|
|
97
|
+
const environment = normalizeEnvironment( payload.environment );
|
|
98
|
+
return environment ? { environment } : null;
|
|
99
|
+
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Reads scene metadata out of a GLTFLoader parse result.
|
|
104
|
+
* @param {Object} gltf - The object GLTFLoader resolves with
|
|
105
|
+
* @returns {{ environment?: { sourceFile: string, rotation?: number, intensity?: number } }|null}
|
|
106
|
+
*/
|
|
107
|
+
export function extractSceneMetadata( gltf ) {
|
|
108
|
+
|
|
109
|
+
if ( ! gltf ) return null;
|
|
110
|
+
|
|
111
|
+
const containers = [ gltf.userData, gltf.asset?.extras, gltf.scene?.userData ];
|
|
112
|
+
if ( Array.isArray( gltf.scenes ) ) containers.push( ...gltf.scenes.map( scene => scene?.userData ) );
|
|
113
|
+
|
|
114
|
+
for ( const container of containers ) {
|
|
115
|
+
|
|
116
|
+
const metadata = parseSceneMetadata( container );
|
|
117
|
+
if ( metadata ) return metadata;
|
|
118
|
+
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return null;
|
|
122
|
+
|
|
123
|
+
}
|
package/src/Processor/utils.js
CHANGED
|
@@ -177,6 +177,17 @@ export function disposeObjectFromMemory( object, exeptions = [] ) {
|
|
|
177
177
|
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
/** Frees only geometry the engine allocated inside an adopted model; the rest is the host's. */
|
|
181
|
+
export function disposeEngineOwnedResources( object ) {
|
|
182
|
+
|
|
183
|
+
object?.traverse( ( child ) => {
|
|
184
|
+
|
|
185
|
+
if ( child.geometry?.userData?.__rayzeeOwned ) child.geometry.dispose();
|
|
186
|
+
|
|
187
|
+
} );
|
|
188
|
+
|
|
189
|
+
}
|
|
190
|
+
|
|
180
191
|
|
|
181
192
|
|
|
182
193
|
/**
|
package/src/index.js
CHANGED
|
@@ -17,6 +17,9 @@ export { EngineEvents } from './EngineEvents.js';
|
|
|
17
17
|
// Asset URL / cache namespace overrides (call before constructing PathTracerApp)
|
|
18
18
|
export { configureAssets, getAssetConfig } from './AssetConfig.js';
|
|
19
19
|
|
|
20
|
+
// Scene-level authoring metadata embedded in model files (glTF `extras`)
|
|
21
|
+
export { extractSceneMetadata, parseSceneMetadata } from './Processor/SceneMetadata.js';
|
|
22
|
+
|
|
20
23
|
// Logging — leveled/namespaced console output shared with the workers
|
|
21
24
|
export { Logger, createLogger, fmt, LOG_LEVELS } from './utils/Logger.js';
|
|
22
25
|
|
|
@@ -360,8 +360,11 @@ export class EnvironmentManager {
|
|
|
360
360
|
/**
|
|
361
361
|
* Set environment map, build CDF, and update shader texture nodes.
|
|
362
362
|
* @param {import('three').Texture|null} envMap
|
|
363
|
+
* @param {Object} [options]
|
|
364
|
+
* @param {boolean} [options.buildCDF=true] - Skip when the caller builds the CDF itself
|
|
365
|
+
* (PathTracerApp.loadSceneData builds it in parallel with the BVH, then calls applyCDFResults).
|
|
363
366
|
*/
|
|
364
|
-
async setEnvironmentMap( envMap ) {
|
|
367
|
+
async setEnvironmentMap( envMap, { buildCDF = true } = {} ) {
|
|
365
368
|
|
|
366
369
|
// Free the outgoing env texture's GPU memory before it is orphaned. Skip: the
|
|
367
370
|
// reusable placeholder, an idempotent re-set of the same texture, and the HDRI
|
|
@@ -379,7 +382,7 @@ export class EnvironmentManager {
|
|
|
379
382
|
|
|
380
383
|
if ( envMap ) {
|
|
381
384
|
|
|
382
|
-
await this.buildEnvironmentCDF();
|
|
385
|
+
if ( buildCDF ) await this.buildEnvironmentCDF();
|
|
383
386
|
|
|
384
387
|
} else {
|
|
385
388
|
|
|
@@ -411,6 +414,35 @@ export class EnvironmentManager {
|
|
|
411
414
|
|
|
412
415
|
}
|
|
413
416
|
|
|
417
|
+
/**
|
|
418
|
+
* Enter 'hdri' mode ahead of an async HDRI install, and drop the texture stashed by a
|
|
419
|
+
* previous setMode( 'gradient' | 'color' | 'procedural' ): a new HDRI makes it
|
|
420
|
+
* unreachable, so leaving it in place both leaks it and lets a setMode( 'hdri' )
|
|
421
|
+
* arriving mid-download restore it over the map we are about to install.
|
|
422
|
+
* @param {import('three').Texture} [incoming] - Never disposed, even if it is the stash.
|
|
423
|
+
*/
|
|
424
|
+
beginHDRI( incoming = null ) {
|
|
425
|
+
|
|
426
|
+
if ( this._previousHDRI && this._previousHDRI !== incoming ) this._previousHDRI.dispose?.();
|
|
427
|
+
|
|
428
|
+
this._previousHDRI = null;
|
|
429
|
+
this.envParams.mode = 'hdri';
|
|
430
|
+
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Install an HDRI as the active environment, switching the mode state machine to 'hdri'.
|
|
435
|
+
* @param {import('three').Texture} envMap
|
|
436
|
+
* @param {Object} [options]
|
|
437
|
+
* @param {boolean} [options.buildCDF=true]
|
|
438
|
+
*/
|
|
439
|
+
async applyHDRI( envMap, { buildCDF = true } = {} ) {
|
|
440
|
+
|
|
441
|
+
this.beginHDRI( envMap );
|
|
442
|
+
await this.setEnvironmentMap( envMap, { buildCDF } );
|
|
443
|
+
|
|
444
|
+
}
|
|
445
|
+
|
|
414
446
|
// ===== SKY GENERATORS =====
|
|
415
447
|
|
|
416
448
|
/**
|