@chocozhang/three-model-render 1.0.3 → 1.0.5
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/CHANGELOG.md +39 -0
- package/README.md +134 -97
- package/dist/camera/index.d.ts +59 -36
- package/dist/camera/index.js +83 -67
- package/dist/camera/index.js.map +1 -1
- package/dist/camera/index.mjs +83 -67
- package/dist/camera/index.mjs.map +1 -1
- package/dist/core/index.d.ts +81 -28
- package/dist/core/index.js +194 -104
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +194 -105
- package/dist/core/index.mjs.map +1 -1
- package/dist/effect/index.d.ts +47 -134
- package/dist/effect/index.js +287 -288
- package/dist/effect/index.js.map +1 -1
- package/dist/effect/index.mjs +287 -288
- package/dist/effect/index.mjs.map +1 -1
- package/dist/index.d.ts +432 -349
- package/dist/index.js +1399 -1228
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1395 -1229
- package/dist/index.mjs.map +1 -1
- package/dist/interaction/index.d.ts +85 -52
- package/dist/interaction/index.js +168 -142
- package/dist/interaction/index.js.map +1 -1
- package/dist/interaction/index.mjs +168 -142
- package/dist/interaction/index.mjs.map +1 -1
- package/dist/loader/index.d.ts +106 -58
- package/dist/loader/index.js +492 -454
- package/dist/loader/index.js.map +1 -1
- package/dist/loader/index.mjs +491 -455
- package/dist/loader/index.mjs.map +1 -1
- package/dist/setup/index.d.ts +26 -24
- package/dist/setup/index.js +125 -163
- package/dist/setup/index.js.map +1 -1
- package/dist/setup/index.mjs +124 -164
- package/dist/setup/index.mjs.map +1 -1
- package/dist/ui/index.d.ts +18 -7
- package/dist/ui/index.js +45 -37
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/index.mjs +45 -37
- package/dist/ui/index.mjs.map +1 -1
- package/package.json +50 -22
package/dist/loader/index.mjs
CHANGED
|
@@ -1,163 +1,166 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
2
|
import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Permission to use, copy, modify, and/or distribute this software for any
|
|
8
|
-
purpose with or without fee is hereby granted.
|
|
9
|
-
|
|
10
|
-
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
11
|
-
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
12
|
-
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
13
|
-
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
14
|
-
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
15
|
-
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
16
|
-
PERFORMANCE OF THIS SOFTWARE.
|
|
17
|
-
***************************************************************************** */
|
|
18
|
-
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
function __awaiter(thisArg, _arguments, P, generator) {
|
|
22
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
23
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
24
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
25
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
26
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
27
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
32
|
-
var e = new Error(message);
|
|
33
|
-
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
4
|
+
let globalConfig = {
|
|
5
|
+
dracoDecoderPath: '/draco/',
|
|
6
|
+
ktx2TranscoderPath: '/basis/',
|
|
34
7
|
};
|
|
8
|
+
/**
|
|
9
|
+
* Update global loader configuration (e.g., set path to CDN)
|
|
10
|
+
*/
|
|
11
|
+
function setLoaderConfig(config) {
|
|
12
|
+
globalConfig = { ...globalConfig, ...config };
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Get current global loader configuration
|
|
16
|
+
*/
|
|
17
|
+
function getLoaderConfig() {
|
|
18
|
+
return globalConfig;
|
|
19
|
+
}
|
|
35
20
|
|
|
21
|
+
/**
|
|
22
|
+
* @file modelLoader.ts
|
|
23
|
+
* @description
|
|
24
|
+
* Utility to load 3D models (GLTF, FBX, OBJ, PLY, STL) from URLs.
|
|
25
|
+
*
|
|
26
|
+
* @best-practice
|
|
27
|
+
* - Use `loadModelByUrl` for a unified loading interface.
|
|
28
|
+
* - Supports Draco compression and KTX2 textures for GLTF.
|
|
29
|
+
* - Includes optimization options like geometry merging and texture downscaling.
|
|
30
|
+
*/
|
|
36
31
|
const DEFAULT_OPTIONS$1 = {
|
|
37
32
|
useKTX2: false,
|
|
38
33
|
mergeGeometries: false,
|
|
39
34
|
maxTextureSize: null,
|
|
40
35
|
useSimpleMaterials: false,
|
|
41
36
|
skipSkinned: true,
|
|
37
|
+
useCache: true,
|
|
42
38
|
};
|
|
43
|
-
|
|
39
|
+
const modelCache = new Map();
|
|
40
|
+
/** Automatically determine which options to enable based on extension (smart judgment) */
|
|
44
41
|
function normalizeOptions(url, opts) {
|
|
45
42
|
const ext = (url.split('.').pop() || '').toLowerCase();
|
|
46
|
-
const merged =
|
|
43
|
+
const merged = { ...DEFAULT_OPTIONS$1, ...opts };
|
|
47
44
|
if (ext === 'gltf' || ext === 'glb') {
|
|
48
|
-
|
|
45
|
+
const globalConfig = getLoaderConfig();
|
|
46
|
+
// gltf/glb defaults to trying draco/ktx2 if user didn't specify
|
|
49
47
|
if (merged.dracoDecoderPath === undefined)
|
|
50
|
-
merged.dracoDecoderPath =
|
|
48
|
+
merged.dracoDecoderPath = globalConfig.dracoDecoderPath;
|
|
51
49
|
if (merged.useKTX2 === undefined)
|
|
52
50
|
merged.useKTX2 = true;
|
|
53
51
|
if (merged.ktx2TranscoderPath === undefined)
|
|
54
|
-
merged.ktx2TranscoderPath =
|
|
52
|
+
merged.ktx2TranscoderPath = globalConfig.ktx2TranscoderPath;
|
|
55
53
|
}
|
|
56
54
|
else {
|
|
57
|
-
// fbx/obj/ply/stl
|
|
55
|
+
// fbx/obj/ply/stl etc. do not need draco/ktx2
|
|
58
56
|
merged.dracoDecoderPath = null;
|
|
59
57
|
merged.ktx2TranscoderPath = null;
|
|
60
58
|
merged.useKTX2 = false;
|
|
61
59
|
}
|
|
62
60
|
return merged;
|
|
63
61
|
}
|
|
64
|
-
function loadModelByUrl(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const ktx2Loader = new KTX2Loader().setTranscoderPath(opts.ktx2TranscoderPath);
|
|
85
|
-
gltfLoader.__ktx2Loader = ktx2Loader;
|
|
86
|
-
}
|
|
87
|
-
loader = gltfLoader;
|
|
88
|
-
}
|
|
89
|
-
else if (ext === 'fbx') {
|
|
90
|
-
const { FBXLoader } = yield import('three/examples/jsm/loaders/FBXLoader.js');
|
|
91
|
-
loader = new FBXLoader(manager);
|
|
92
|
-
}
|
|
93
|
-
else if (ext === 'obj') {
|
|
94
|
-
const { OBJLoader } = yield import('three/examples/jsm/loaders/OBJLoader.js');
|
|
95
|
-
loader = new OBJLoader(manager);
|
|
96
|
-
}
|
|
97
|
-
else if (ext === 'ply') {
|
|
98
|
-
const { PLYLoader } = yield import('three/examples/jsm/loaders/PLYLoader.js');
|
|
99
|
-
loader = new PLYLoader(manager);
|
|
100
|
-
}
|
|
101
|
-
else if (ext === 'stl') {
|
|
102
|
-
const { STLLoader } = yield import('three/examples/jsm/loaders/STLLoader.js');
|
|
103
|
-
loader = new STLLoader(manager);
|
|
62
|
+
async function loadModelByUrl(url, options = {}) {
|
|
63
|
+
if (!url)
|
|
64
|
+
throw new Error('url required');
|
|
65
|
+
const ext = (url.split('.').pop() || '').toLowerCase();
|
|
66
|
+
const opts = normalizeOptions(url, options);
|
|
67
|
+
const manager = opts.manager ?? new THREE.LoadingManager();
|
|
68
|
+
// Cache key includes URL and relevant optimization options
|
|
69
|
+
const cacheKey = `${url}_${opts.mergeGeometries}_${opts.maxTextureSize}_${opts.useSimpleMaterials}`;
|
|
70
|
+
if (opts.useCache && modelCache.has(cacheKey)) {
|
|
71
|
+
return modelCache.get(cacheKey).clone();
|
|
72
|
+
}
|
|
73
|
+
let loader;
|
|
74
|
+
if (ext === 'gltf' || ext === 'glb') {
|
|
75
|
+
const { GLTFLoader } = await import('three/examples/jsm/loaders/GLTFLoader.js');
|
|
76
|
+
const gltfLoader = new GLTFLoader(manager);
|
|
77
|
+
if (opts.dracoDecoderPath) {
|
|
78
|
+
const { DRACOLoader } = await import('three/examples/jsm/loaders/DRACOLoader.js');
|
|
79
|
+
const draco = new DRACOLoader();
|
|
80
|
+
draco.setDecoderPath(opts.dracoDecoderPath);
|
|
81
|
+
gltfLoader.setDRACOLoader(draco);
|
|
104
82
|
}
|
|
105
|
-
|
|
106
|
-
|
|
83
|
+
if (opts.useKTX2 && opts.ktx2TranscoderPath) {
|
|
84
|
+
const { KTX2Loader } = await import('three/examples/jsm/loaders/KTX2Loader.js');
|
|
85
|
+
const ktx2Loader = new KTX2Loader().setTranscoderPath(opts.ktx2TranscoderPath);
|
|
86
|
+
gltfLoader.__ktx2Loader = ktx2Loader;
|
|
107
87
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
88
|
+
loader = gltfLoader;
|
|
89
|
+
}
|
|
90
|
+
else if (ext === 'fbx') {
|
|
91
|
+
const { FBXLoader } = await import('three/examples/jsm/loaders/FBXLoader.js');
|
|
92
|
+
loader = new FBXLoader(manager);
|
|
93
|
+
}
|
|
94
|
+
else if (ext === 'obj') {
|
|
95
|
+
const { OBJLoader } = await import('three/examples/jsm/loaders/OBJLoader.js');
|
|
96
|
+
loader = new OBJLoader(manager);
|
|
97
|
+
}
|
|
98
|
+
else if (ext === 'ply') {
|
|
99
|
+
const { PLYLoader } = await import('three/examples/jsm/loaders/PLYLoader.js');
|
|
100
|
+
loader = new PLYLoader(manager);
|
|
101
|
+
}
|
|
102
|
+
else if (ext === 'stl') {
|
|
103
|
+
const { STLLoader } = await import('three/examples/jsm/loaders/STLLoader.js');
|
|
104
|
+
loader = new STLLoader(manager);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
throw new Error(`Unsupported model extension: .${ext}`);
|
|
108
|
+
}
|
|
109
|
+
const object = await new Promise((resolve, reject) => {
|
|
110
|
+
loader.load(url, (res) => {
|
|
111
|
+
if (ext === 'gltf' || ext === 'glb') {
|
|
112
|
+
const sceneObj = res.scene || res;
|
|
113
|
+
// --- Critical: Expose animations to scene.userData (or scene.animations) ---
|
|
114
|
+
// So the caller can access clips simply by getting sceneObj.userData.animations
|
|
115
|
+
sceneObj.userData = sceneObj?.userData || {};
|
|
116
|
+
sceneObj.userData.animations = res.animations ?? [];
|
|
117
|
+
resolve(sceneObj);
|
|
133
118
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
downscaleTexturesInObject(object, opts.maxTextureSize);
|
|
137
|
-
if (opts.useSimpleMaterials) {
|
|
138
|
-
object.traverse((child) => {
|
|
139
|
-
const m = child.material;
|
|
140
|
-
if (!m)
|
|
141
|
-
return;
|
|
142
|
-
if (Array.isArray(m))
|
|
143
|
-
child.material = m.map((mat) => toSimpleMaterial(mat));
|
|
144
|
-
else
|
|
145
|
-
child.material = toSimpleMaterial(m);
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
if (opts.mergeGeometries) {
|
|
149
|
-
try {
|
|
150
|
-
yield tryMergeGeometries(object, { skipSkinned: (_b = opts.skipSkinned) !== null && _b !== void 0 ? _b : true });
|
|
119
|
+
else {
|
|
120
|
+
resolve(res);
|
|
151
121
|
}
|
|
152
|
-
|
|
153
|
-
|
|
122
|
+
}, undefined, (err) => reject(err));
|
|
123
|
+
});
|
|
124
|
+
// Optimize
|
|
125
|
+
object.traverse((child) => {
|
|
126
|
+
const mesh = child;
|
|
127
|
+
if (mesh.isMesh && mesh.geometry && !mesh.geometry.isBufferGeometry) {
|
|
128
|
+
try {
|
|
129
|
+
mesh.geometry = new THREE.BufferGeometry().fromGeometry?.(mesh.geometry) ?? mesh.geometry;
|
|
154
130
|
}
|
|
131
|
+
catch { }
|
|
155
132
|
}
|
|
156
|
-
return object;
|
|
157
133
|
});
|
|
134
|
+
if (opts.maxTextureSize && opts.maxTextureSize > 0)
|
|
135
|
+
await downscaleTexturesInObject(object, opts.maxTextureSize);
|
|
136
|
+
if (opts.useSimpleMaterials) {
|
|
137
|
+
object.traverse((child) => {
|
|
138
|
+
const m = child.material;
|
|
139
|
+
if (!m)
|
|
140
|
+
return;
|
|
141
|
+
if (Array.isArray(m))
|
|
142
|
+
child.material = m.map((mat) => toSimpleMaterial(mat));
|
|
143
|
+
else
|
|
144
|
+
child.material = toSimpleMaterial(m);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
if (opts.mergeGeometries) {
|
|
148
|
+
try {
|
|
149
|
+
await tryMergeGeometries(object, { skipSkinned: opts.skipSkinned ?? true });
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
console.warn('mergeGeometries failed', e);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (opts.useCache) {
|
|
156
|
+
modelCache.set(cacheKey, object);
|
|
157
|
+
return object.clone();
|
|
158
|
+
}
|
|
159
|
+
return object;
|
|
158
160
|
}
|
|
159
|
-
/**
|
|
160
|
-
function downscaleTexturesInObject(obj, maxSize) {
|
|
161
|
+
/** Runtime downscale textures in mesh to maxSize (createImageBitmap or canvas) to save GPU memory */
|
|
162
|
+
async function downscaleTexturesInObject(obj, maxSize) {
|
|
163
|
+
const tasks = [];
|
|
161
164
|
obj.traverse((ch) => {
|
|
162
165
|
if (!ch.isMesh)
|
|
163
166
|
return;
|
|
@@ -176,115 +179,128 @@ function downscaleTexturesInObject(obj, maxSize) {
|
|
|
176
179
|
const max = maxSize;
|
|
177
180
|
if (image.width <= max && image.height <= max)
|
|
178
181
|
return;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
182
|
+
tasks.push((async () => {
|
|
183
|
+
try {
|
|
184
|
+
const scale = Math.min(max / image.width, max / image.height);
|
|
185
|
+
const newWidth = Math.floor(image.width * scale);
|
|
186
|
+
const newHeight = Math.floor(image.height * scale);
|
|
187
|
+
let newSource;
|
|
188
|
+
if (typeof createImageBitmap !== 'undefined') {
|
|
189
|
+
newSource = await createImageBitmap(image, {
|
|
190
|
+
resizeWidth: newWidth,
|
|
191
|
+
resizeHeight: newHeight,
|
|
192
|
+
resizeQuality: 'high'
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
// Fallback for environments without createImageBitmap
|
|
197
|
+
const canvas = document.createElement('canvas');
|
|
198
|
+
canvas.width = newWidth;
|
|
199
|
+
canvas.height = newHeight;
|
|
200
|
+
const ctx = canvas.getContext('2d');
|
|
201
|
+
if (ctx) {
|
|
202
|
+
ctx.drawImage(image, 0, 0, newWidth, newHeight);
|
|
203
|
+
newSource = canvas;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (newSource) {
|
|
207
|
+
const newTex = new THREE.Texture(newSource);
|
|
208
|
+
newTex.needsUpdate = true;
|
|
209
|
+
newTex.encoding = tex.encoding;
|
|
210
|
+
mat[p] = newTex;
|
|
211
|
+
}
|
|
193
212
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
213
|
+
catch (e) {
|
|
214
|
+
console.warn('downscale texture failed', e);
|
|
215
|
+
}
|
|
216
|
+
})());
|
|
198
217
|
});
|
|
199
218
|
});
|
|
219
|
+
await Promise.all(tasks);
|
|
200
220
|
}
|
|
201
221
|
/**
|
|
202
|
-
*
|
|
203
|
-
* -
|
|
204
|
-
* -
|
|
205
|
-
* -
|
|
222
|
+
* Try to merge geometries in object (Only merge: non-transparent, non-SkinnedMesh, attribute compatible BufferGeometry)
|
|
223
|
+
* - Before merging, apply world matrix to each mesh's geometry (so merged geometry is in world space)
|
|
224
|
+
* - Merging will group by material UUID (different materials cannot be merged)
|
|
225
|
+
* - Merge function is compatible with common export names of BufferGeometryUtils
|
|
206
226
|
*/
|
|
207
|
-
function tryMergeGeometries(root, opts) {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
var _a;
|
|
213
|
-
if (!ch.isMesh)
|
|
214
|
-
return;
|
|
215
|
-
const mesh = ch;
|
|
216
|
-
if (opts.skipSkinned && mesh.isSkinnedMesh)
|
|
217
|
-
return;
|
|
218
|
-
const mat = mesh.material;
|
|
219
|
-
// don't merge transparent or morph-enabled or skinned meshes
|
|
220
|
-
if (!mesh.geometry || mesh.visible === false)
|
|
221
|
-
return;
|
|
222
|
-
if (mat && mat.transparent)
|
|
223
|
-
return;
|
|
224
|
-
const geom = mesh.geometry.clone();
|
|
225
|
-
mesh.updateWorldMatrix(true, false);
|
|
226
|
-
geom.applyMatrix4(mesh.matrixWorld);
|
|
227
|
-
// ensure attributes compatible? we'll rely on merge function to return null if incompatible
|
|
228
|
-
const key = (mat && mat.uuid) || 'default';
|
|
229
|
-
const bucket = (_a = groups.get(key)) !== null && _a !== void 0 ? _a : { material: mat !== null && mat !== void 0 ? mat : new THREE.MeshStandardMaterial(), geoms: [] };
|
|
230
|
-
bucket.geoms.push(geom);
|
|
231
|
-
groups.set(key, bucket);
|
|
232
|
-
// mark for removal (we'll remove meshes after)
|
|
233
|
-
mesh.userData.__toRemoveForMerge = true;
|
|
234
|
-
});
|
|
235
|
-
if (groups.size === 0)
|
|
227
|
+
async function tryMergeGeometries(root, opts) {
|
|
228
|
+
// collect meshes by material uuid
|
|
229
|
+
const groups = new Map();
|
|
230
|
+
root.traverse((ch) => {
|
|
231
|
+
if (!ch.isMesh)
|
|
236
232
|
return;
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if (
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
233
|
+
const mesh = ch;
|
|
234
|
+
if (opts.skipSkinned && mesh.isSkinnedMesh)
|
|
235
|
+
return;
|
|
236
|
+
const mat = mesh.material;
|
|
237
|
+
// don't merge transparent or morph-enabled or skinned meshes
|
|
238
|
+
if (!mesh.geometry || mesh.visible === false)
|
|
239
|
+
return;
|
|
240
|
+
if (mat && mat.transparent)
|
|
241
|
+
return;
|
|
242
|
+
const geom = mesh.geometry.clone();
|
|
243
|
+
mesh.updateWorldMatrix(true, false);
|
|
244
|
+
geom.applyMatrix4(mesh.matrixWorld);
|
|
245
|
+
// ensure attributes compatible? we'll rely on merge function to return null if incompatible
|
|
246
|
+
const key = (mat && mat.uuid) || 'default';
|
|
247
|
+
const bucket = groups.get(key) ?? { material: mat ?? new THREE.MeshStandardMaterial(), geoms: [] };
|
|
248
|
+
bucket.geoms.push(geom);
|
|
249
|
+
groups.set(key, bucket);
|
|
250
|
+
// mark for removal (we'll remove meshes after)
|
|
251
|
+
mesh.userData.__toRemoveForMerge = true;
|
|
252
|
+
});
|
|
253
|
+
if (groups.size === 0)
|
|
254
|
+
return;
|
|
255
|
+
// dynamic import BufferGeometryUtils and find merge function name
|
|
256
|
+
const bufUtilsMod = await import('three/examples/jsm/utils/BufferGeometryUtils.js');
|
|
257
|
+
// use || chain (avoid mixing ?? with || without parentheses)
|
|
258
|
+
const mergeFn = bufUtilsMod.mergeBufferGeometries ||
|
|
259
|
+
bufUtilsMod.mergeGeometries ||
|
|
260
|
+
bufUtilsMod.mergeBufferGeometries || // defensive duplicate
|
|
261
|
+
bufUtilsMod.mergeGeometries;
|
|
262
|
+
if (!mergeFn)
|
|
263
|
+
throw new Error('No merge function found in BufferGeometryUtils');
|
|
264
|
+
// for each group, try merge
|
|
265
|
+
for (const [key, { material, geoms }] of groups) {
|
|
266
|
+
if (geoms.length <= 1) {
|
|
267
|
+
// nothing to merge
|
|
268
|
+
continue;
|
|
261
269
|
}
|
|
262
|
-
//
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
270
|
+
// call merge function - signature typically mergeBufferGeometries(array, useGroups)
|
|
271
|
+
const merged = mergeFn(geoms, false);
|
|
272
|
+
if (!merged) {
|
|
273
|
+
console.warn('merge returned null for group', key);
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
// create merged mesh at root (world-space geometry already applied)
|
|
277
|
+
const mergedMesh = new THREE.Mesh(merged, material);
|
|
278
|
+
root.add(mergedMesh);
|
|
279
|
+
}
|
|
280
|
+
// now remove original meshes flagged for removal
|
|
281
|
+
const toRemove = [];
|
|
282
|
+
root.traverse((ch) => {
|
|
283
|
+
if (ch.userData?.__toRemoveForMerge)
|
|
284
|
+
toRemove.push(ch);
|
|
285
|
+
});
|
|
286
|
+
toRemove.forEach((m) => {
|
|
287
|
+
if (m.parent)
|
|
288
|
+
m.parent.remove(m);
|
|
289
|
+
// free original resources (geometries already cloned/applied), but careful with shared materials
|
|
290
|
+
if (m.isMesh) {
|
|
291
|
+
const mm = m;
|
|
292
|
+
try {
|
|
293
|
+
mm.geometry.dispose();
|
|
280
294
|
}
|
|
281
|
-
|
|
295
|
+
catch { }
|
|
296
|
+
// we do NOT dispose material because it may be reused by mergedMesh
|
|
297
|
+
}
|
|
282
298
|
});
|
|
283
299
|
}
|
|
284
300
|
/* ---------------------
|
|
285
|
-
|
|
301
|
+
Dispose Utils
|
|
286
302
|
--------------------- */
|
|
287
|
-
/**
|
|
303
|
+
/** Completely dispose object: geometry, material and its textures (Danger: shared resources will be disposed) */
|
|
288
304
|
function disposeObject(obj) {
|
|
289
305
|
if (!obj)
|
|
290
306
|
return;
|
|
@@ -295,7 +311,7 @@ function disposeObject(obj) {
|
|
|
295
311
|
try {
|
|
296
312
|
m.geometry.dispose();
|
|
297
313
|
}
|
|
298
|
-
catch
|
|
314
|
+
catch { }
|
|
299
315
|
}
|
|
300
316
|
const mat = m.material;
|
|
301
317
|
if (mat) {
|
|
@@ -307,7 +323,7 @@ function disposeObject(obj) {
|
|
|
307
323
|
}
|
|
308
324
|
});
|
|
309
325
|
}
|
|
310
|
-
/**
|
|
326
|
+
/** Dispose material and its textures */
|
|
311
327
|
function disposeMaterial(mat) {
|
|
312
328
|
if (!mat)
|
|
313
329
|
return;
|
|
@@ -317,232 +333,244 @@ function disposeMaterial(mat) {
|
|
|
317
333
|
try {
|
|
318
334
|
mat[k].dispose();
|
|
319
335
|
}
|
|
320
|
-
catch
|
|
336
|
+
catch { }
|
|
321
337
|
}
|
|
322
338
|
});
|
|
323
339
|
try {
|
|
324
340
|
if (typeof mat.dispose === 'function')
|
|
325
341
|
mat.dispose();
|
|
326
342
|
}
|
|
327
|
-
catch
|
|
343
|
+
catch { }
|
|
344
|
+
}
|
|
345
|
+
// Helper to convert to simple material (stub)
|
|
346
|
+
function toSimpleMaterial(mat) {
|
|
347
|
+
// Basic implementation, preserve color/map
|
|
348
|
+
const m = new THREE.MeshBasicMaterial();
|
|
349
|
+
if (mat.color)
|
|
350
|
+
m.color.copy(mat.color);
|
|
351
|
+
if (mat.map)
|
|
352
|
+
m.map = mat.map;
|
|
353
|
+
return m;
|
|
328
354
|
}
|
|
329
355
|
|
|
330
|
-
/**
|
|
356
|
+
/**
|
|
357
|
+
* @file skyboxLoader.ts
|
|
358
|
+
* @description
|
|
359
|
+
* Utility for loading skyboxes (CubeTexture or Equirectangular/HDR).
|
|
360
|
+
*
|
|
361
|
+
* @best-practice
|
|
362
|
+
* - Use `loadSkybox` for a unified interface.
|
|
363
|
+
* - Supports internal caching to avoid reloading the same skybox.
|
|
364
|
+
* - Can set background and environment map independently.
|
|
365
|
+
*/
|
|
366
|
+
/** Default Values */
|
|
331
367
|
const DEFAULT_OPTIONS = {
|
|
332
368
|
setAsBackground: true,
|
|
333
369
|
setAsEnvironment: true,
|
|
334
370
|
useSRGBEncoding: true,
|
|
335
371
|
cache: true
|
|
336
372
|
};
|
|
337
|
-
/**
|
|
373
|
+
/** Internal Cache: key -> { handle, refCount } */
|
|
338
374
|
const cubeCache = new Map();
|
|
339
375
|
const equirectCache = new Map();
|
|
340
376
|
/* -------------------------------------------
|
|
341
|
-
|
|
377
|
+
Public Function: Load skybox (Automatically choose cube or equirect)
|
|
342
378
|
------------------------------------------- */
|
|
343
379
|
/**
|
|
344
|
-
*
|
|
345
|
-
* @param renderer THREE.WebGLRenderer -
|
|
380
|
+
* Load Cube Texture (6 images)
|
|
381
|
+
* @param renderer THREE.WebGLRenderer - Used for PMREM generating environment map
|
|
346
382
|
* @param scene THREE.Scene
|
|
347
|
-
* @param paths string[] 6
|
|
383
|
+
* @param paths string[] 6 image paths, order: [px, nx, py, ny, pz, nz]
|
|
348
384
|
* @param opts SkyboxOptions
|
|
349
385
|
*/
|
|
350
|
-
function loadCubeSkybox(
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
rec.refCount += 1;
|
|
361
|
-
// reapply to scene (in case it was removed)
|
|
362
|
-
if (options.setAsBackground)
|
|
363
|
-
scene.background = rec.handle.backgroundTexture;
|
|
364
|
-
if (options.setAsEnvironment && rec.handle.envRenderTarget)
|
|
365
|
-
scene.environment = rec.handle.envRenderTarget.texture;
|
|
366
|
-
return rec.handle;
|
|
367
|
-
}
|
|
368
|
-
// 加载立方体贴图
|
|
369
|
-
const loader = new THREE.CubeTextureLoader();
|
|
370
|
-
const texture = yield new Promise((resolve, reject) => {
|
|
371
|
-
loader.load(paths, (tex) => resolve(tex), undefined, (err) => reject(err));
|
|
372
|
-
});
|
|
373
|
-
// 设置编码与映射
|
|
374
|
-
if (options.useSRGBEncoding)
|
|
375
|
-
texture.encoding = THREE.sRGBEncoding;
|
|
376
|
-
texture.mapping = THREE.CubeReflectionMapping;
|
|
377
|
-
// apply as background if required
|
|
386
|
+
async function loadCubeSkybox(renderer, scene, paths, opts = {}) {
|
|
387
|
+
const options = { ...DEFAULT_OPTIONS, ...opts };
|
|
388
|
+
if (!Array.isArray(paths) || paths.length !== 6)
|
|
389
|
+
throw new Error('cube skybox requires 6 image paths');
|
|
390
|
+
const key = paths.join('|');
|
|
391
|
+
// Cache handling
|
|
392
|
+
if (options.cache && cubeCache.has(key)) {
|
|
393
|
+
const rec = cubeCache.get(key);
|
|
394
|
+
rec.refCount += 1;
|
|
395
|
+
// reapply to scene (in case it was removed)
|
|
378
396
|
if (options.setAsBackground)
|
|
379
|
-
scene.background =
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
397
|
+
scene.background = rec.handle.backgroundTexture;
|
|
398
|
+
if (options.setAsEnvironment && rec.handle.envRenderTarget)
|
|
399
|
+
scene.environment = rec.handle.envRenderTarget.texture;
|
|
400
|
+
return rec.handle;
|
|
401
|
+
}
|
|
402
|
+
// Load cube texture
|
|
403
|
+
const loader = new THREE.CubeTextureLoader();
|
|
404
|
+
const texture = await new Promise((resolve, reject) => {
|
|
405
|
+
loader.load(paths, (tex) => resolve(tex), undefined, (err) => reject(err));
|
|
406
|
+
});
|
|
407
|
+
// Set encoding and mapping
|
|
408
|
+
if (options.useSRGBEncoding)
|
|
409
|
+
texture.encoding = THREE.sRGBEncoding;
|
|
410
|
+
texture.mapping = THREE.CubeReflectionMapping;
|
|
411
|
+
// apply as background if required
|
|
412
|
+
if (options.setAsBackground)
|
|
413
|
+
scene.background = texture;
|
|
414
|
+
// environment: use PMREM to produce a proper prefiltered env map for PBR
|
|
415
|
+
let pmremGenerator = options.pmremGenerator ?? new THREE.PMREMGenerator(renderer);
|
|
416
|
+
pmremGenerator.compileCubemapShader?.( /* optional */);
|
|
417
|
+
// fromCubemap might be available in your three.js; fallback to fromEquirectangular approach if not
|
|
418
|
+
let envRenderTarget = null;
|
|
419
|
+
if (pmremGenerator.fromCubemap) {
|
|
420
|
+
envRenderTarget = pmremGenerator.fromCubemap(texture);
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
// Fallback: render cube to env map by using generator.fromEquirectangular with a converted equirect if needed.
|
|
424
|
+
// Simpler fallback: use the cube texture directly as environment (less correct for reflections).
|
|
425
|
+
envRenderTarget = null;
|
|
426
|
+
}
|
|
427
|
+
if (options.setAsEnvironment) {
|
|
428
|
+
if (envRenderTarget) {
|
|
429
|
+
scene.environment = envRenderTarget.texture;
|
|
387
430
|
}
|
|
388
431
|
else {
|
|
389
|
-
//
|
|
390
|
-
|
|
391
|
-
envRenderTarget = null;
|
|
432
|
+
// fallback: use cube texture directly (works but not prefiltered)
|
|
433
|
+
scene.environment = texture;
|
|
392
434
|
}
|
|
393
|
-
|
|
435
|
+
}
|
|
436
|
+
const handle = {
|
|
437
|
+
key,
|
|
438
|
+
backgroundTexture: options.setAsBackground ? texture : null,
|
|
439
|
+
envRenderTarget: envRenderTarget,
|
|
440
|
+
pmremGenerator: options.pmremGenerator ? null : pmremGenerator, // only dispose if we created it
|
|
441
|
+
setAsBackground: !!options.setAsBackground,
|
|
442
|
+
setAsEnvironment: !!options.setAsEnvironment,
|
|
443
|
+
dispose() {
|
|
444
|
+
// remove from scene
|
|
445
|
+
if (options.setAsBackground && scene.background === texture)
|
|
446
|
+
scene.background = null;
|
|
447
|
+
if (options.setAsEnvironment && scene.environment) {
|
|
448
|
+
// only clear if it's the same texture we set
|
|
449
|
+
if (envRenderTarget && scene.environment === envRenderTarget.texture)
|
|
450
|
+
scene.environment = null;
|
|
451
|
+
else if (scene.environment === texture)
|
|
452
|
+
scene.environment = null;
|
|
453
|
+
}
|
|
454
|
+
// dispose resources only if not cached/shared
|
|
394
455
|
if (envRenderTarget) {
|
|
395
|
-
|
|
456
|
+
try {
|
|
457
|
+
envRenderTarget.dispose();
|
|
458
|
+
}
|
|
459
|
+
catch { }
|
|
396
460
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
scene.environment = texture;
|
|
461
|
+
try {
|
|
462
|
+
texture.dispose();
|
|
400
463
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
backgroundTexture: options.setAsBackground ? texture : null,
|
|
405
|
-
envRenderTarget: envRenderTarget,
|
|
406
|
-
pmremGenerator: options.pmremGenerator ? null : pmremGenerator, // only dispose if we created it
|
|
407
|
-
setAsBackground: !!options.setAsBackground,
|
|
408
|
-
setAsEnvironment: !!options.setAsEnvironment,
|
|
409
|
-
dispose() {
|
|
410
|
-
// remove from scene
|
|
411
|
-
if (options.setAsBackground && scene.background === texture)
|
|
412
|
-
scene.background = null;
|
|
413
|
-
if (options.setAsEnvironment && scene.environment) {
|
|
414
|
-
// only clear if it's the same texture we set
|
|
415
|
-
if (envRenderTarget && scene.environment === envRenderTarget.texture)
|
|
416
|
-
scene.environment = null;
|
|
417
|
-
else if (scene.environment === texture)
|
|
418
|
-
scene.environment = null;
|
|
419
|
-
}
|
|
420
|
-
// dispose resources only if not cached/shared
|
|
421
|
-
if (envRenderTarget) {
|
|
422
|
-
try {
|
|
423
|
-
envRenderTarget.dispose();
|
|
424
|
-
}
|
|
425
|
-
catch (_a) { }
|
|
426
|
-
}
|
|
464
|
+
catch { }
|
|
465
|
+
// dispose pmremGenerator we created
|
|
466
|
+
if (!options.pmremGenerator && pmremGenerator) {
|
|
427
467
|
try {
|
|
428
|
-
|
|
429
|
-
}
|
|
430
|
-
catch (_b) { }
|
|
431
|
-
// dispose pmremGenerator we created
|
|
432
|
-
if (!options.pmremGenerator && pmremGenerator) {
|
|
433
|
-
try {
|
|
434
|
-
pmremGenerator.dispose();
|
|
435
|
-
}
|
|
436
|
-
catch (_c) { }
|
|
468
|
+
pmremGenerator.dispose();
|
|
437
469
|
}
|
|
470
|
+
catch { }
|
|
438
471
|
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
if (options.cache)
|
|
475
|
+
cubeCache.set(key, { handle, refCount: 1 });
|
|
476
|
+
return handle;
|
|
444
477
|
}
|
|
445
478
|
/**
|
|
446
|
-
*
|
|
479
|
+
* Load Equirectangular/Single Image (Supports HDR via RGBELoader)
|
|
447
480
|
* @param renderer THREE.WebGLRenderer
|
|
448
481
|
* @param scene THREE.Scene
|
|
449
482
|
* @param url string - *.hdr, *.exr, *.jpg, *.png
|
|
450
483
|
* @param opts SkyboxOptions
|
|
451
484
|
*/
|
|
452
|
-
function loadEquirectSkybox(
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
//
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
485
|
+
async function loadEquirectSkybox(renderer, scene, url, opts = {}) {
|
|
486
|
+
const options = { ...DEFAULT_OPTIONS, ...opts };
|
|
487
|
+
const key = url;
|
|
488
|
+
if (options.cache && equirectCache.has(key)) {
|
|
489
|
+
const rec = equirectCache.get(key);
|
|
490
|
+
rec.refCount += 1;
|
|
491
|
+
if (options.setAsBackground)
|
|
492
|
+
scene.background = rec.handle.backgroundTexture;
|
|
493
|
+
if (options.setAsEnvironment && rec.handle.envRenderTarget)
|
|
494
|
+
scene.environment = rec.handle.envRenderTarget.texture;
|
|
495
|
+
return rec.handle;
|
|
496
|
+
}
|
|
497
|
+
// Dynamically import RGBELoader (for .hdr/.exr), if loading normal jpg/png directly use TextureLoader
|
|
498
|
+
const isHDR = /\.hdr$|\.exr$/i.test(url);
|
|
499
|
+
let hdrTexture;
|
|
500
|
+
if (isHDR) {
|
|
501
|
+
const { RGBELoader } = await import('three/examples/jsm/loaders/RGBELoader.js');
|
|
502
|
+
hdrTexture = await new Promise((resolve, reject) => {
|
|
503
|
+
new RGBELoader().load(url, (tex) => resolve(tex), undefined, (err) => reject(err));
|
|
504
|
+
});
|
|
505
|
+
// RGBE textures typically use LinearEncoding
|
|
506
|
+
hdrTexture.encoding = THREE.LinearEncoding;
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
// ordinary image - use TextureLoader
|
|
510
|
+
const loader = new THREE.TextureLoader();
|
|
511
|
+
hdrTexture = await new Promise((resolve, reject) => {
|
|
512
|
+
loader.load(url, (t) => resolve(t), undefined, (err) => reject(err));
|
|
513
|
+
});
|
|
514
|
+
if (options.useSRGBEncoding)
|
|
515
|
+
hdrTexture.encoding = THREE.sRGBEncoding;
|
|
516
|
+
}
|
|
517
|
+
// PMREMGenerator to convert equirectangular to prefiltered cubemap (good for PBR)
|
|
518
|
+
const pmremGenerator = options.pmremGenerator ?? new THREE.PMREMGenerator(renderer);
|
|
519
|
+
pmremGenerator.compileEquirectangularShader?.();
|
|
520
|
+
const envRenderTarget = pmremGenerator.fromEquirectangular(hdrTexture);
|
|
521
|
+
// envTexture to use for scene.environment
|
|
522
|
+
const envTexture = envRenderTarget.texture;
|
|
523
|
+
// set background and/or environment
|
|
524
|
+
if (options.setAsBackground) {
|
|
525
|
+
// for background it's ok to use the equirect texture directly or the envTexture
|
|
526
|
+
// envTexture is cubemap-like and usually better for reflections; using it as background creates cube-projected look
|
|
527
|
+
scene.background = envTexture;
|
|
528
|
+
}
|
|
529
|
+
if (options.setAsEnvironment) {
|
|
530
|
+
scene.environment = envTexture;
|
|
531
|
+
}
|
|
532
|
+
// We can dispose the original hdrTexture (the PMREM target contains the needed data)
|
|
533
|
+
try {
|
|
534
|
+
hdrTexture.dispose();
|
|
535
|
+
}
|
|
536
|
+
catch { }
|
|
537
|
+
const handle = {
|
|
538
|
+
key,
|
|
539
|
+
backgroundTexture: options.setAsBackground ? envTexture : null,
|
|
540
|
+
envRenderTarget,
|
|
541
|
+
pmremGenerator: options.pmremGenerator ? null : pmremGenerator,
|
|
542
|
+
setAsBackground: !!options.setAsBackground,
|
|
543
|
+
setAsEnvironment: !!options.setAsEnvironment,
|
|
544
|
+
dispose() {
|
|
545
|
+
if (options.setAsBackground && scene.background === envTexture)
|
|
546
|
+
scene.background = null;
|
|
547
|
+
if (options.setAsEnvironment && scene.environment === envTexture)
|
|
548
|
+
scene.environment = null;
|
|
549
|
+
try {
|
|
550
|
+
envRenderTarget.dispose();
|
|
551
|
+
}
|
|
552
|
+
catch { }
|
|
553
|
+
if (!options.pmremGenerator && pmremGenerator) {
|
|
518
554
|
try {
|
|
519
|
-
|
|
520
|
-
}
|
|
521
|
-
catch (_a) { }
|
|
522
|
-
if (!options.pmremGenerator && pmremGenerator) {
|
|
523
|
-
try {
|
|
524
|
-
pmremGenerator.dispose();
|
|
525
|
-
}
|
|
526
|
-
catch (_b) { }
|
|
555
|
+
pmremGenerator.dispose();
|
|
527
556
|
}
|
|
557
|
+
catch { }
|
|
528
558
|
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
if (options.cache)
|
|
562
|
+
equirectCache.set(key, { handle, refCount: 1 });
|
|
563
|
+
return handle;
|
|
534
564
|
}
|
|
535
|
-
function loadSkybox(
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
return loadEquirectSkybox(renderer, scene, params.url, opts);
|
|
540
|
-
});
|
|
565
|
+
async function loadSkybox(renderer, scene, params, opts = {}) {
|
|
566
|
+
if (params.type === 'cube')
|
|
567
|
+
return loadCubeSkybox(renderer, scene, params.paths, opts);
|
|
568
|
+
return loadEquirectSkybox(renderer, scene, params.url, opts);
|
|
541
569
|
}
|
|
542
570
|
/* -------------------------
|
|
543
|
-
|
|
571
|
+
Cache / Reference Counting Helper Methods
|
|
544
572
|
------------------------- */
|
|
545
|
-
/**
|
|
573
|
+
/** Release a cached skybox (decrements refCount, only truly disposes when refCount=0) */
|
|
546
574
|
function releaseSkybox(handle) {
|
|
547
575
|
// check cube cache
|
|
548
576
|
if (cubeCache.has(handle.key)) {
|
|
@@ -567,85 +595,94 @@ function releaseSkybox(handle) {
|
|
|
567
595
|
// handle.dispose()
|
|
568
596
|
}
|
|
569
597
|
|
|
570
|
-
// utils/BlueSkyManager.ts - 优化版
|
|
571
598
|
/**
|
|
572
|
-
*
|
|
599
|
+
* @file blueSkyManager.ts
|
|
600
|
+
* @description
|
|
601
|
+
* Global singleton manager for loading and managing HDR/EXR blue sky environment maps.
|
|
602
|
+
*
|
|
603
|
+
* @best-practice
|
|
604
|
+
* - Call `init` once before use.
|
|
605
|
+
* - Use `loadAsync` to load skyboxes with progress tracking.
|
|
606
|
+
* - Automatically handles PMREM generation for realistic lighting.
|
|
607
|
+
*/
|
|
608
|
+
/**
|
|
609
|
+
* BlueSkyManager - Optimized
|
|
573
610
|
* ---------------------------------------------------------
|
|
574
|
-
*
|
|
611
|
+
* A global singleton manager for loading and managing HDR/EXR based blue sky environment maps.
|
|
575
612
|
*
|
|
576
|
-
*
|
|
577
|
-
* -
|
|
578
|
-
* -
|
|
579
|
-
* -
|
|
580
|
-
* -
|
|
581
|
-
* -
|
|
613
|
+
* Features:
|
|
614
|
+
* - Adds load progress callback
|
|
615
|
+
* - Supports load cancellation
|
|
616
|
+
* - Improved error handling
|
|
617
|
+
* - Returns Promise for async operation
|
|
618
|
+
* - Adds loading state management
|
|
582
619
|
*/
|
|
583
620
|
class BlueSkyManager {
|
|
584
621
|
constructor() {
|
|
585
|
-
/**
|
|
622
|
+
/** RenderTarget for current environment map, used for subsequent disposal */
|
|
586
623
|
this.skyRT = null;
|
|
587
|
-
/**
|
|
624
|
+
/** Whether already initialized */
|
|
588
625
|
this.isInitialized = false;
|
|
589
|
-
/**
|
|
626
|
+
/** Current loader, used for cancelling load */
|
|
590
627
|
this.currentLoader = null;
|
|
591
|
-
/**
|
|
628
|
+
/** Loading state */
|
|
592
629
|
this.loadingState = 'idle';
|
|
593
630
|
}
|
|
594
631
|
/**
|
|
595
|
-
*
|
|
632
|
+
* Initialize
|
|
596
633
|
* ---------------------------------------------------------
|
|
597
|
-
*
|
|
598
|
-
* @param renderer WebGLRenderer
|
|
599
|
-
* @param scene Three.js
|
|
600
|
-
* @param exposure
|
|
634
|
+
* Must be called once before using BlueSkyManager.
|
|
635
|
+
* @param renderer WebGLRenderer instance
|
|
636
|
+
* @param scene Three.js Scene
|
|
637
|
+
* @param exposure Exposure (default 1.0)
|
|
601
638
|
*/
|
|
602
639
|
init(renderer, scene, exposure = 1.0) {
|
|
603
640
|
if (this.isInitialized) {
|
|
604
|
-
console.warn('BlueSkyManager:
|
|
641
|
+
console.warn('BlueSkyManager: Already initialized, skipping duplicate initialization');
|
|
605
642
|
return;
|
|
606
643
|
}
|
|
607
644
|
this.renderer = renderer;
|
|
608
645
|
this.scene = scene;
|
|
609
|
-
//
|
|
646
|
+
// Use ACESFilmicToneMapping, effect is closer to reality
|
|
610
647
|
this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
611
648
|
this.renderer.toneMappingExposure = exposure;
|
|
612
|
-
//
|
|
649
|
+
// Initialize PMREM generator (only one needed globally)
|
|
613
650
|
this.pmremGen = new THREE.PMREMGenerator(renderer);
|
|
614
651
|
this.pmremGen.compileEquirectangularShader();
|
|
615
652
|
this.isInitialized = true;
|
|
616
653
|
}
|
|
617
654
|
/**
|
|
618
|
-
*
|
|
655
|
+
* Load blue sky HDR/EXR map and apply to scene (Promise version)
|
|
619
656
|
* ---------------------------------------------------------
|
|
620
|
-
* @param exrPath HDR/EXR
|
|
621
|
-
* @param options
|
|
657
|
+
* @param exrPath HDR/EXR file path
|
|
658
|
+
* @param options Load options
|
|
622
659
|
* @returns Promise<void>
|
|
623
660
|
*/
|
|
624
661
|
loadAsync(exrPath, options = {}) {
|
|
625
662
|
if (!this.isInitialized) {
|
|
626
663
|
return Promise.reject(new Error('BlueSkyManager not initialized!'));
|
|
627
664
|
}
|
|
628
|
-
//
|
|
665
|
+
// Cancel previous load
|
|
629
666
|
this.cancelLoad();
|
|
630
667
|
const { background = true, onProgress, onComplete, onError } = options;
|
|
631
668
|
this.loadingState = 'loading';
|
|
632
669
|
this.currentLoader = new EXRLoader();
|
|
633
670
|
return new Promise((resolve, reject) => {
|
|
634
671
|
this.currentLoader.load(exrPath,
|
|
635
|
-
//
|
|
672
|
+
// Success callback
|
|
636
673
|
(texture) => {
|
|
637
674
|
try {
|
|
638
|
-
//
|
|
675
|
+
// Set texture mapping to EquirectangularReflectionMapping
|
|
639
676
|
texture.mapping = THREE.EquirectangularReflectionMapping;
|
|
640
|
-
//
|
|
677
|
+
// Clear old environment map
|
|
641
678
|
this.dispose();
|
|
642
|
-
//
|
|
679
|
+
// Generate efficient environment map using PMREM
|
|
643
680
|
this.skyRT = this.pmremGen.fromEquirectangular(texture);
|
|
644
|
-
//
|
|
681
|
+
// Apply to scene: Environment Lighting & Background
|
|
645
682
|
this.scene.environment = this.skyRT.texture;
|
|
646
683
|
if (background)
|
|
647
684
|
this.scene.background = this.skyRT.texture;
|
|
648
|
-
//
|
|
685
|
+
// Dispose original HDR/EXR texture immediately to save memory
|
|
649
686
|
texture.dispose();
|
|
650
687
|
this.loadingState = 'loaded';
|
|
651
688
|
this.currentLoader = null;
|
|
@@ -663,14 +700,14 @@ class BlueSkyManager {
|
|
|
663
700
|
reject(error);
|
|
664
701
|
}
|
|
665
702
|
},
|
|
666
|
-
//
|
|
703
|
+
// Progress callback
|
|
667
704
|
(xhr) => {
|
|
668
705
|
if (onProgress && xhr.lengthComputable) {
|
|
669
706
|
const progress = xhr.loaded / xhr.total;
|
|
670
707
|
onProgress(progress);
|
|
671
708
|
}
|
|
672
709
|
},
|
|
673
|
-
//
|
|
710
|
+
// Error callback
|
|
674
711
|
(err) => {
|
|
675
712
|
this.loadingState = 'error';
|
|
676
713
|
this.currentLoader = null;
|
|
@@ -682,10 +719,10 @@ class BlueSkyManager {
|
|
|
682
719
|
});
|
|
683
720
|
}
|
|
684
721
|
/**
|
|
685
|
-
*
|
|
722
|
+
* Load blue sky HDR/EXR map and apply to scene (Sync API, for backward compatibility)
|
|
686
723
|
* ---------------------------------------------------------
|
|
687
|
-
* @param exrPath HDR/EXR
|
|
688
|
-
* @param background
|
|
724
|
+
* @param exrPath HDR/EXR file path
|
|
725
|
+
* @param background Whether to apply as scene background (default true)
|
|
689
726
|
*/
|
|
690
727
|
load(exrPath, background = true) {
|
|
691
728
|
this.loadAsync(exrPath, { background }).catch((error) => {
|
|
@@ -693,32 +730,32 @@ class BlueSkyManager {
|
|
|
693
730
|
});
|
|
694
731
|
}
|
|
695
732
|
/**
|
|
696
|
-
*
|
|
733
|
+
* Cancel current load
|
|
697
734
|
*/
|
|
698
735
|
cancelLoad() {
|
|
699
736
|
if (this.currentLoader) {
|
|
700
|
-
// EXRLoader
|
|
737
|
+
// EXRLoader itself does not have abort method, but we can clear the reference
|
|
701
738
|
this.currentLoader = null;
|
|
702
739
|
this.loadingState = 'idle';
|
|
703
740
|
}
|
|
704
741
|
}
|
|
705
742
|
/**
|
|
706
|
-
*
|
|
743
|
+
* Get loading state
|
|
707
744
|
*/
|
|
708
745
|
getLoadingState() {
|
|
709
746
|
return this.loadingState;
|
|
710
747
|
}
|
|
711
748
|
/**
|
|
712
|
-
*
|
|
749
|
+
* Is loading
|
|
713
750
|
*/
|
|
714
751
|
isLoading() {
|
|
715
752
|
return this.loadingState === 'loading';
|
|
716
753
|
}
|
|
717
754
|
/**
|
|
718
|
-
*
|
|
755
|
+
* Release current sky texture resources
|
|
719
756
|
* ---------------------------------------------------------
|
|
720
|
-
*
|
|
721
|
-
*
|
|
757
|
+
* Only cleans up skyRT, does not destroy PMREM
|
|
758
|
+
* Suitable for calling when switching HDR/EXR files
|
|
722
759
|
*/
|
|
723
760
|
dispose() {
|
|
724
761
|
if (this.skyRT) {
|
|
@@ -732,27 +769,26 @@ class BlueSkyManager {
|
|
|
732
769
|
this.scene.environment = null;
|
|
733
770
|
}
|
|
734
771
|
/**
|
|
735
|
-
*
|
|
772
|
+
* Completely destroy BlueSkyManager
|
|
736
773
|
* ---------------------------------------------------------
|
|
737
|
-
*
|
|
738
|
-
*
|
|
774
|
+
* Includes destruction of PMREMGenerator
|
|
775
|
+
* Usually called when the scene is completely destroyed or the application exits
|
|
739
776
|
*/
|
|
740
777
|
destroy() {
|
|
741
|
-
var _a;
|
|
742
778
|
this.cancelLoad();
|
|
743
779
|
this.dispose();
|
|
744
|
-
|
|
780
|
+
this.pmremGen?.dispose();
|
|
745
781
|
this.isInitialized = false;
|
|
746
782
|
this.loadingState = 'idle';
|
|
747
783
|
}
|
|
748
784
|
}
|
|
749
785
|
/**
|
|
750
|
-
*
|
|
786
|
+
* Global Singleton
|
|
751
787
|
* ---------------------------------------------------------
|
|
752
|
-
*
|
|
753
|
-
*
|
|
788
|
+
* Directly export a globally unique BlueSkyManager instance,
|
|
789
|
+
* Ensuring only one PMREMGenerator is used throughout the application for best performance.
|
|
754
790
|
*/
|
|
755
791
|
const BlueSky = new BlueSkyManager();
|
|
756
792
|
|
|
757
|
-
export { BlueSky, disposeMaterial, disposeObject, loadCubeSkybox, loadEquirectSkybox, loadModelByUrl, loadSkybox, releaseSkybox };
|
|
793
|
+
export { BlueSky, disposeMaterial, disposeObject, getLoaderConfig, loadCubeSkybox, loadEquirectSkybox, loadModelByUrl, loadSkybox, releaseSkybox, setLoaderConfig };
|
|
758
794
|
//# sourceMappingURL=index.mjs.map
|