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