@chocozhang/three-model-render 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +609 -0
  3. package/dist/camera/index.d.ts +133 -0
  4. package/dist/camera/index.js +291 -0
  5. package/dist/camera/index.js.map +1 -0
  6. package/dist/camera/index.mjs +265 -0
  7. package/dist/camera/index.mjs.map +1 -0
  8. package/dist/core/index.d.ts +102 -0
  9. package/dist/core/index.js +455 -0
  10. package/dist/core/index.js.map +1 -0
  11. package/dist/core/index.mjs +432 -0
  12. package/dist/core/index.mjs.map +1 -0
  13. package/dist/effect/index.d.ts +214 -0
  14. package/dist/effect/index.js +749 -0
  15. package/dist/effect/index.js.map +1 -0
  16. package/dist/effect/index.mjs +728 -0
  17. package/dist/effect/index.mjs.map +1 -0
  18. package/dist/index.d.ts +852 -0
  19. package/dist/index.js +3268 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/index.mjs +3223 -0
  22. package/dist/index.mjs.map +1 -0
  23. package/dist/interaction/index.d.ts +160 -0
  24. package/dist/interaction/index.js +661 -0
  25. package/dist/interaction/index.js.map +1 -0
  26. package/dist/interaction/index.mjs +637 -0
  27. package/dist/interaction/index.mjs.map +1 -0
  28. package/dist/loader/index.d.ts +175 -0
  29. package/dist/loader/index.js +786 -0
  30. package/dist/loader/index.js.map +1 -0
  31. package/dist/loader/index.mjs +758 -0
  32. package/dist/loader/index.mjs.map +1 -0
  33. package/dist/setup/index.d.ts +47 -0
  34. package/dist/setup/index.js +199 -0
  35. package/dist/setup/index.js.map +1 -0
  36. package/dist/setup/index.mjs +178 -0
  37. package/dist/setup/index.mjs.map +1 -0
  38. package/dist/ui/index.d.ts +36 -0
  39. package/dist/ui/index.js +292 -0
  40. package/dist/ui/index.js.map +1 -0
  41. package/dist/ui/index.mjs +271 -0
  42. package/dist/ui/index.mjs.map +1 -0
  43. package/package.json +98 -0
