关于 threejs-loaders
面向 Three.js 开发者的资源加载参考技能,说明模型、纹理与环境文件的载入和处理,并涵盖异步操作及 WebGL、WebGPU 用法。
主要功能
- 01协调多加载器并报告进度
- 02加载 glTF、OBJ、FBX、STL、PLY 和 USD
- 03载入纹理及 HDR、EXR 环境
- 04处理压缩资源与自定义数据源
- 05介绍克隆、缓存和资源释放
适用场景
- 01载入并显示 3D 模型
- 02并行加载模型、环境和纹理
- 03为压缩 glTF 配置解码器
- 04从已有二进制数据解析模型
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ cloudai-x/ threejs-skills --skill threejs-loaders安装后在 Claude Code 中输入/threejs-loaders确认已加载。
详细介绍
Three.js 加载器
适用于 three.js r186。示例使用 WebGLRenderer;WebGPU 和 TSL 部分使用 WebGPURenderer。
快速入门
import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { RoomEnvironment } from "three/addons/environments/RoomEnvironment.js";
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.environment = new THREE.PMREMGenerator(renderer).fromScene(new RoomEnvironment(), 0.04).texture; // glTF PBR materials render black without lights or an environment
const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(2, 1.5, 3);
camera.lookAt(0, 0, 0);
const gltf = await new GLTFLoader().loadAsync("model.glb");
scene.add(gltf.scene);
function animate() {
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
LoadingManager
一个管理器可协调多个加载器并报告合并后的进度。loadAsync 仍会在失败时拒绝;manager.onError 也会触发。
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
const manager = new THREE.LoadingManager();
manager.onStart = (url, loaded, total) => {
console.log(`Started loading: ${url}`);
};
manager.onProgress = (url, loaded, total) => {
console.log(`Loading: ${((loaded / total) * 100).toFixed(1)}%`);
};
manager.onLoad = () => {
console.log("All assets loaded");
};
manager.onError = (url) => {
console.error(`Error loading: ${url}`);
};
const gltfLoader = new GLTFLoader(manager);
const textureLoader = new THREE.TextureLoader(manager);
// onLoad fires when every tracked item has finished
await Promise.all([
gltfLoader.loadAsync("model.glb"),
textureLoader.loadAsync("texture.jpg"),
textureLoader.loadAsync("normal.jpg"),
]);
manager.abort() 会取消通过 FileLoader 加载器发起的进行中请求(模型、HDR、EXR、KTX2)。通过 HTML 图片元素加载的加载器(例如 TextureLoader)会忽略它。
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
const manager = new THREE.LoadingManager();
const timeoutId = setTimeout(() => manager.abort(), 30000);
try {
const gltf = await new GLTFLoader(manager).loadAsync("model.glb");
scene.add(gltf.scene);
} catch (error) {
console.error(error);
} finally {
clearTimeout(timeoutId);
}
纹理加载
TextureLoader.load() 会立即返回纹理,并在图像解码后填充纹理;可以立即为其赋值,首次渲染时就会使用它。如果为已经渲染过且此前没有贴图的材质添加贴图,则需要设置 material.needsUpdate = true。加载图像时绝不会调用 onProgress。
new THREE.TextureLoader().load(
"texture.jpg",
(map) => {
map.colorSpace = THREE.SRGBColorSpace; // a color map; scalar maps keep NoColorSpace
mesh.material.map = map;
mesh.material.needsUpdate = true; // the material rendered without a map before
},
undefined, // onProgress: not supported for image loading
(error) => {
console.error("Could not load texture", error);
},
);
纹理配置
const map = new THREE.TextureLoader().load("color.jpg");
map.colorSpace = THREE.SRGBColorSpace; // LDR color images; scalar maps keep NoColorSpace
map.wrapS = THREE.RepeatWrapping; // ClampToEdgeWrapping (default), RepeatWrapping, MirroredRepeatWrapping
map.wrapT = THREE.RepeatWrapping;
map.repeat.set(2, 2);
map.offset.set(0.5, 0.5);
map.rotation = Math.PI / 4;
map.center.set(0.5, 0.5);
map.minFilter = THREE.LinearMipmapLinearFilter; // default
map.magFilter = THREE.LinearFilter; // default; NearestFilter for a pixelated look
map.anisotropy = renderer.capabilities.getMaxAnisotropy(); // sharper textures at grazing angles
mesh.material.map = map;
// wrap, filters, anisotropy, colorSpace and flipY (true by default) apply at upload: set them before the first render or set map.needsUpdate = true; repeat, offset, rotation and center apply every frame
色彩空间:LDR 彩色图像(反照率、自发光)使用 SRGBColorSpace;标量贴图(法线、凹凸、粗糙度、金属度、AO、透明度)保留默认的 NoColorSpace。GLTFLoader 会为其加载的纹理设置正确的色彩空间,下文的 HDR 加载器也会自行设置 LinearSRGBColorSpace。对于自行加载、用于 glTF 模型材质的纹理,需要设置 map.flipY = false,以匹配 GLTFLoader 加载的纹理。
CubeTextureLoader
用于天空盒和立方体环境贴图,六个面按 +X、-X、+Y、-Y、+Z、-Z 的顺序排列;加载器会自行设置 sRGB。
const cubeTexture = await new THREE.CubeTextureLoader().loadAsync([
"px.jpg",
"nx.jpg",
"py.jpg",
"ny.jpg",
"pz.jpg",
"nz.jpg",
]);
scene.background = cubeTexture;
scene.environment = cubeTexture;
HDR 环境
HDRLoader(Radiance .hdr)和 EXRLoader 会返回浮点等距柱状纹理。两个渲染器都会自行预过滤等距柱状环境贴图,因此文件无需经过 PMREM 步骤;设置 mapping 并赋值即可。
import { HDRLoader } from "three/addons/loaders/HDRLoader.js";
import { EXRLoader } from "three/addons/loaders/EXRLoader.js";
const env = await new HDRLoader().loadAsync("environment.hdr");
env.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = env;
scene.background = env; // without mapping, a background texture draws as a flat screen quad
const exr = await new EXRLoader().loadAsync("environment.exr");
exr.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = exr;
使用 scene.environmentIntensity 调暗环境(并使用 scene.environmentRotation 旋转环境);material.envMapIntensity 只会缩放材质自身的 envMap,而由 scene.environment 照明的模型中,envMap 仍为 null。
UltraHDRLoader 也能以相同方式读取增益图 JPEG——以 JPEG 文件大小实现 HDR 动态范围:
import { UltraHDRLoader } from "three/addons/loaders/UltraHDRLoader.js";
const env = await new UltraHDRLoader().loadAsync("environment.jpg");
env.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = env;
对于生成的环境贴图,使用 PMREMGenerator,不要用于文件:
import { RoomEnvironment } from "three/addons/environments/RoomEnvironment.js";
const pmremGenerator = new THREE.PMREMGenerator(renderer);
const room = new RoomEnvironment();
scene.environment = pmremGenerator.fromScene(room, 0.04).texture;
room.dispose(); pmremGenerator.dispose(); // the room's geometries and materials, then the generator
glTF 和 GLB
Web 交付的主要格式。返回结果包含 scene、animations、cameras、asset(版本、生成器)和 userData(作者自定义数据)。
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
const gltf = await new GLTFLoader().loadAsync("model.glb");
scene.add(gltf.scene);
if (gltf.animations.length > 0) {
const mixer = new THREE.AnimationMixer(gltf.scene);
mixer.clipAction(gltf.animations[0]).play();
const timer = new THREE.Timer();
timer.connect(document);
function animate() {
timer.update();
mixer.update(timer.getDelta());
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
}
Draco、meshopt 和 KTX2 压缩
Draco 解码器和 Basis 转码器默认从 three 包加载;只有在自行托管时才调用 setDecoderPath / setTranscoderPath,并将路径固定到你所发布的 three 版本。需要 EXT_meshopt_compression 或 KHR_meshopt_compression 的文件,如果没有设置 setMeshoptDecoder,就会加载失败。
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { DRACOLoader } from "three/addons/loaders/DRACOLoader.js";
import { KTX2Loader } from "three/addons/loaders/KTX2Loader.js";
import { MeshoptDecoder } from "three/addons/libs/meshopt_decoder.module.js";
const gltfLoader = new GLTFLoader()
.setDRACOLoader(new DRACOLoader())
.setMeshoptDecoder(MeshoptDecoder)
.setKTX2Loader(new KTX2Loader().detectSupport(renderer));
const gltf = await gltfLoader.loadAsync("compressed-model.glb");
scene.add(gltf.scene);
处理已加载的内容
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
const gltf = await new GLTFLoader().loadAsync("model.glb");
const model = gltf.scene;
model.traverse((child) => {
if (child.isMesh) {
child.castShadow = true; // shadows also need renderer.shadowMap.enabled = true and a light with castShadow
child.receiveShadow = true;
}
});
model.getObjectByName("Head"); // look up named nodes
scene.environmentIntensity = 0.5; // dim scene.environment for this scene
// Center the model and scale it into a unit box
const box = new THREE.Box3().setFromObject(model);
const center = box.getCenter(new THREE.Vector3());
const size = box.getSize(new THREE.Vector3());
const scale = 1 / Math.max(size.x, size.y, size.z);
model.scale.setScalar(scale);
model.position.copy(center).multiplyScalar(-scale); // the offset is scaled too
scene.add(model);
其他模型格式
OBJ + MTL
import { MTLLoader } from "three/addons/loaders/MTLLoader.js";
import { OBJLoader } from "three/addons/loaders/OBJLoader.js";
const materials = await new MTLLoader().loadAsync("model.mtl");
materials.preload();
const object = await new OBJLoader().setMaterials(materials).loadAsync("model.obj");
scene.add(object);
FBX
import { FBXLoader } from "three/addons/loaders/FBXLoader.js";
const object = await new FBXLoader().loadAsync("model.fbx");
object.scale.setScalar(0.01); // FBX files are often exported in centimeters
const mixer = new THREE.AnimationMixer(object); // call mixer.update(timer.getDelta()) every frame, as in the glTF example
if (object.animations.length > 0) mixer.clipAction(object.animations[0]).play();
scene.add(object);
STL
import { STLLoader } from "three/addons/loaders/STLLoader.js";
const geometry = await new STLLoader().loadAsync("model.stl");
scene.add(new THREE.Mesh(geometry, new THREE.MeshStandardMaterial({ color: 0x888888 })));
scene.add(new THREE.HemisphereLight(0xffffff, 0x444444, 1));
scene.add(new THREE.DirectionalLight(0xffffff, 2)); // lit materials render black without lights; this one shines down from (0, 1, 0)
PLY
加载器会遵循文件的数据类型,因此 double 属性会作为由 Float64Array 支持的属性载入,而 WebGLRenderer 无法上传这种属性;渲染前请将其转换为 Float32BufferAttribute。
import { PLYLoader } from "three/addons/loaders/PLYLoader.js";
const geometry = await new PLYLoader().loadAsync("model.ply");
for (const name of Object.keys(geometry.attributes)) {
const attribute = geometry.getAttribute(name);
if (attribute.array instanceof Float64Array) {
geometry.setAttribute(name, new THREE.Float32BufferAttribute(attribute.array, attribute.itemSize));
}
}
if (!geometry.hasAttribute("normal")) geometry.computeVertexNormals(); // keep the file's own normals
const material = new THREE.MeshStandardMaterial({ vertexColors: geometry.hasAttribute("color") }); // vertexColors without a color attribute renders black
scene.add(new THREE.Mesh(geometry, material));
scene.add(new THREE.HemisphereLight(0xffffff, 0x444444, 1));
scene.add(new THREE.DirectionalLight(0xffffff, 2)); // lit materials render black without lights; this one shines down from (0, 1, 0)
USD
import { USDLoader } from "three/addons/loaders/USDLoader.js";
const model = await new USDLoader().loadAsync("model.usdz"); // USD, USDA, USDC and USDZ archives
scene.add(model);
异步模式和自定义来源
并行加载互不依赖的资源:
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { HDRLoader } from "three/addons/loaders/HDRLoader.js";
const [model, environment, colorMap] = await Promise.all([
new GLTFLoader().loadAsync("model.glb"),
new HDRLoader().loadAsync("environment.hdr"),
new THREE.TextureLoader().loadAsync("color.jpg"),
]);
environment.mapping = THREE.EquirectangularReflectionMapping;
colorMap.colorSpace = THREE.SRGBColorSpace;
scene.add(model.scene);
scene.environment = environment;
解析已持有的数据(已下载的文件、文件输入、数据库记录):
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
const response = await fetch("model.glb");
const buffer = await response.arrayBuffer();
const gltf = await new GLTFLoader().parseAsync(buffer, "");
scene.add(gltf.scene);
Blob URL 和 data URL 均可用作加载器 URL:
async function loadTextureFromBlob(blob) {
const url = URL.createObjectURL(blob);
const texture = await new THREE.TextureLoader().loadAsync(url);
URL.revokeObjectURL(url);
return texture;
}
通过管理器设置路径前缀并重写 URL:
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
const loader = new GLTFLoader();
loader.setPath("assets/models/"); // prefix for the URL you pass to load
loader.setResourcePath("assets/textures/"); // where the file's own references resolve
const gltf = await loader.loadAsync("model.glb"); // loads assets/models/model.glb
const manager = new THREE.LoadingManager();
manager.setURLModifier((url) => url.replace("assets/", "https://cdn.example.com/assets/"));
const cdnGltf = await new GLTFLoader(manager).loadAsync("assets/models/model.glb"); // fetched from https://cdn.example.com/assets/models/model.glb
启用共享缓存,将重复 URL 从内存中提供:
THREE.Cache.enabled = true;
THREE.Cache.clear(); // empty it when you need to force reloads
尽量缩小载荷——几何体使用 Draco 或 meshopt,纹理使用 KTX2,并通过 threejs-asset-pipeline 生成——同时在真实模型加载时显示占位网格,加载完成后再将其替换。
克隆与释放
对蒙皮模型调用 Object3D.clone() 会共享骨架:每个副本仍绑定到原始模型的骨骼,因此会跟随原始模型,而忽略自身的动画。请使用 SkeletonUtils.clone() 创建实例。
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import * as SkeletonUtils from "three/addons/utils/SkeletonUtils.js";
const gltf = await new GLTFLoader().loadAsync("character.glb");
scene.add(gltf.scene);
const copy = SkeletonUtils.clone(gltf.scene);
scene.add(copy);
object.dispose() 只释放该对象的渲染器端状态(WebGPU 绑定、InstancedMesh 缓冲区),不会遍历子对象,也不会释放几何体、材质或纹理——请遍历整个子树:
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
const gltf = await new GLTFLoader().loadAsync("model.glb");
const model = gltf.scene;
scene.add(model);
// ... later
scene.remove(model);
model.traverse((child) => {
child.dispose(); // per-object renderer state: InstancedMesh buffers, WebGPU bindings
if (child.geometry) { // meshes, lines and points
child.geometry.dispose();
const materials = Array.isArray(child.material) ? child.material : [child.material];
for (const material of materials) {
for (const value of Object.values(material)) {
if (value?.isTexture) value.dispose();
}
material.dispose();
}
}
});
WebGPU 和 TSL
import * as THREE from "three/webgpu";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { RoomEnvironment } from "three/addons/environments/RoomEnvironment.js";
const renderer = new THREE.WebGPURenderer({ antialias: true });
await renderer.init(); // before any other use of the renderer
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x111827); // the WebGPU canvas is transparent by default
scene.environment = new THREE.PMREMGenerator(renderer).fromScene(new RoomEnvironment(), 0.04).texture;
const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(2, 1.5, 3);
camera.lookAt(0, 0, 0);
const gltf = await new GLTFLoader().loadAsync("model.glb"); // loaded materials become node materials automatically
scene.add(gltf.scene);
function animate() { renderer.render(scene, camera); } renderer.setAnimationLoop(animate);
ktx2Loader.detectSupport(renderer)和PMREMGenerator的方法(fromScene、fromEquirectangular、fromCubemap)都会在await renderer.init()之前抛出异常。最大各向异性为renderer.getMaxAnisotropy();WebGPURenderer 下不存在renderer.capabilities。- 高斯泼溅:
SPLATLoader、SPZLoader、KSPLATLoader和GaussianSplatPLYLoader会加载数据,以供从three/addons/objects/GaussianSplat.js导入的new GaussianSplat(data)使用;它需要 WebGPURenderer。
常见错误
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js"→import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js":three/examples/jsm/说明符无法通过 three.js 文档中所述的导入映射解析。import { RGBELoader } from "three/addons/loaders/RGBELoader.js"→import { HDRLoader } from "three/addons/loaders/HDRLoader.js":在 r180 中由 RGBELoader 更名而来;每次实例化旧类都会发出警告。new Promise((resolve, reject) => loader.load(url, resolve, undefined, reject))→loader.loadAsync(url):每个加载器都已实现 loadAsync,出错时会拒绝 Promise。material.map = new THREE.TextureLoader().load("color.jpg")→const map = new THREE.TextureLoader().load("color.jpg"); map.colorSpace = THREE.SRGBColorSpace; material.map = map;:加载的颜色贴图默认使用 NoColorSpace,渲染出来会泛白。normalMap.colorSpace = THREE.LinearSRGBColorSpace→normalMap.colorSpace = THREE.NoColorSpace:标量贴图(法线、凹凸、粗糙度、金属度、环境光遮蔽、透明度)应保持 NoColorSpace;LinearSRGBColorSpace 用于线性颜色数据。texture.encoding = THREE.sRGBEncoding→texture.colorSpace = THREE.SRGBColorSpace:encoding 和 sRGBEncoding 已不存在,因此旧代码会让贴图保持 NoColorSpace,渲染出来会泛白。child.material.envMapIntensity = 0.5→scene.environmentIntensity = 0.5:envMapIntensity 只会缩放 material.envMap;当模型由 scene.environment 提供光照时,该属性仍为 null。scene.environment = pmremGenerator.fromEquirectangular(hdrTexture).texture→hdrTexture.mapping = THREE.EquirectangularReflectionMapping; scene.environment = hdrTexture;:两个渲染器都会自行对等距柱状纹理执行 PMREM 处理,因此手动处理只会增加代码和一个需要释放的渲染目标。scene.background = hdrTexture→hdrTexture.mapping = THREE.EquirectangularReflectionMapping; scene.background = hdrTexture;:如果没有设置映射,背景纹理会被绘制成平面屏幕四边形。ktx2Loader.setTranscoderPath(pathFromAnotherRelease)→const ktx2Loader = new KTX2Loader().detectSupport(renderer):Basis 转码器和 Draco 解码器默认从 three 包中加载;指定其他版本的路径会与加载器不匹配。ktx2Loader.detectSupportAsync(renderer)→await renderer.init(); ktx2Loader.detectSupport(renderer);:detectSupportAsync 在 r181 中已弃用;请在渲染器初始化后调用 detectSupport。renderer.capabilities.getMaxAnisotropy()→renderer.getMaxAnisotropy():只有 WebGLRenderer 才有 capabilities;使用 WebGPURenderer 时请调用渲染器的方法。scene.add(new THREE.Mesh(await new PLYLoader().loadAsync("scan.ply"), material))→const geometry = await new PLYLoader().loadAsync("scan.ply"); for (const [name, a] of Object.entries(geometry.attributes)) if (a.array instanceof Float64Array) geometry.setAttribute(name, new THREE.Float32BufferAttribute(a.array, a.itemSize)); scene.add(new THREE.Mesh(geometry, material));:PLYLoader 会将双精度属性保留为 Float64 属性,而 WebGLRenderer 无法上传这些属性(会抛出 Unsupported buffer data format)。gltf.scene.clone()→SkeletonUtils.clone(gltf.scene):Object3D.clone() 会共享骨架,因此所有副本仍绑定到原始骨骼,并随其动画移动。gltf.scene.dispose()→gltf.scene.traverse((child) => { child.geometry?.dispose(); for (const material of [child.material ?? []].flat()) { for (const value of Object.values(material)) if (value?.isTexture) value.dispose(); material.dispose(); } child.dispose(); }):Object3D.dispose() 只会释放该对象的渲染器状态,不会释放其几何体、材质或纹理;material.dispose() 也不会释放材质的纹理。fetch(url, { signal: controller.signal })→setTimeout(() => manager.abort(), 30000):通过 LoadingManager 取消加载器,该管理器会中止正在进行的 FileLoader 请求。new USDZLoader()→new USDLoader():在 r179 中已弃用;USDLoader 可读取 USD、USDA、USDC 和 USDZ。
另请参阅
threejs-fundamentals- 渲染器设置、尺寸调整处理,以及围绕已加载资源搭建的应用程序脚手架threejs-textures和threejs-materials- 纹理属性和色彩空间,以及调整已加载模型自带的材质threejs-asset-pipeline- 在 glTF 文件交给这些加载器之前对其进行压缩和优化threejs-animation- AnimationMixer、片段和蒙皮网格播放