在 Three.js 中加载 gltfpack 模型

优化后的 GLB 有时不能只靠 GLTFLoader 加载。本文说明本站 Meshopt 与 KTX2 输出所需的加载配置,不讨论压缩参数选择。请先查看结果面板,确认文件使用了哪些扩展。

安装匹配的依赖

示例用于支持 ES 模块打包的 npm 项目,例如已有的 Vite 应用。下面的版本与本站查看器一致,不能把带裸模块导入的 JavaScript 文件直接在浏览器中打开。

npm install [email protected] [email protected]

将已安装的 Three.js 包中的 Basis 转码文件复制到公共目录。如果 public 目录映射到网站根路径,这些文件会通过 /basis/ 提供。转码文件应与 KTX2Loader 来自同一 Three.js 版本。

node -e "require('fs').cpSync('node_modules/three/examples/jsm/libs/basis', 'public/basis', {recursive:true})"

加载前配置解码器

把优化后的文件放到 public/models/optimized.glb,并创建 id 为 model 的 canvas。下面的代码可作为应用入口模块,按模型边界设置相机,存在动画时播放第一段动画。

<canvas id="model"></canvas>
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
import { MeshoptDecoder } from 'meshoptimizer';

const renderer = new THREE.WebGLRenderer({
  canvas: document.querySelector('#model'), antialias: true,
});
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xeeeeee);
scene.add(new THREE.HemisphereLight(0xffffff, 0x444444, 3));
const light = new THREE.DirectionalLight(0xffffff, 3);
light.position.set(3, 5, 4);
scene.add(light);
const camera = new THREE.PerspectiveCamera(45, 1, 0.01, 1000);

const ktx2 = new KTX2Loader()
  .setTranscoderPath('/basis/')
  .detectSupport(renderer);
const loader = new GLTFLoader()
  .setMeshoptDecoder(MeshoptDecoder)
  .setKTX2Loader(ktx2);

function resize() {
  renderer.setSize(window.innerWidth, window.innerHeight);
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
}
window.addEventListener('resize', resize);
resize();

try {
  await MeshoptDecoder.ready;
  const gltf = await loader.loadAsync('/models/optimized.glb');
  scene.add(gltf.scene);
  const bounds = new THREE.Box3().setFromObject(gltf.scene);
  const center = bounds.getCenter(new THREE.Vector3());
  const radius = Math.max(bounds.getBoundingSphere(new THREE.Sphere()).radius, 0.01);
  const verticalFov = THREE.MathUtils.degToRad(camera.fov);
  const horizontalFov = 2 * Math.atan(Math.tan(verticalFov / 2) * camera.aspect);
  const distance = 1.2 * radius / Math.sin(Math.min(verticalFov, horizontalFov) / 2);
  camera.position.copy(center).add(new THREE.Vector3(1, 0.6, 1).normalize().multiplyScalar(distance));
  camera.near = radius / 100;
  camera.far = distance + radius * 10;
  camera.lookAt(center);
  camera.updateProjectionMatrix();

  const mixer = new THREE.AnimationMixer(gltf.scene);
  if (gltf.animations.length) mixer.clipAction(gltf.animations[0]).play();
  const clock = new THREE.Clock();
  renderer.setAnimationLoop(() => {
    mixer.update(Math.min(clock.getDelta(), 0.1));
    renderer.render(scene, camera);
  });
} catch (error) {
  console.error('Model loading failed:', error);
  const message = document.createElement('p');
  message.textContent = 'Model loading failed. Check the browser console and network requests.';
  document.body.prepend(message);
}

根据扩展和失败请求排查

现象检查项
Meshopt 解码器报错在 loadAsync 前调用 setMeshoptDecoder,并确保打包时包含 meshoptimizer。
KTX2 纹理无法加载将 KTX2Loader 设置给 GLTFLoader,用 renderer 调用 detectSupport,并检查转码器地址。
模型或 WASM 请求返回 HTML文件缺失时,请求可能回退到了应用首页。检查 HTTP 状态、响应内容和部署路径。

如果网站使用严格的 Content Security Policy,检查控制台是否拦截了 Web Worker 或 WebAssembly。应针对应用配置 worker 和 WASM 的提供方式,不要为了消除报错直接移除策略。请求成功也不表示解码一定成功。

示例同时配置两种解码器,便于加载不同输出。保留 PNG 或 JPEG 的文件不需要 KTX2Loader;WebP 需要浏览器和加载器支持,但不使用 Basis 转码器。应用反复更换模型时,还应释放旧几何、材质和纹理,不再使用 KTX2 加载器时也应释放。

Three.js GLTFLoader · Three.js KTX2Loader

查看其他输出兼容性要求 →

引擎参考: gltfpack

打开优化工具 ↗