@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.mjs
CHANGED
|
@@ -1,37 +1,22 @@
|
|
|
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
|
|
|
36
21
|
/**
|
|
37
22
|
* @file modelLoader.ts
|
|
@@ -49,19 +34,22 @@ const DEFAULT_OPTIONS$1 = {
|
|
|
49
34
|
maxTextureSize: null,
|
|
50
35
|
useSimpleMaterials: false,
|
|
51
36
|
skipSkinned: true,
|
|
37
|
+
useCache: true,
|
|
52
38
|
};
|
|
39
|
+
const modelCache = new Map();
|
|
53
40
|
/** Automatically determine which options to enable based on extension (smart judgment) */
|
|
54
41
|
function normalizeOptions(url, opts) {
|
|
55
42
|
const ext = (url.split('.').pop() || '').toLowerCase();
|
|
56
|
-
const merged =
|
|
43
|
+
const merged = { ...DEFAULT_OPTIONS$1, ...opts };
|
|
57
44
|
if (ext === 'gltf' || ext === 'glb') {
|
|
45
|
+
const globalConfig = getLoaderConfig();
|
|
58
46
|
// gltf/glb defaults to trying draco/ktx2 if user didn't specify
|
|
59
47
|
if (merged.dracoDecoderPath === undefined)
|
|
60
|
-
merged.dracoDecoderPath =
|
|
48
|
+
merged.dracoDecoderPath = globalConfig.dracoDecoderPath;
|
|
61
49
|
if (merged.useKTX2 === undefined)
|
|
62
50
|
merged.useKTX2 = true;
|
|
63
51
|
if (merged.ktx2TranscoderPath === undefined)
|
|
64
|
-
merged.ktx2TranscoderPath =
|
|
52
|
+
merged.ktx2TranscoderPath = globalConfig.ktx2TranscoderPath;
|
|
65
53
|
}
|
|
66
54
|
else {
|
|
67
55
|
// fbx/obj/ply/stl etc. do not need draco/ktx2
|
|
@@ -71,103 +59,108 @@ function normalizeOptions(url, opts) {
|
|
|
71
59
|
}
|
|
72
60
|
return merged;
|
|
73
61
|
}
|
|
74
|
-
function loadModelByUrl(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const ktx2Loader = new KTX2Loader().setTranscoderPath(opts.ktx2TranscoderPath);
|
|
95
|
-
gltfLoader.__ktx2Loader = ktx2Loader;
|
|
96
|
-
}
|
|
97
|
-
loader = gltfLoader;
|
|
98
|
-
}
|
|
99
|
-
else if (ext === 'fbx') {
|
|
100
|
-
const { FBXLoader } = yield import('three/examples/jsm/loaders/FBXLoader.js');
|
|
101
|
-
loader = new FBXLoader(manager);
|
|
102
|
-
}
|
|
103
|
-
else if (ext === 'obj') {
|
|
104
|
-
const { OBJLoader } = yield import('three/examples/jsm/loaders/OBJLoader.js');
|
|
105
|
-
loader = new OBJLoader(manager);
|
|
106
|
-
}
|
|
107
|
-
else if (ext === 'ply') {
|
|
108
|
-
const { PLYLoader } = yield import('three/examples/jsm/loaders/PLYLoader.js');
|
|
109
|
-
loader = new PLYLoader(manager);
|
|
110
|
-
}
|
|
111
|
-
else if (ext === 'stl') {
|
|
112
|
-
const { STLLoader } = yield import('three/examples/jsm/loaders/STLLoader.js');
|
|
113
|
-
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);
|
|
114
82
|
}
|
|
115
|
-
|
|
116
|
-
|
|
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;
|
|
117
87
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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);
|
|
143
118
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
downscaleTexturesInObject(object, opts.maxTextureSize);
|
|
147
|
-
if (opts.useSimpleMaterials) {
|
|
148
|
-
object.traverse((child) => {
|
|
149
|
-
const m = child.material;
|
|
150
|
-
if (!m)
|
|
151
|
-
return;
|
|
152
|
-
if (Array.isArray(m))
|
|
153
|
-
child.material = m.map((mat) => toSimpleMaterial(mat));
|
|
154
|
-
else
|
|
155
|
-
child.material = toSimpleMaterial(m);
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
if (opts.mergeGeometries) {
|
|
159
|
-
try {
|
|
160
|
-
yield tryMergeGeometries(object, { skipSkinned: (_b = opts.skipSkinned) !== null && _b !== void 0 ? _b : true });
|
|
119
|
+
else {
|
|
120
|
+
resolve(res);
|
|
161
121
|
}
|
|
162
|
-
|
|
163
|
-
|
|
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;
|
|
164
130
|
}
|
|
131
|
+
catch { }
|
|
165
132
|
}
|
|
166
|
-
return object;
|
|
167
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;
|
|
168
160
|
}
|
|
169
|
-
/** Runtime downscale textures in mesh to maxSize (canvas
|
|
170
|
-
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 = [];
|
|
171
164
|
obj.traverse((ch) => {
|
|
172
165
|
if (!ch.isMesh)
|
|
173
166
|
return;
|
|
@@ -186,27 +179,44 @@ function downscaleTexturesInObject(obj, maxSize) {
|
|
|
186
179
|
const max = maxSize;
|
|
187
180
|
if (image.width <= max && image.height <= max)
|
|
188
181
|
return;
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
+
}
|
|
203
212
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
213
|
+
catch (e) {
|
|
214
|
+
console.warn('downscale texture failed', e);
|
|
215
|
+
}
|
|
216
|
+
})());
|
|
208
217
|
});
|
|
209
218
|
});
|
|
219
|
+
await Promise.all(tasks);
|
|
210
220
|
}
|
|
211
221
|
/**
|
|
212
222
|
* Try to merge geometries in object (Only merge: non-transparent, non-SkinnedMesh, attribute compatible BufferGeometry)
|
|
@@ -214,81 +224,77 @@ function downscaleTexturesInObject(obj, maxSize) {
|
|
|
214
224
|
* - Merging will group by material UUID (different materials cannot be merged)
|
|
215
225
|
* - Merge function is compatible with common export names of BufferGeometryUtils
|
|
216
226
|
*/
|
|
217
|
-
function tryMergeGeometries(root, opts) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
var _a;
|
|
223
|
-
if (!ch.isMesh)
|
|
224
|
-
return;
|
|
225
|
-
const mesh = ch;
|
|
226
|
-
if (opts.skipSkinned && mesh.isSkinnedMesh)
|
|
227
|
-
return;
|
|
228
|
-
const mat = mesh.material;
|
|
229
|
-
// don't merge transparent or morph-enabled or skinned meshes
|
|
230
|
-
if (!mesh.geometry || mesh.visible === false)
|
|
231
|
-
return;
|
|
232
|
-
if (mat && mat.transparent)
|
|
233
|
-
return;
|
|
234
|
-
const geom = mesh.geometry.clone();
|
|
235
|
-
mesh.updateWorldMatrix(true, false);
|
|
236
|
-
geom.applyMatrix4(mesh.matrixWorld);
|
|
237
|
-
// ensure attributes compatible? we'll rely on merge function to return null if incompatible
|
|
238
|
-
const key = (mat && mat.uuid) || 'default';
|
|
239
|
-
const bucket = (_a = groups.get(key)) !== null && _a !== void 0 ? _a : { material: mat !== null && mat !== void 0 ? mat : new THREE.MeshStandardMaterial(), geoms: [] };
|
|
240
|
-
bucket.geoms.push(geom);
|
|
241
|
-
groups.set(key, bucket);
|
|
242
|
-
// mark for removal (we'll remove meshes after)
|
|
243
|
-
mesh.userData.__toRemoveForMerge = true;
|
|
244
|
-
});
|
|
245
|
-
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)
|
|
246
232
|
return;
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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;
|
|
271
269
|
}
|
|
272
|
-
//
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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();
|
|
290
294
|
}
|
|
291
|
-
|
|
295
|
+
catch { }
|
|
296
|
+
// we do NOT dispose material because it may be reused by mergedMesh
|
|
297
|
+
}
|
|
292
298
|
});
|
|
293
299
|
}
|
|
294
300
|
/* ---------------------
|
|
@@ -305,7 +311,7 @@ function disposeObject(obj) {
|
|
|
305
311
|
try {
|
|
306
312
|
m.geometry.dispose();
|
|
307
313
|
}
|
|
308
|
-
catch
|
|
314
|
+
catch { }
|
|
309
315
|
}
|
|
310
316
|
const mat = m.material;
|
|
311
317
|
if (mat) {
|
|
@@ -327,14 +333,14 @@ function disposeMaterial(mat) {
|
|
|
327
333
|
try {
|
|
328
334
|
mat[k].dispose();
|
|
329
335
|
}
|
|
330
|
-
catch
|
|
336
|
+
catch { }
|
|
331
337
|
}
|
|
332
338
|
});
|
|
333
339
|
try {
|
|
334
340
|
if (typeof mat.dispose === 'function')
|
|
335
341
|
mat.dispose();
|
|
336
342
|
}
|
|
337
|
-
catch
|
|
343
|
+
catch { }
|
|
338
344
|
}
|
|
339
345
|
// Helper to convert to simple material (stub)
|
|
340
346
|
function toSimpleMaterial(mat) {
|
|
@@ -377,100 +383,97 @@ const equirectCache = new Map();
|
|
|
377
383
|
* @param paths string[] 6 image paths, order: [px, nx, py, ny, pz, nz]
|
|
378
384
|
* @param opts SkyboxOptions
|
|
379
385
|
*/
|
|
380
|
-
function loadCubeSkybox(
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
rec.refCount += 1;
|
|
391
|
-
// reapply to scene (in case it was removed)
|
|
392
|
-
if (options.setAsBackground)
|
|
393
|
-
scene.background = rec.handle.backgroundTexture;
|
|
394
|
-
if (options.setAsEnvironment && rec.handle.envRenderTarget)
|
|
395
|
-
scene.environment = rec.handle.envRenderTarget.texture;
|
|
396
|
-
return rec.handle;
|
|
397
|
-
}
|
|
398
|
-
// Load cube texture
|
|
399
|
-
const loader = new THREE.CubeTextureLoader();
|
|
400
|
-
const texture = yield new Promise((resolve, reject) => {
|
|
401
|
-
loader.load(paths, (tex) => resolve(tex), undefined, (err) => reject(err));
|
|
402
|
-
});
|
|
403
|
-
// Set encoding and mapping
|
|
404
|
-
if (options.useSRGBEncoding)
|
|
405
|
-
texture.encoding = THREE.sRGBEncoding;
|
|
406
|
-
texture.mapping = THREE.CubeReflectionMapping;
|
|
407
|
-
// 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)
|
|
408
396
|
if (options.setAsBackground)
|
|
409
|
-
scene.background =
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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;
|
|
417
430
|
}
|
|
418
431
|
else {
|
|
419
|
-
//
|
|
420
|
-
|
|
421
|
-
envRenderTarget = null;
|
|
432
|
+
// fallback: use cube texture directly (works but not prefiltered)
|
|
433
|
+
scene.environment = texture;
|
|
422
434
|
}
|
|
423
|
-
|
|
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
|
|
424
455
|
if (envRenderTarget) {
|
|
425
|
-
|
|
456
|
+
try {
|
|
457
|
+
envRenderTarget.dispose();
|
|
458
|
+
}
|
|
459
|
+
catch { }
|
|
426
460
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
scene.environment = texture;
|
|
461
|
+
try {
|
|
462
|
+
texture.dispose();
|
|
430
463
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
backgroundTexture: options.setAsBackground ? texture : null,
|
|
435
|
-
envRenderTarget: envRenderTarget,
|
|
436
|
-
pmremGenerator: options.pmremGenerator ? null : pmremGenerator, // only dispose if we created it
|
|
437
|
-
setAsBackground: !!options.setAsBackground,
|
|
438
|
-
setAsEnvironment: !!options.setAsEnvironment,
|
|
439
|
-
dispose() {
|
|
440
|
-
// remove from scene
|
|
441
|
-
if (options.setAsBackground && scene.background === texture)
|
|
442
|
-
scene.background = null;
|
|
443
|
-
if (options.setAsEnvironment && scene.environment) {
|
|
444
|
-
// only clear if it's the same texture we set
|
|
445
|
-
if (envRenderTarget && scene.environment === envRenderTarget.texture)
|
|
446
|
-
scene.environment = null;
|
|
447
|
-
else if (scene.environment === texture)
|
|
448
|
-
scene.environment = null;
|
|
449
|
-
}
|
|
450
|
-
// dispose resources only if not cached/shared
|
|
451
|
-
if (envRenderTarget) {
|
|
452
|
-
try {
|
|
453
|
-
envRenderTarget.dispose();
|
|
454
|
-
}
|
|
455
|
-
catch (_a) { }
|
|
456
|
-
}
|
|
464
|
+
catch { }
|
|
465
|
+
// dispose pmremGenerator we created
|
|
466
|
+
if (!options.pmremGenerator && pmremGenerator) {
|
|
457
467
|
try {
|
|
458
|
-
|
|
459
|
-
}
|
|
460
|
-
catch (_b) { }
|
|
461
|
-
// dispose pmremGenerator we created
|
|
462
|
-
if (!options.pmremGenerator && pmremGenerator) {
|
|
463
|
-
try {
|
|
464
|
-
pmremGenerator.dispose();
|
|
465
|
-
}
|
|
466
|
-
catch (_c) { }
|
|
468
|
+
pmremGenerator.dispose();
|
|
467
469
|
}
|
|
470
|
+
catch { }
|
|
468
471
|
}
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
if (options.cache)
|
|
475
|
+
cubeCache.set(key, { handle, refCount: 1 });
|
|
476
|
+
return handle;
|
|
474
477
|
}
|
|
475
478
|
/**
|
|
476
479
|
* Load Equirectangular/Single Image (Supports HDR via RGBELoader)
|
|
@@ -479,95 +482,90 @@ function loadCubeSkybox(renderer_1, scene_1, paths_1) {
|
|
|
479
482
|
* @param url string - *.hdr, *.exr, *.jpg, *.png
|
|
480
483
|
* @param opts SkyboxOptions
|
|
481
484
|
*/
|
|
482
|
-
function loadEquirectSkybox(
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
const
|
|
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
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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) {
|
|
548
554
|
try {
|
|
549
|
-
|
|
550
|
-
}
|
|
551
|
-
catch (_a) { }
|
|
552
|
-
if (!options.pmremGenerator && pmremGenerator) {
|
|
553
|
-
try {
|
|
554
|
-
pmremGenerator.dispose();
|
|
555
|
-
}
|
|
556
|
-
catch (_b) { }
|
|
555
|
+
pmremGenerator.dispose();
|
|
557
556
|
}
|
|
557
|
+
catch { }
|
|
558
558
|
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
if (options.cache)
|
|
562
|
+
equirectCache.set(key, { handle, refCount: 1 });
|
|
563
|
+
return handle;
|
|
564
564
|
}
|
|
565
|
-
function loadSkybox(
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
return loadEquirectSkybox(renderer, scene, params.url, opts);
|
|
570
|
-
});
|
|
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);
|
|
571
569
|
}
|
|
572
570
|
/* -------------------------
|
|
573
571
|
Cache / Reference Counting Helper Methods
|
|
@@ -777,10 +775,9 @@ class BlueSkyManager {
|
|
|
777
775
|
* Usually called when the scene is completely destroyed or the application exits
|
|
778
776
|
*/
|
|
779
777
|
destroy() {
|
|
780
|
-
var _a;
|
|
781
778
|
this.cancelLoad();
|
|
782
779
|
this.dispose();
|
|
783
|
-
|
|
780
|
+
this.pmremGen?.dispose();
|
|
784
781
|
this.isInitialized = false;
|
|
785
782
|
this.loadingState = 'idle';
|
|
786
783
|
}
|
|
@@ -793,5 +790,5 @@ class BlueSkyManager {
|
|
|
793
790
|
*/
|
|
794
791
|
const BlueSky = new BlueSkyManager();
|
|
795
792
|
|
|
796
|
-
export { BlueSky, disposeMaterial, disposeObject, loadCubeSkybox, loadEquirectSkybox, loadModelByUrl, loadSkybox, releaseSkybox };
|
|
793
|
+
export { BlueSky, disposeMaterial, disposeObject, getLoaderConfig, loadCubeSkybox, loadEquirectSkybox, loadModelByUrl, loadSkybox, releaseSkybox, setLoaderConfig };
|
|
797
794
|
//# sourceMappingURL=index.mjs.map
|