mcpField
T
threejs-loaders

threejs-loaders

github.com/cloudai-x开发3.8k13.5k 次安装1 周前更新
代码仓库

关于 threejs-loaders

面向 Three.js 开发者的资源加载参考技能,说明模型、纹理与环境文件的载入和处理,并涵盖异步操作及 WebGL、WebGPU 用法。

主要功能

  1. 01协调多加载器并报告进度
  2. 02加载 glTF、OBJ、FBX、STL、PLY 和 USD
  3. 03载入纹理及 HDR、EXR 环境
  4. 04处理压缩资源与自定义数据源
  5. 05介绍克隆、缓存和资源释放

适用场景

  1. 01载入并显示 3D 模型
  2. 02并行加载模型、环境和纹理
  3. 03为压缩 glTF 配置解码器
  4. 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、片段和蒙皮网格播放

评论

支持 Markdown