@chocozhang/three-model-render 1.0.4 → 1.0.6
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 +46 -6
- package/dist/camera/index.js +6 -10
- package/dist/camera/index.js.map +1 -1
- package/dist/camera/index.mjs +6 -10
- package/dist/camera/index.mjs.map +1 -1
- package/dist/core/index.d.ts +21 -1
- package/dist/core/index.js +70 -9
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +70 -10
- package/dist/core/index.mjs.map +1 -1
- package/dist/effect/index.js +185 -230
- package/dist/effect/index.js.map +1 -1
- package/dist/effect/index.mjs +185 -230
- package/dist/effect/index.mjs.map +1 -1
- package/dist/index.d.ts +61 -28
- package/dist/index.js +812 -806
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +808 -807
- package/dist/index.mjs.map +1 -1
- package/dist/interaction/index.d.ts +12 -6
- package/dist/interaction/index.js +26 -14
- package/dist/interaction/index.js.map +1 -1
- package/dist/interaction/index.mjs +26 -14
- package/dist/interaction/index.mjs.map +1 -1
- package/dist/loader/index.d.ts +17 -2
- package/dist/loader/index.js +385 -386
- package/dist/loader/index.js.map +1 -1
- package/dist/loader/index.mjs +384 -387
- package/dist/loader/index.mjs.map +1 -1
- package/dist/setup/index.d.ts +13 -21
- package/dist/setup/index.js +120 -167
- package/dist/setup/index.js.map +1 -1
- package/dist/setup/index.mjs +119 -168
- package/dist/setup/index.mjs.map +1 -1
- package/dist/ui/index.js +15 -17
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/index.mjs +15 -17
- package/dist/ui/index.mjs.map +1 -1
- package/package.json +49 -21
package/dist/loader/index.js
CHANGED
|
@@ -22,37 +22,22 @@ 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
|
|
|
57
42
|
/**
|
|
58
43
|
* @file modelLoader.ts
|
|
@@ -70,19 +55,22 @@ const DEFAULT_OPTIONS$1 = {
|
|
|
70
55
|
maxTextureSize: null,
|
|
71
56
|
useSimpleMaterials: false,
|
|
72
57
|
skipSkinned: true,
|
|
58
|
+
useCache: true,
|
|
73
59
|
};
|
|
60
|
+
const modelCache = new Map();
|
|
74
61
|
/** Automatically determine which options to enable based on extension (smart judgment) */
|
|
75
62
|
function normalizeOptions(url, opts) {
|
|
76
63
|
const ext = (url.split('.').pop() || '').toLowerCase();
|
|
77
|
-
const merged =
|
|
64
|
+
const merged = { ...DEFAULT_OPTIONS$1, ...opts };
|
|
78
65
|
if (ext === 'gltf' || ext === 'glb') {
|
|
66
|
+
const globalConfig = getLoaderConfig();
|
|
79
67
|
// gltf/glb defaults to trying draco/ktx2 if user didn't specify
|
|
80
68
|
if (merged.dracoDecoderPath === undefined)
|
|
81
|
-
merged.dracoDecoderPath =
|
|
69
|
+
merged.dracoDecoderPath = globalConfig.dracoDecoderPath;
|
|
82
70
|
if (merged.useKTX2 === undefined)
|
|
83
71
|
merged.useKTX2 = true;
|
|
84
72
|
if (merged.ktx2TranscoderPath === undefined)
|
|
85
|
-
merged.ktx2TranscoderPath =
|
|
73
|
+
merged.ktx2TranscoderPath = globalConfig.ktx2TranscoderPath;
|
|
86
74
|
}
|
|
87
75
|
else {
|
|
88
76
|
// fbx/obj/ply/stl etc. do not need draco/ktx2
|
|
@@ -92,103 +80,108 @@ function normalizeOptions(url, opts) {
|
|
|
92
80
|
}
|
|
93
81
|
return merged;
|
|
94
82
|
}
|
|
95
|
-
function loadModelByUrl(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const ktx2Loader = new KTX2Loader().setTranscoderPath(opts.ktx2TranscoderPath);
|
|
116
|
-
gltfLoader.__ktx2Loader = ktx2Loader;
|
|
117
|
-
}
|
|
118
|
-
loader = gltfLoader;
|
|
119
|
-
}
|
|
120
|
-
else if (ext === 'fbx') {
|
|
121
|
-
const { FBXLoader } = yield import('three/examples/jsm/loaders/FBXLoader.js');
|
|
122
|
-
loader = new FBXLoader(manager);
|
|
123
|
-
}
|
|
124
|
-
else if (ext === 'obj') {
|
|
125
|
-
const { OBJLoader } = yield import('three/examples/jsm/loaders/OBJLoader.js');
|
|
126
|
-
loader = new OBJLoader(manager);
|
|
127
|
-
}
|
|
128
|
-
else if (ext === 'ply') {
|
|
129
|
-
const { PLYLoader } = yield import('three/examples/jsm/loaders/PLYLoader.js');
|
|
130
|
-
loader = new PLYLoader(manager);
|
|
131
|
-
}
|
|
132
|
-
else if (ext === 'stl') {
|
|
133
|
-
const { STLLoader } = yield import('three/examples/jsm/loaders/STLLoader.js');
|
|
134
|
-
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);
|
|
135
103
|
}
|
|
136
|
-
|
|
137
|
-
|
|
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;
|
|
138
108
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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);
|
|
164
139
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
downscaleTexturesInObject(object, opts.maxTextureSize);
|
|
168
|
-
if (opts.useSimpleMaterials) {
|
|
169
|
-
object.traverse((child) => {
|
|
170
|
-
const m = child.material;
|
|
171
|
-
if (!m)
|
|
172
|
-
return;
|
|
173
|
-
if (Array.isArray(m))
|
|
174
|
-
child.material = m.map((mat) => toSimpleMaterial(mat));
|
|
175
|
-
else
|
|
176
|
-
child.material = toSimpleMaterial(m);
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
if (opts.mergeGeometries) {
|
|
180
|
-
try {
|
|
181
|
-
yield tryMergeGeometries(object, { skipSkinned: (_b = opts.skipSkinned) !== null && _b !== void 0 ? _b : true });
|
|
140
|
+
else {
|
|
141
|
+
resolve(res);
|
|
182
142
|
}
|
|
183
|
-
|
|
184
|
-
|
|
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;
|
|
185
151
|
}
|
|
152
|
+
catch { }
|
|
186
153
|
}
|
|
187
|
-
return object;
|
|
188
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;
|
|
189
181
|
}
|
|
190
|
-
/** Runtime downscale textures in mesh to maxSize (canvas
|
|
191
|
-
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 = [];
|
|
192
185
|
obj.traverse((ch) => {
|
|
193
186
|
if (!ch.isMesh)
|
|
194
187
|
return;
|
|
@@ -207,27 +200,44 @@ function downscaleTexturesInObject(obj, maxSize) {
|
|
|
207
200
|
const max = maxSize;
|
|
208
201
|
if (image.width <= max && image.height <= max)
|
|
209
202
|
return;
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
+
}
|
|
224
233
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
234
|
+
catch (e) {
|
|
235
|
+
console.warn('downscale texture failed', e);
|
|
236
|
+
}
|
|
237
|
+
})());
|
|
229
238
|
});
|
|
230
239
|
});
|
|
240
|
+
await Promise.all(tasks);
|
|
231
241
|
}
|
|
232
242
|
/**
|
|
233
243
|
* Try to merge geometries in object (Only merge: non-transparent, non-SkinnedMesh, attribute compatible BufferGeometry)
|
|
@@ -235,81 +245,77 @@ function downscaleTexturesInObject(obj, maxSize) {
|
|
|
235
245
|
* - Merging will group by material UUID (different materials cannot be merged)
|
|
236
246
|
* - Merge function is compatible with common export names of BufferGeometryUtils
|
|
237
247
|
*/
|
|
238
|
-
function tryMergeGeometries(root, opts) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
var _a;
|
|
244
|
-
if (!ch.isMesh)
|
|
245
|
-
return;
|
|
246
|
-
const mesh = ch;
|
|
247
|
-
if (opts.skipSkinned && mesh.isSkinnedMesh)
|
|
248
|
-
return;
|
|
249
|
-
const mat = mesh.material;
|
|
250
|
-
// don't merge transparent or morph-enabled or skinned meshes
|
|
251
|
-
if (!mesh.geometry || mesh.visible === false)
|
|
252
|
-
return;
|
|
253
|
-
if (mat && mat.transparent)
|
|
254
|
-
return;
|
|
255
|
-
const geom = mesh.geometry.clone();
|
|
256
|
-
mesh.updateWorldMatrix(true, false);
|
|
257
|
-
geom.applyMatrix4(mesh.matrixWorld);
|
|
258
|
-
// ensure attributes compatible? we'll rely on merge function to return null if incompatible
|
|
259
|
-
const key = (mat && mat.uuid) || 'default';
|
|
260
|
-
const bucket = (_a = groups.get(key)) !== null && _a !== void 0 ? _a : { material: mat !== null && mat !== void 0 ? mat : new THREE__namespace.MeshStandardMaterial(), geoms: [] };
|
|
261
|
-
bucket.geoms.push(geom);
|
|
262
|
-
groups.set(key, bucket);
|
|
263
|
-
// mark for removal (we'll remove meshes after)
|
|
264
|
-
mesh.userData.__toRemoveForMerge = true;
|
|
265
|
-
});
|
|
266
|
-
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)
|
|
267
253
|
return;
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if (
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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;
|
|
292
290
|
}
|
|
293
|
-
//
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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();
|
|
311
315
|
}
|
|
312
|
-
|
|
316
|
+
catch { }
|
|
317
|
+
// we do NOT dispose material because it may be reused by mergedMesh
|
|
318
|
+
}
|
|
313
319
|
});
|
|
314
320
|
}
|
|
315
321
|
/* ---------------------
|
|
@@ -326,7 +332,7 @@ function disposeObject(obj) {
|
|
|
326
332
|
try {
|
|
327
333
|
m.geometry.dispose();
|
|
328
334
|
}
|
|
329
|
-
catch
|
|
335
|
+
catch { }
|
|
330
336
|
}
|
|
331
337
|
const mat = m.material;
|
|
332
338
|
if (mat) {
|
|
@@ -348,14 +354,14 @@ function disposeMaterial(mat) {
|
|
|
348
354
|
try {
|
|
349
355
|
mat[k].dispose();
|
|
350
356
|
}
|
|
351
|
-
catch
|
|
357
|
+
catch { }
|
|
352
358
|
}
|
|
353
359
|
});
|
|
354
360
|
try {
|
|
355
361
|
if (typeof mat.dispose === 'function')
|
|
356
362
|
mat.dispose();
|
|
357
363
|
}
|
|
358
|
-
catch
|
|
364
|
+
catch { }
|
|
359
365
|
}
|
|
360
366
|
// Helper to convert to simple material (stub)
|
|
361
367
|
function toSimpleMaterial(mat) {
|
|
@@ -398,100 +404,97 @@ const equirectCache = new Map();
|
|
|
398
404
|
* @param paths string[] 6 image paths, order: [px, nx, py, ny, pz, nz]
|
|
399
405
|
* @param opts SkyboxOptions
|
|
400
406
|
*/
|
|
401
|
-
function loadCubeSkybox(
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
rec.refCount += 1;
|
|
412
|
-
// reapply to scene (in case it was removed)
|
|
413
|
-
if (options.setAsBackground)
|
|
414
|
-
scene.background = rec.handle.backgroundTexture;
|
|
415
|
-
if (options.setAsEnvironment && rec.handle.envRenderTarget)
|
|
416
|
-
scene.environment = rec.handle.envRenderTarget.texture;
|
|
417
|
-
return rec.handle;
|
|
418
|
-
}
|
|
419
|
-
// Load cube texture
|
|
420
|
-
const loader = new THREE__namespace.CubeTextureLoader();
|
|
421
|
-
const texture = yield new Promise((resolve, reject) => {
|
|
422
|
-
loader.load(paths, (tex) => resolve(tex), undefined, (err) => reject(err));
|
|
423
|
-
});
|
|
424
|
-
// Set encoding and mapping
|
|
425
|
-
if (options.useSRGBEncoding)
|
|
426
|
-
texture.encoding = THREE__namespace.sRGBEncoding;
|
|
427
|
-
texture.mapping = THREE__namespace.CubeReflectionMapping;
|
|
428
|
-
// 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)
|
|
429
417
|
if (options.setAsBackground)
|
|
430
|
-
scene.background =
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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;
|
|
438
451
|
}
|
|
439
452
|
else {
|
|
440
|
-
//
|
|
441
|
-
|
|
442
|
-
envRenderTarget = null;
|
|
453
|
+
// fallback: use cube texture directly (works but not prefiltered)
|
|
454
|
+
scene.environment = texture;
|
|
443
455
|
}
|
|
444
|
-
|
|
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
|
|
445
476
|
if (envRenderTarget) {
|
|
446
|
-
|
|
477
|
+
try {
|
|
478
|
+
envRenderTarget.dispose();
|
|
479
|
+
}
|
|
480
|
+
catch { }
|
|
447
481
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
scene.environment = texture;
|
|
482
|
+
try {
|
|
483
|
+
texture.dispose();
|
|
451
484
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
backgroundTexture: options.setAsBackground ? texture : null,
|
|
456
|
-
envRenderTarget: envRenderTarget,
|
|
457
|
-
pmremGenerator: options.pmremGenerator ? null : pmremGenerator, // only dispose if we created it
|
|
458
|
-
setAsBackground: !!options.setAsBackground,
|
|
459
|
-
setAsEnvironment: !!options.setAsEnvironment,
|
|
460
|
-
dispose() {
|
|
461
|
-
// remove from scene
|
|
462
|
-
if (options.setAsBackground && scene.background === texture)
|
|
463
|
-
scene.background = null;
|
|
464
|
-
if (options.setAsEnvironment && scene.environment) {
|
|
465
|
-
// only clear if it's the same texture we set
|
|
466
|
-
if (envRenderTarget && scene.environment === envRenderTarget.texture)
|
|
467
|
-
scene.environment = null;
|
|
468
|
-
else if (scene.environment === texture)
|
|
469
|
-
scene.environment = null;
|
|
470
|
-
}
|
|
471
|
-
// dispose resources only if not cached/shared
|
|
472
|
-
if (envRenderTarget) {
|
|
473
|
-
try {
|
|
474
|
-
envRenderTarget.dispose();
|
|
475
|
-
}
|
|
476
|
-
catch (_a) { }
|
|
477
|
-
}
|
|
485
|
+
catch { }
|
|
486
|
+
// dispose pmremGenerator we created
|
|
487
|
+
if (!options.pmremGenerator && pmremGenerator) {
|
|
478
488
|
try {
|
|
479
|
-
|
|
480
|
-
}
|
|
481
|
-
catch (_b) { }
|
|
482
|
-
// dispose pmremGenerator we created
|
|
483
|
-
if (!options.pmremGenerator && pmremGenerator) {
|
|
484
|
-
try {
|
|
485
|
-
pmremGenerator.dispose();
|
|
486
|
-
}
|
|
487
|
-
catch (_c) { }
|
|
489
|
+
pmremGenerator.dispose();
|
|
488
490
|
}
|
|
491
|
+
catch { }
|
|
489
492
|
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
if (options.cache)
|
|
496
|
+
cubeCache.set(key, { handle, refCount: 1 });
|
|
497
|
+
return handle;
|
|
495
498
|
}
|
|
496
499
|
/**
|
|
497
500
|
* Load Equirectangular/Single Image (Supports HDR via RGBELoader)
|
|
@@ -500,95 +503,90 @@ function loadCubeSkybox(renderer_1, scene_1, paths_1) {
|
|
|
500
503
|
* @param url string - *.hdr, *.exr, *.jpg, *.png
|
|
501
504
|
* @param opts SkyboxOptions
|
|
502
505
|
*/
|
|
503
|
-
function loadEquirectSkybox(
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
const
|
|
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
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
//
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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) {
|
|
569
575
|
try {
|
|
570
|
-
|
|
571
|
-
}
|
|
572
|
-
catch (_a) { }
|
|
573
|
-
if (!options.pmremGenerator && pmremGenerator) {
|
|
574
|
-
try {
|
|
575
|
-
pmremGenerator.dispose();
|
|
576
|
-
}
|
|
577
|
-
catch (_b) { }
|
|
576
|
+
pmremGenerator.dispose();
|
|
578
577
|
}
|
|
578
|
+
catch { }
|
|
579
579
|
}
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
if (options.cache)
|
|
583
|
+
equirectCache.set(key, { handle, refCount: 1 });
|
|
584
|
+
return handle;
|
|
585
585
|
}
|
|
586
|
-
function loadSkybox(
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
return loadEquirectSkybox(renderer, scene, params.url, opts);
|
|
591
|
-
});
|
|
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);
|
|
592
590
|
}
|
|
593
591
|
/* -------------------------
|
|
594
592
|
Cache / Reference Counting Helper Methods
|
|
@@ -798,10 +796,9 @@ class BlueSkyManager {
|
|
|
798
796
|
* Usually called when the scene is completely destroyed or the application exits
|
|
799
797
|
*/
|
|
800
798
|
destroy() {
|
|
801
|
-
var _a;
|
|
802
799
|
this.cancelLoad();
|
|
803
800
|
this.dispose();
|
|
804
|
-
|
|
801
|
+
this.pmremGen?.dispose();
|
|
805
802
|
this.isInitialized = false;
|
|
806
803
|
this.loadingState = 'idle';
|
|
807
804
|
}
|
|
@@ -817,9 +814,11 @@ const BlueSky = new BlueSkyManager();
|
|
|
817
814
|
exports.BlueSky = BlueSky;
|
|
818
815
|
exports.disposeMaterial = disposeMaterial;
|
|
819
816
|
exports.disposeObject = disposeObject;
|
|
817
|
+
exports.getLoaderConfig = getLoaderConfig;
|
|
820
818
|
exports.loadCubeSkybox = loadCubeSkybox;
|
|
821
819
|
exports.loadEquirectSkybox = loadEquirectSkybox;
|
|
822
820
|
exports.loadModelByUrl = loadModelByUrl;
|
|
823
821
|
exports.loadSkybox = loadSkybox;
|
|
824
822
|
exports.releaseSkybox = releaseSkybox;
|
|
823
|
+
exports.setLoaderConfig = setLoaderConfig;
|
|
825
824
|
//# sourceMappingURL=index.js.map
|