@@ -0,0 +1,786 @@
1
+ 'use strict';
2
+
3
+ var THREE = require('three');
4
+ var EXRLoader_js = require('three/examples/jsm/loaders/EXRLoader.js');
5
+
6
+ function _interopNamespaceDefault(e) {
7
+ var n = Object.create(null);
8
+ if (e) {
9
+ Object.keys(e).forEach(function (k) {
10
+ if (k !== 'default') {
11
+ var d = Object.getOwnPropertyDescriptor(e, k);
12
+ Object.defineProperty(n, k, d.get ? d : {
13
+ enumerable: true,
14
+ get: function () { return e[k]; }
15
+ });
16
+ }
17
+ });
18
+ }
19
+ n.default = e;
20
+ return Object.freeze(n);
21
+ }
22
+
23
+ var THREE__namespace = /*#__PURE__*/_interopNamespaceDefault(THREE);
24
+
25
+ /******************************************************************************
26
+ Copyright (c) Microsoft Corporation.
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;
55
+ };
56
+
57
+ const DEFAULT_OPTIONS$1 = {
58
+ useKTX2: false,
59
+ mergeGeometries: false,
60
+ maxTextureSize: null,
61
+ useSimpleMaterials: false,
62
+ skipSkinned: true,
63
+ };
64
+ /** 自动根据扩展名决定启用哪些选项(智能判断) */
65
+ function normalizeOptions(url, opts) {
66
+ const ext = (url.split('.').pop() || '').toLowerCase();
67
+ const merged = Object.assign(Object.assign({}, DEFAULT_OPTIONS$1), opts);
68
+ if (ext === 'gltf' || ext === 'glb') {
69
+ // gltf/glb 默认尝试 draco/ktx2,如果用户没填
70
+ if (merged.dracoDecoderPath === undefined)
71
+ merged.dracoDecoderPath = '/draco/';
72
+ if (merged.useKTX2 === undefined)
73
+ merged.useKTX2 = true;
74
+ if (merged.ktx2TranscoderPath === undefined)
75
+ merged.ktx2TranscoderPath = '/basis/';
76
+ }
77
+ else {
78
+ // fbx/obj/ply/stl 等不需要 draco/ktx2
79
+ merged.dracoDecoderPath = null;
80
+ merged.ktx2TranscoderPath = null;
81
+ merged.useKTX2 = false;
82
+ }
83
+ return merged;
84
+ }
85
+ function loadModelByUrl(url_1) {
86
+ return __awaiter(this, arguments, void 0, function* (url, options = {}) {
87
+ var _a, _b;
88
+ if (!url)
89
+ throw new Error('url required');
90
+ const ext = (url.split('.').pop() || '').toLowerCase();
91
+ const opts = normalizeOptions(url, options);
92
+ const manager = (_a = opts.manager) !== null && _a !== void 0 ? _a : new THREE__namespace.LoadingManager();
93
+ let loader;
94
+ if (ext === 'gltf' || ext === 'glb') {
95
+ const { GLTFLoader } = yield import('three/examples/jsm/loaders/GLTFLoader.js');
96
+ const gltfLoader = new GLTFLoader(manager);
97
+ if (opts.dracoDecoderPath) {
98
+ const { DRACOLoader } = yield import('three/examples/jsm/loaders/DRACOLoader.js');
99
+ const draco = new DRACOLoader();
100
+ draco.setDecoderPath(opts.dracoDecoderPath);
101
+ gltfLoader.setDRACOLoader(draco);
102
+ }
103
+ if (opts.useKTX2 && opts.ktx2TranscoderPath) {
104
+ const { KTX2Loader } = yield import('three/examples/jsm/loaders/KTX2Loader.js');
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);
125
+ }
126
+ else {
127
+ throw new Error(`Unsupported model extension: .${ext}`);
128
+ }
129
+ const object = yield new Promise((resolve, reject) => {
130
+ loader.load(url, (res) => {
131
+ var _a;
132
+ if (ext === 'gltf' || ext === 'glb') {
133
+ const sceneObj = res.scene || res;
134
+ // --- 关键:把 animations 暴露到 scene.userData(或 scene.animations)上 ---
135
+ // 这样调用方只要拿到 sceneObj,就能通过 sceneObj.userData.animations 读取到 clips
136
+ sceneObj.userData = (sceneObj === null || sceneObj === void 0 ? void 0 : sceneObj.userData) || {};
137
+ sceneObj.userData.animations = (_a = res.animations) !== null && _a !== void 0 ? _a : [];
138
+ resolve(sceneObj);
139
+ }
140
+ else {
141
+ resolve(res);
142
+ }
143
+ }, undefined, (err) => reject(err));
144
+ });
145
+ // 优化
146
+ object.traverse((child) => {
147
+ var _a, _b, _c;
148
+ const mesh = child;
149
+ if (mesh.isMesh && mesh.geometry && !mesh.geometry.isBufferGeometry) {
150
+ try {
151
+ mesh.geometry = (_c = (_b = (_a = new THREE__namespace.BufferGeometry()).fromGeometry) === null || _b === void 0 ? void 0 : _b.call(_a, mesh.geometry)) !== null && _c !== void 0 ? _c : mesh.geometry;
152
+ }
153
+ catch (_d) { }
154
+ }
155
+ });
156
+ if (opts.maxTextureSize && opts.maxTextureSize > 0)
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 });
172
+ }
173
+ catch (e) {
174
+ console.warn('mergeGeometries failed', e);
175
+ }
176
+ }
177
+ return object;
178
+ });
179
+ }
180
+ /** 运行时下采样网格中的贴图到 maxSize(canvas drawImage)以节省 GPU 内存 */
181
+ function downscaleTexturesInObject(obj, maxSize) {
182
+ obj.traverse((ch) => {
183
+ if (!ch.isMesh)
184
+ return;
185
+ const mesh = ch;
186
+ const mat = mesh.material;
187
+ if (!mat)
188
+ return;
189
+ const props = ['map', 'normalMap', 'roughnessMap', 'metalnessMap', 'aoMap', 'emissiveMap', 'alphaMap'];
190
+ props.forEach((p) => {
191
+ const tex = mat[p];
192
+ if (!tex || !tex.image)
193
+ return;
194
+ const image = tex.image;
195
+ if (!image.width || !image.height)
196
+ return;
197
+ const max = maxSize;
198
+ if (image.width <= max && image.height <= max)
199
+ return;
200
+ // downscale using canvas (sync, may be heavy for many textures)
201
+ try {
202
+ const scale = Math.min(max / image.width, max / image.height);
203
+ const canvas = document.createElement('canvas');
204
+ canvas.width = Math.floor(image.width * scale);
205
+ canvas.height = Math.floor(image.height * scale);
206
+ const ctx = canvas.getContext('2d');
207
+ if (ctx) {
208
+ ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
209
+ const newTex = new THREE__namespace.Texture(canvas);
210
+ newTex.needsUpdate = true;
211
+ // copy common settings (encoding etc)
212
+ newTex.encoding = tex.encoding;
213
+ mat[p] = newTex;
214
+ }
215
+ }
216
+ catch (e) {
217
+ console.warn('downscale texture failed', e);
218
+ }
219
+ });
220
+ });
221
+ }
222
+ /**
223
+ * 尝试合并 object 中的几何体(只合并:非透明、非 SkinnedMesh、attribute 集合兼容的 BufferGeometry)
224
+ * - 合并前会把每个 mesh 的几何体应用 world matrix(so merged geometry in world space)
225
+ * - 合并会按材质 UUID 分组(不同材质不能合并)
226
+ * - 合并函数会兼容 BufferGeometryUtils 的常见导出名
227
+ */
228
+ function tryMergeGeometries(root, opts) {
229
+ return __awaiter(this, void 0, void 0, function* () {
230
+ // collect meshes by material uuid
231
+ const groups = new Map();
232
+ root.traverse((ch) => {
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)
257
+ return;
258
+ // dynamic import BufferGeometryUtils and find merge function name
259
+ const bufUtilsMod = yield import('three/examples/jsm/utils/BufferGeometryUtils.js');
260
+ // use || chain (avoid mixing ?? with || without parentheses)
261
+ const mergeFn = bufUtilsMod.mergeBufferGeometries ||
262
+ bufUtilsMod.mergeGeometries ||
263
+ bufUtilsMod.mergeBufferGeometries || // defensive duplicate
264
+ bufUtilsMod.mergeGeometries;
265
+ if (!mergeFn)
266
+ throw new Error('No merge function found in BufferGeometryUtils');
267
+ // for each group, try merge
268
+ for (const [key, { material, geoms }] of groups) {
269
+ if (geoms.length <= 1) {
270
+ // nothing to merge
271
+ continue;
272
+ }
273
+ // call merge function - signature typically mergeBufferGeometries(array, useGroups)
274
+ const merged = mergeFn(geoms, false);
275
+ if (!merged) {
276
+ console.warn('merge returned null for group', key);
277
+ continue;
278
+ }
279
+ // create merged mesh at root (world-space geometry already applied)
280
+ const mergedMesh = new THREE__namespace.Mesh(merged, material);
281
+ root.add(mergedMesh);
282
+ }
283
+ // now remove original meshes flagged for removal
284
+ const toRemove = [];
285
+ root.traverse((ch) => {
286
+ var _a;
287
+ if ((_a = ch.userData) === null || _a === void 0 ? void 0 : _a.__toRemoveForMerge)
288
+ toRemove.push(ch);
289
+ });
290
+ toRemove.forEach((m) => {
291
+ if (m.parent)
292
+ m.parent.remove(m);
293
+ // free original resources (geometries already cloned/applied), but careful with shared materials
294
+ if (m.isMesh) {
295
+ const mm = m;
296
+ try {
297
+ mm.geometry.dispose();
298
+ }
299
+ catch (_a) { }
300
+ // we do NOT dispose material because it may be reused by mergedMesh
301
+ }
302
+ });
303
+ });
304
+ }
305
+ /* ---------------------
306
+ 释放工具
307
+ --------------------- */
308
+ /** 彻底释放对象:几何体,材质和其贴图(危险:共享资源会被释放) */
309
+ function disposeObject(obj) {
310
+ if (!obj)
311
+ return;
312
+ obj.traverse((ch) => {
313
+ if (ch.isMesh) {
314
+ const m = ch;
315
+ if (m.geometry) {
316
+ try {
317
+ m.geometry.dispose();
318
+ }
319
+ catch (_a) { }
320
+ }
321
+ const mat = m.material;
322
+ if (mat) {
323
+ if (Array.isArray(mat))
324
+ mat.forEach((x) => disposeMaterial(x));
325
+ else
326
+ disposeMaterial(mat);
327
+ }
328
+ }
329
+ });
330
+ }
331
+ /** 释放材质及其贴图 */
332
+ function disposeMaterial(mat) {
333
+ if (!mat)
334
+ return;
335
+ const texNames = ['map', 'alphaMap', 'aoMap', 'emissiveMap', 'envMap', 'metalnessMap', 'roughnessMap', 'normalMap', 'bumpMap', 'displacementMap', 'lightMap'];
336
+ texNames.forEach((k) => {
337
+ if (mat[k] && typeof mat[k].dispose === 'function') {
338
+ try {
339
+ mat[k].dispose();
340
+ }
341
+ catch (_a) { }
342
+ }
343
+ });
344
+ try {
345
+ if (typeof mat.dispose === 'function')
346
+ mat.dispose();
347
+ }
348
+ catch (_a) { }
349
+ }
350
+
351
+ /** 默认值 */
352
+ const DEFAULT_OPTIONS = {
353
+ setAsBackground: true,
354
+ setAsEnvironment: true,
355
+ useSRGBEncoding: true,
356
+ cache: true
357
+ };
358
+ /** 内部缓存:key -> { handle, refCount } */
359
+ const cubeCache = new Map();
360
+ const equirectCache = new Map();
361
+ /* -------------------------------------------
362
+ 公共函数:加载 skybox(自动选 cube 或 equirect)
363
+ ------------------------------------------- */
364
+ /**
365
+ * 加载立方体贴图(6张)
366
+ * @param renderer THREE.WebGLRenderer - 用于 PMREM 生成环境贴图
367
+ * @param scene THREE.Scene
368
+ * @param paths string[] 6 张图片地址,顺序:[px, nx, py, ny, pz, nz]
369
+ * @param opts SkyboxOptions
370
+ */
371
+ function loadCubeSkybox(renderer_1, scene_1, paths_1) {
372
+ return __awaiter(this, arguments, void 0, function* (renderer, scene, paths, opts = {}) {
373
+ var _a, _b;
374
+ const options = Object.assign(Object.assign({}, DEFAULT_OPTIONS), opts);
375
+ if (!Array.isArray(paths) || paths.length !== 6)
376
+ throw new Error('cube skybox requires 6 image paths');
377
+ const key = paths.join('|');
378
+ // 缓存处理
379
+ if (options.cache && cubeCache.has(key)) {
380
+ const rec = cubeCache.get(key);
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
399
+ if (options.setAsBackground)
400
+ scene.background = texture;
401
+ // environment: use PMREM to produce a proper prefiltered env map for PBR
402
+ let pmremGenerator = (_a = options.pmremGenerator) !== null && _a !== void 0 ? _a : new THREE__namespace.PMREMGenerator(renderer);
403
+ (_b = pmremGenerator.compileCubemapShader) === null || _b === void 0 ? void 0 : _b.call(pmremGenerator);
404
+ // fromCubemap might be available in your three.js; fallback to fromEquirectangular approach if not
405
+ let envRenderTarget = null;
406
+ if (pmremGenerator.fromCubemap) {
407
+ envRenderTarget = pmremGenerator.fromCubemap(texture);
408
+ }
409
+ else {
410
+ // Fallback: render cube to env map by using generator.fromEquirectangular with a converted equirect if needed.
411
+ // Simpler fallback: use the cube texture directly as environment (less correct for reflections).
412
+ envRenderTarget = null;
413
+ }
414
+ if (options.setAsEnvironment) {
415
+ if (envRenderTarget) {
416
+ scene.environment = envRenderTarget.texture;
417
+ }
418
+ else {
419
+ // fallback: use cube texture directly (works but not prefiltered)
420
+ scene.environment = texture;
421
+ }
422
+ }
423
+ const handle = {
424
+ key,
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
+ }
448
+ try {
449
+ texture.dispose();
450
+ }
451
+ catch (_b) { }
452
+ // dispose pmremGenerator we created
453
+ if (!options.pmremGenerator && pmremGenerator) {
454
+ try {
455
+ pmremGenerator.dispose();
456
+ }
457
+ catch (_c) { }
458
+ }
459
+ }
460
+ };
461
+ if (options.cache)
462
+ cubeCache.set(key, { handle, refCount: 1 });
463
+ return handle;
464
+ });
465
+ }
466
+ /**
467
+ * 加载等距/单图(支持 HDR via RGBELoader)
468
+ * @param renderer THREE.WebGLRenderer
469
+ * @param scene THREE.Scene
470
+ * @param url string - *.hdr, *.exr, *.jpg, *.png
471
+ * @param opts SkyboxOptions
472
+ */
473
+ function loadEquirectSkybox(renderer_1, scene_1, url_1) {
474
+ return __awaiter(this, arguments, void 0, function* (renderer, scene, url, opts = {}) {
475
+ var _a, _b;
476
+ const options = Object.assign(Object.assign({}, DEFAULT_OPTIONS), opts);
477
+ const key = url;
478
+ if (options.cache && equirectCache.has(key)) {
479
+ const rec = equirectCache.get(key);
480
+ rec.refCount += 1;
481
+ if (options.setAsBackground)
482
+ scene.background = rec.handle.backgroundTexture;
483
+ if (options.setAsEnvironment && rec.handle.envRenderTarget)
484
+ scene.environment = rec.handle.envRenderTarget.texture;
485
+ return rec.handle;
486
+ }
487
+ // 动态导入 RGBELoader(用于 .hdr/.exr),如果加载的是普通 jpg/png 可直接用 TextureLoader
488
+ const isHDR = /\.hdr$|\.exr$/i.test(url);
489
+ let hdrTexture;
490
+ if (isHDR) {
491
+ const { RGBELoader } = yield import('three/examples/jsm/loaders/RGBELoader.js');
492
+ hdrTexture = yield new Promise((resolve, reject) => {
493
+ new RGBELoader().load(url, (tex) => resolve(tex), undefined, (err) => reject(err));
494
+ });
495
+ // RGBE textures typically use LinearEncoding
496
+ hdrTexture.encoding = THREE__namespace.LinearEncoding;
497
+ }
498
+ else {
499
+ // ordinary image - use TextureLoader
500
+ const loader = new THREE__namespace.TextureLoader();
501
+ hdrTexture = yield new Promise((resolve, reject) => {
502
+ loader.load(url, (t) => resolve(t), undefined, (err) => reject(err));
503
+ });
504
+ if (options.useSRGBEncoding)
505
+ hdrTexture.encoding = THREE__namespace.sRGBEncoding;
506
+ }
507
+ // PMREMGenerator to convert equirectangular to prefiltered cubemap (good for PBR)
508
+ const pmremGenerator = (_a = options.pmremGenerator) !== null && _a !== void 0 ? _a : new THREE__namespace.PMREMGenerator(renderer);
509
+ (_b = pmremGenerator.compileEquirectangularShader) === null || _b === void 0 ? void 0 : _b.call(pmremGenerator);
510
+ const envRenderTarget = pmremGenerator.fromEquirectangular(hdrTexture);
511
+ // envTexture to use for scene.environment
512
+ const envTexture = envRenderTarget.texture;
513
+ // set background and/or environment
514
+ if (options.setAsBackground) {
515
+ // for background it's ok to use the equirect texture directly or the envTexture
516
+ // envTexture is cubemap-like and usually better for reflections; using it as background creates cube-projected look
517
+ scene.background = envTexture;
518
+ }
519
+ if (options.setAsEnvironment) {
520
+ scene.environment = envTexture;
521
+ }
522
+ // We can dispose the original hdrTexture (the PMREM target contains the needed data)
523
+ try {
524
+ hdrTexture.dispose();
525
+ }
526
+ catch (_c) { }
527
+ const handle = {
528
+ key,
529
+ backgroundTexture: options.setAsBackground ? envTexture : null,
530
+ envRenderTarget,
531
+ pmremGenerator: options.pmremGenerator ? null : pmremGenerator,
532
+ setAsBackground: !!options.setAsBackground,
533
+ setAsEnvironment: !!options.setAsEnvironment,
534
+ dispose() {
535
+ if (options.setAsBackground && scene.background === envTexture)
536
+ scene.background = null;
537
+ if (options.setAsEnvironment && scene.environment === envTexture)
538
+ scene.environment = null;
539
+ try {
540
+ envRenderTarget.dispose();
541
+ }
542
+ catch (_a) { }
543
+ if (!options.pmremGenerator && pmremGenerator) {
544
+ try {
545
+ pmremGenerator.dispose();
546
+ }
547
+ catch (_b) { }
548
+ }
549
+ }
550
+ };
551
+ if (options.cache)
552
+ equirectCache.set(key, { handle, refCount: 1 });
553
+ return handle;
554
+ });
555
+ }
556
+ function loadSkybox(renderer_1, scene_1, params_1) {
557
+ return __awaiter(this, arguments, void 0, function* (renderer, scene, params, opts = {}) {
558
+ if (params.type === 'cube')
559
+ return loadCubeSkybox(renderer, scene, params.paths, opts);
560
+ return loadEquirectSkybox(renderer, scene, params.url, opts);
561
+ });
562
+ }
563
+ /* -------------------------
564
+ 缓存/引用计数 辅助方法
565
+ ------------------------- */
566
+ /** 释放一个缓存的 skybox(会减少 refCount,refCount=0 时才真正 dispose) */
567
+ function releaseSkybox(handle) {
568
+ // check cube cache
569
+ if (cubeCache.has(handle.key)) {
570
+ const rec = cubeCache.get(handle.key);
571
+ rec.refCount -= 1;
572
+ if (rec.refCount <= 0) {
573
+ rec.handle.dispose();
574
+ cubeCache.delete(handle.key);
575
+ }
576
+ return;
577
+ }
578
+ if (equirectCache.has(handle.key)) {
579
+ const rec = equirectCache.get(handle.key);
580
+ rec.refCount -= 1;
581
+ if (rec.refCount <= 0) {
582
+ rec.handle.dispose();
583
+ equirectCache.delete(handle.key);
584
+ }
585
+ return;
586
+ }
587
+ // if not cached, just dispose
588
+ // handle.dispose()
589
+ }
590
+
591
+ // utils/BlueSkyManager.ts - 优化版
592
+ /**
593
+ * BlueSkyManager - 优化版
594
+ * ---------------------------------------------------------
595
+ * 一个全局单例管理器,用于加载和管理基于 HDR/EXR 的蓝天白云环境贴图。
596
+ *
597
+ * ✨ 优化内容:
598
+ * - 添加加载进度回调
599
+ * - 支持加载取消
600
+ * - 完善错误处理
601
+ * - 返回 Promise 支持异步
602
+ * - 添加加载状态管理
603
+ */
604
+ class BlueSkyManager {
605
+ constructor() {
606
+ /** 当前环境贴图的 RenderTarget,用于后续释放 */
607
+ this.skyRT = null;
608
+ /** 是否已经初始化 */
609
+ this.isInitialized = false;
610
+ /** ✨ 当前加载器,用于取消加载 */
611
+ this.currentLoader = null;
612
+ /** ✨ 加载状态 */
613
+ this.loadingState = 'idle';
614
+ }
615
+ /**
616
+ * 初始化
617
+ * ---------------------------------------------------------
618
+ * 必须在使用 BlueSkyManager 之前调用一次。
619
+ * @param renderer WebGLRenderer 实例
620
+ * @param scene Three.js 场景
621
+ * @param exposure 曝光度 (默认 1.0)
622
+ */
623
+ init(renderer, scene, exposure = 1.0) {
624
+ if (this.isInitialized) {
625
+ console.warn('BlueSkyManager: 已经初始化,跳过重复初始化');
626
+ return;
627
+ }
628
+ this.renderer = renderer;
629
+ this.scene = scene;
630
+ // 使用 ACESFilmicToneMapping,效果更接近真实
631
+ this.renderer.toneMapping = THREE__namespace.ACESFilmicToneMapping;
632
+ this.renderer.toneMappingExposure = exposure;
633
+ // 初始化 PMREM 生成器(全局只需一个)
634
+ this.pmremGen = new THREE__namespace.PMREMGenerator(renderer);
635
+ this.pmremGen.compileEquirectangularShader();
636
+ this.isInitialized = true;
637
+ }
638
+ /**
639
+ * ✨ 加载蓝天 HDR/EXR 贴图并应用到场景(Promise 版本)
640
+ * ---------------------------------------------------------
641
+ * @param exrPath HDR/EXR 文件路径
642
+ * @param options 加载选项
643
+ * @returns Promise<void>
644
+ */
645
+ loadAsync(exrPath, options = {}) {
646
+ if (!this.isInitialized) {
647
+ return Promise.reject(new Error('BlueSkyManager not initialized!'));
648
+ }
649
+ // ✨ 取消之前的加载
650
+ this.cancelLoad();
651
+ const { background = true, onProgress, onComplete, onError } = options;
652
+ this.loadingState = 'loading';
653
+ this.currentLoader = new EXRLoader_js.EXRLoader();
654
+ return new Promise((resolve, reject) => {
655
+ this.currentLoader.load(exrPath,
656
+ // 成功回调
657
+ (texture) => {
658
+ try {
659
+ // 设置贴图为球面反射映射
660
+ texture.mapping = THREE__namespace.EquirectangularReflectionMapping;
661
+ // 清理旧的环境贴图
662
+ this.dispose();
663
+ // 用 PMREM 生成高效的环境贴图
664
+ this.skyRT = this.pmremGen.fromEquirectangular(texture);
665
+ // 应用到场景:环境光照 & 背景
666
+ this.scene.environment = this.skyRT.texture;
667
+ if (background)
668
+ this.scene.background = this.skyRT.texture;
669
+ // 原始 HDR/EXR 贴图用完即销毁,节省内存
670
+ texture.dispose();
671
+ this.loadingState = 'loaded';
672
+ this.currentLoader = null;
673
+ console.log('✅ Blue sky EXR loaded:', exrPath);
674
+ if (onComplete)
675
+ onComplete();
676
+ resolve();
677
+ }
678
+ catch (error) {
679
+ this.loadingState = 'error';
680
+ this.currentLoader = null;
681
+ console.error('❌ Processing EXR sky failed:', error);
682
+ if (onError)
683
+ onError(error);
684
+ reject(error);
685
+ }
686
+ },
687
+ // 进度回调
688
+ (xhr) => {
689
+ if (onProgress && xhr.lengthComputable) {
690
+ const progress = xhr.loaded / xhr.total;
691
+ onProgress(progress);
692
+ }
693
+ },
694
+ // 错误回调
695
+ (err) => {
696
+ this.loadingState = 'error';
697
+ this.currentLoader = null;
698
+ console.error('❌ Failed to load EXR sky:', err);
699
+ if (onError)
700
+ onError(err);
701
+ reject(err);
702
+ });
703
+ });
704
+ }
705
+ /**
706
+ * 加载蓝天 HDR/EXR 贴图并应用到场景(同步 API,保持向后兼容)
707
+ * ---------------------------------------------------------
708
+ * @param exrPath HDR/EXR 文件路径
709
+ * @param background 是否应用为场景背景 (默认 true)
710
+ */
711
+ load(exrPath, background = true) {
712
+ this.loadAsync(exrPath, { background }).catch((error) => {
713
+ console.error('BlueSkyManager load error:', error);
714
+ });
715
+ }
716
+ /**
717
+ * ✨ 取消当前加载
718
+ */
719
+ cancelLoad() {
720
+ if (this.currentLoader) {
721
+ // EXRLoader 本身没有 abort 方法,但我们可以清空引用
722
+ this.currentLoader = null;
723
+ this.loadingState = 'idle';
724
+ }
725
+ }
726
+ /**
727
+ * ✨ 获取加载状态
728
+ */
729
+ getLoadingState() {
730
+ return this.loadingState;
731
+ }
732
+ /**
733
+ * ✨ 是否正在加载
734
+ */
735
+ isLoading() {
736
+ return this.loadingState === 'loading';
737
+ }
738
+ /**
739
+ * 释放当前的天空贴图资源
740
+ * ---------------------------------------------------------
741
+ * 仅清理 skyRT,不销毁 PMREM
742
+ * 适用于切换 HDR/EXR 文件时调用
743
+ */
744
+ dispose() {
745
+ if (this.skyRT) {
746
+ this.skyRT.texture.dispose();
747
+ this.skyRT.dispose();
748
+ this.skyRT = null;
749
+ }
750
+ if (this.scene && this.scene.background)
751
+ this.scene.background = null;
752
+ if (this.scene && this.scene.environment)
753
+ this.scene.environment = null;
754
+ }
755
+ /**
756
+ * 完全销毁 BlueSkyManager
757
+ * ---------------------------------------------------------
758
+ * 包括 PMREMGenerator 的销毁
759
+ * 通常在场景彻底销毁或应用退出时调用
760
+ */
761
+ destroy() {
762
+ var _a;
763
+ this.cancelLoad();
764
+ this.dispose();
765
+ (_a = this.pmremGen) === null || _a === void 0 ? void 0 : _a.dispose();
766
+ this.isInitialized = false;
767
+ this.loadingState = 'idle';
768
+ }
769
+ }
770
+ /**
771
+ * 🌐 全局单例
772
+ * ---------------------------------------------------------
773
+ * 直接导出一个全局唯一的 BlueSkyManager 实例,
774
+ * 保证整个应用中只用一个 PMREMGenerator,性能最佳。
775
+ */
776
+ const BlueSky = new BlueSkyManager();
777
+
778
+ exports.BlueSky = BlueSky;
779
+ exports.disposeMaterial = disposeMaterial;
780
+ exports.disposeObject = disposeObject;
781
+ exports.loadCubeSkybox = loadCubeSkybox;
782
+ exports.loadEquirectSkybox = loadEquirectSkybox;
783
+ exports.loadModelByUrl = loadModelByUrl;
784
+ exports.loadSkybox = loadSkybox;
785
+ exports.releaseSkybox = releaseSkybox;
786
+ //# sourceMappingURL=index.js.map