import * as THREE from "three"; import { GLTFLoader, type GLTF } from "three/addons/loaders/GLTFLoader.js"; export type TextureEntry = { key: string; url: string }; export type ModelEntry = { key: string; url: string }; export type AudioEntry = { key: string; url: string }; export type CubeTextureEntry = { key: string; urls: [string, string, string, string, string, string] }; export type AssetManifest = { textures?: TextureEntry[]; models?: ModelEntry[]; audio?: AudioEntry[]; cubeTextures?: CubeTextureEntry[]; }; export type LoadedAssets = { textures: Map; models: Map; audio: Map; cubeTextures: Map; }; type ProgressCallback = (progress: number) => void; /** * 统一资源加载器。 * * 支持 Texture / GLTF 模型 / 音频 / CubeTexture 批量加载, * 提供 0~1 的进度回调。加载结果按 key 存入 Map,方便后续使用。 * * ```typescript * const loader = new AssetLoader(); * await loader.loadAll({ * textures: [{ key: "logo", url: logoUrl }], * models: [{ key: "char", url: charUrl }], * }, (p) => console.log(`${(p*100).toFixed(0)}%`)); * * const tex = loader.getTexture("logo"); * const gltf = loader.getModel("char"); * ``` */ export class AssetLoader { private assets: LoadedAssets = { textures: new Map(), models: new Map(), audio: new Map(), cubeTextures: new Map(), }; private textureLoader = new THREE.TextureLoader(); private gltfLoader = new GLTFLoader(); private cubeTextureLoader = new THREE.CubeTextureLoader(); private audioContext: AudioContext | null = null; async loadAll(manifest: AssetManifest, onProgress?: ProgressCallback): Promise { const tasks: (() => Promise)[] = []; for (const entry of manifest.textures ?? []) { tasks.push(() => this.loadTexture(entry)); } for (const entry of manifest.models ?? []) { tasks.push(() => this.loadModel(entry)); } for (const entry of manifest.audio ?? []) { tasks.push(() => this.loadAudio(entry)); } for (const entry of manifest.cubeTextures ?? []) { tasks.push(() => this.loadCubeTexture(entry)); } if (tasks.length === 0) { onProgress?.(1); return this.assets; } let done = 0; const total = tasks.length; await Promise.all( tasks.map(async (task) => { await task(); done++; onProgress?.(done / total); }), ); return this.assets; } getTexture(key: string): THREE.Texture | undefined { return this.assets.textures.get(key); } getModel(key: string): GLTF | undefined { return this.assets.models.get(key); } getAudioBuffer(key: string): AudioBuffer | undefined { return this.assets.audio.get(key); } getCubeTexture(key: string): THREE.CubeTexture | undefined { return this.assets.cubeTextures.get(key); } private loadTexture(entry: TextureEntry): Promise { return new Promise((resolve, reject) => { this.textureLoader.load( entry.url, (tex) => { tex.colorSpace = THREE.SRGBColorSpace; this.assets.textures.set(entry.key, tex); resolve(); }, undefined, reject, ); }); } private loadModel(entry: ModelEntry): Promise { return new Promise((resolve, reject) => { this.gltfLoader.load( entry.url, (gltf) => { this.assets.models.set(entry.key, gltf); resolve(); }, undefined, reject, ); }); } private async loadAudio(entry: AudioEntry): Promise { if (!this.audioContext) { this.audioContext = new AudioContext(); } const response = await fetch(entry.url); const arrayBuffer = await response.arrayBuffer(); const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer); this.assets.audio.set(entry.key, audioBuffer); } private loadCubeTexture(entry: CubeTextureEntry): Promise { return new Promise((resolve, reject) => { this.cubeTextureLoader.load( entry.urls, (tex) => { this.assets.cubeTextures.set(entry.key, tex); resolve(); }, undefined, reject, ); }); } }