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