Three.js 学习系列(六)加载外部资产:GLTF、Draco、KTX2 与大模型工程姿势

自己写 BoxGeometry 只是入门。真实项目里的 3D 资产都是设计师在 Blender / Maya 做完导出的——GLTF 怎么用、Draco/Meshopt 怎么压、KTX2 纹理怎么搞、加载进度怎么显示、几百兆模型怎么不卡死浏览器,这一篇全部讲透。
threejsgltfdracoktx2loaderlearning-series

上一篇 让模型动了起来。 但你怎么获得那个模型?真实项目里没人自己写顶点—— 设计师 Blender / Maya / 3ds Max 做完,导出成文件,前端加载进 Three.js。 这一篇拆开 GLTF 时代的资产工作流。

一、为什么是 GLTF(不是 OBJ / FBX)

3D 模型格式有几十种,Three.js 支持的也有十多种。但Web 3D 的事实标准只有一个GLTF(.gltf / .glb),由 Khronos 组织维护——和 OpenGL / WebGL / Vulkan 同一家。

1.1 主流格式速查

格式出身优点缺点Web 推荐度
GLTF / glbKhronos标准化、PBR 原生、Draco/KTX2、动画/骨骼齐全编辑器支持新一点★★★★★
OBJWavefront(1990s)简单、文本可读、几乎所有软件支持没材质(要配 .mtl)、没骨骼、没动画★★
FBXAutodesk编辑器生态最广(Maya/3ds Max 原生)闭源二进制、大、慢、Web 不友好★★
USDZPixar / AppleiOS AR 标准(AR Quick Look)Web 工具少★★(iOS AR 用)
Collada (.dae)Khronos 旧版XML 可读老旧,被 GLTF 取代
STL3D 打印极简单只有三角形,没材质★(仅 3D 打印场景)
PLY学术点云常用同 STL★(点云)

为什么 GLTF 赢了

  • 二进制格式(.glb)紧凑,单文件就能携带几何 + 材质 + 贴图 + 骨骼 + 动画
  • PBR 原生支持(metalness / roughness 工作流),和 Three.js / Unity / Unreal 同一套语义
  • Khronos 维护标准,所有引擎遵守同一规范,跨平台无翻译损耗
  • 支持 Draco / Meshopt / KTX2 扩展——网络传输极致优化
  • "传输级"格式:和 PNG 之于图片是同等地位

没有"该用哪个"的选择题—— 设计师交付 3D 资产,永远要求 GLTF。 如果手上是 OBJ / FBX,先用 Blendergltf.report 转成 GLTF 再开发。

二、GLTFLoader 基础

2.1 最小可用版本

import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'

const loader = new GLTFLoader()
loader.load(
  '/models/duck.glb',
  (gltf) => {
    scene.add(gltf.scene)            // 模型本体
    console.log(gltf.animations)     // AnimationClip[]
    console.log(gltf.cameras)        // 模型自带的相机
    console.log(gltf.scenes)         // 多个场景(GLTF 支持多场景)
  },
  (xhr) => {
    console.log(`${(xhr.loaded / xhr.total * 100).toFixed(1)}% loaded`)
  },
  (err) => {
    console.error('Error loading model:', err)
  }
)

GLTFLoader.load() 的四个参数:URL / 成功回调 / 进度回调 / 错误回调。 gltf.scene 是一个 Group,可以直接 scene.add(gltf.scene)

2.2 GLTF 返回对象的全貌

type GLTF = {
  animations: AnimationClip[]   // 动画(配合上一篇 AnimationMixer)
  scene: Group                  // 默认场景的根节点
  scenes: Group[]               // 所有场景
  cameras: Camera[]             // 自带相机
  asset: { generator, version } // 元数据
  parser: GLTFParser            // 内部解析器(高级用法)
  userData: Record<string, any> // 自定义数据
}

2.3 Promise 写法(实战推荐)

async function load(url) {
  const loader = new GLTFLoader()
  return new Promise((resolve, reject) => {
    loader.load(url, resolve, undefined, reject)
  })
}

const duck = await load('/models/duck.glb')
scene.add(duck.scene)

写复杂应用时几乎都用 Promise 包一层—— 方便和 React Suspense、Vue Suspense、Promise.all() 组合。

三、Draco:把几何数据压到 1/10

GLTF 默认存储几何数据用 Float32Array,带宽不友好Draco 是 Google 出品的 3D 几何压缩库—— 典型压缩比 5–10 倍,对网络分发模型至关重要。

3.1 启用 Draco

import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'

const draco = new DRACOLoader()
draco.setDecoderPath('https://www.gstatic.com/draco/v1/decoders/')  // CDN
// 或本地: draco.setDecoderPath('/draco/')

const loader = new GLTFLoader()
loader.setDRACOLoader(draco)

loader.load('/models/compressed.glb', (gltf) => scene.add(gltf.scene))

CDN 路径会自动加载 draco_decoder.wasm 等运行时文件。 本地部署时建议把 draco 解码器复制到 /public/draco/,避免运行时跨域。

3.2 怎么压缩你的 GLTF

模型本身要先用工具压缩成 Draco 编码版本:

# 方法 1: gltf-pipeline (Node)
npx gltf-pipeline -i duck.glb -o duck-draco.glb -d

# 方法 2: 在线工具 gltf.report (拖入即可)
# 方法 3: Blender 导出时勾选 "Draco mesh compression"

效果对比:

原始 duck.glb        : 4.2 MB
Draco 压缩 duck.glb  : 320 KB
压缩比               : 13x
解码耗时             : <100ms (WASM)

四、Meshopt:Draco 的现代替代

Meshopt 是 Arseny Kapoulkine 开发的更现代的网格优化库—— 解码更快(10–100x 比 Draco 快)、对 GPU 更友好

import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js'

const loader = new GLTFLoader()
loader.setMeshoptDecoder(MeshoptDecoder)

loader.load('/models/optimized.glb', (gltf) => scene.add(gltf.scene))

压缩自己的模型:

npx gltf-transform optimize input.glb output.glb
# 或 npx gltfpack -i input.glb -o output.glb -cc

Draco vs Meshopt 怎么选

维度DracoMeshopt
压缩率更高较高
解码速度快 10–100x
GPU 友好度解码后还要重新组织直接 GPU 可用
推荐场景极致带宽优化大多数项目

2026 年新项目默认用 Meshopt—— 解码快、GPU 友好、和 KTX2 / GLTF Transform 工具链配套好。 Draco 仍然有用,但 Meshopt 已经成为主流选择。

五、KTX2:GPU 友好的纹理压缩

纹理是 3D 资产里最占带宽的部分——一张 4K PBR 贴图很容易就 10MB+。 传统做法是 JPG/PNG 压缩,但GPU 不能直接用 JPG,必须解码成 RGBA—— 显存占用爆炸(4K RGBA = 64MB / 张)。

KTX2(Basis Universal) 是新一代 GPU 原生纹理格式:

  • 同时是网络压缩 + GPU 压缩
  • 显存占用约 1/4
  • 自动适配设备能力(ASTC / BC7 / ETC2)
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js'

const ktx2 = new KTX2Loader()
ktx2.setTranscoderPath('https://www.gstatic.com/basis-universal/versioned/2021-04-15-ba1c3e4/')
ktx2.detectSupport(renderer)

const loader = new GLTFLoader()
loader.setKTX2Loader(ktx2)

转换工具:

# 单张图片转 KTX2
npx ktx-tools  # 或 toktx
basisu input.png -ktx2 -o output.ktx2

# 整个 GLTF 的所有纹理批量转
npx gltf-transform ktxify input.glb output.glb

KTX2 + Meshopt + Draco 三件套是 2026 年 Web 3D 优化的标配:

# 一条命令搞定(gltf-transform)
npx gltf-transform optimize input.glb output.glb --texture-compress webp --compress meshopt

六、其他常用 Loader

虽然 GLTF 是首选,但某些场景仍要其他格式:

6.1 OBJLoader + MTLLoader

import { OBJLoader } from 'three/addons/loaders/OBJLoader.js'
import { MTLLoader } from 'three/addons/loaders/MTLLoader.js'

new MTLLoader().load('/model.mtl', (materials) => {
  materials.preload()
  new OBJLoader().setMaterials(materials).load('/model.obj', (obj) => {
    scene.add(obj)
  })
})

只在历史项目或简单几何转换时用

6.2 FBXLoader

import { FBXLoader } from 'three/addons/loaders/FBXLoader.js'

new FBXLoader().load('/character.fbx', (group) => {
  scene.add(group)
  // 动画也在里面
  const mixer = new THREE.AnimationMixer(group)
  group.animations.forEach(clip => mixer.clipAction(clip).play())
})

仅在拿不到 GLTF 时用。FBX 是 Autodesk 私有格式,文件大、解析慢。

6.3 USDZLoader(iOS AR)

import { USDZLoader } from 'three/addons/loaders/USDZLoader.js'

主要用于 iOS 的 AR Quick Look——一般场景不需要。

6.4 STL / PLY

import { STLLoader } from 'three/addons/loaders/STLLoader.js'
import { PLYLoader } from 'three/addons/loaders/PLYLoader.js'

3D 打印、点云、医学影像才用。

6.5 SVGLoader(把 SVG 变 3D)

import { SVGLoader } from 'three/addons/loaders/SVGLoader.js'
import { ExtrudeGeometry } from 'three'

new SVGLoader().load('/logo.svg', (data) => {
  data.paths.forEach(path => {
    const shapes = path.toShapes(true)
    shapes.forEach(shape => {
      const geo = new ExtrudeGeometry(shape, { depth: 5, bevelEnabled: false })
      scene.add(new THREE.Mesh(geo, new THREE.MeshStandardMaterial()))
    })
  })
})

把 logo 或 icon 拉成 3D 字 的常用方案。

七、LoadingManager:统一管理多个资产

加载多个资产时,逐个监听进度太啰嗦——用 LoadingManager

const manager = new THREE.LoadingManager()

manager.onStart = (url, loaded, total) => {
  console.log(`开始: ${url}`)
}
manager.onProgress = (url, loaded, total) => {
  const percent = ((loaded / total) * 100).toFixed(1)
  document.querySelector('#progress').textContent = `${percent}%`
}
manager.onLoad = () => {
  console.log('所有资源加载完毕')
  document.querySelector('#loading').remove()
}
manager.onError = (url) => {
  console.error(`加载失败: ${url}`)
}

// 所有 loader 共用这个 manager
const gltfLoader = new GLTFLoader(manager)
const textureLoader = new THREE.TextureLoader(manager)
const audioLoader = new THREE.AudioLoader(manager)

// 并发加载
gltfLoader.load('/model.glb', ...)
textureLoader.load('/diffuse.jpg', ...)
audioLoader.load('/bgm.mp3', ...)

onProgress / onLoad自动综合所有 loader 的进度—— 实现 loading 界面就靠这个

八、纹理加载工作流

PBR 模型往往需要多张贴图。批量加载的标准姿势:

async function loadTextures(paths) {
  const loader = new THREE.TextureLoader()
  const entries = await Promise.all(
    Object.entries(paths).map(([key, url]) =>
      new Promise((resolve) => loader.load(url, (tex) => resolve([key, tex])))
    )
  )
  return Object.fromEntries(entries)
}

const tex = await loadTextures({
  map:          '/wood_color.jpg',
  normalMap:    '/wood_normal.jpg',
  roughnessMap: '/wood_roughness.jpg',
  aoMap:        '/wood_ao.jpg',
})

// ★ 颜色贴图设 sRGB, 其他不设
tex.map.colorSpace = THREE.SRGBColorSpace

const material = new THREE.MeshStandardMaterial(tex)

回顾 Part 03 的 colorSpace 坑 —— 颜色贴图必须 sRGB,法线/粗糙度/AO 不要设 colorSpace。

九、大模型工程姿势(几百 MB 级)

当模型上百 MB 时,朴素加载会让浏览器卡死。几个实战姿势:

9.1 切割成多个 chunk

const chunks = ['part1.glb', 'part2.glb', 'part3.glb', 'part4.glb']
for (const url of chunks) {
  const gltf = await load(url)
  scene.add(gltf.scene)
  await new Promise(r => requestAnimationFrame(r))  // 让浏览器喘口气
}

按部件 / 区域分包,首屏只加载用户能看到的

9.2 LOD(细节层级)

import { LOD } from 'three'

const lod = new LOD()
lod.addLevel(highDetailMesh, 0)     // < 10 单位距离
lod.addLevel(mediumDetailMesh, 10)
lod.addLevel(lowDetailMesh, 30)
scene.add(lod)

// 渲染时 Three.js 自动根据距离切换

远处自动切换到低面数版本——Part 09 会详细讲。

9.3 后台预加载

const link = document.createElement('link')
link.rel = 'preload'
link.as = 'fetch'
link.href = '/next-scene.glb'
link.crossOrigin = 'anonymous'
document.head.appendChild(link)

下一场景的资源在当前场景就开始下——切换时无感。

9.4 流式 GLTF(KHR_meshopt / extensions)

GLTF 2.0 支持 KHR_meshopt_compressionMSFT_lodEXT_meshopt_compression 等扩展—— 配合 Meshopt 工具链,可以边下边显示

十、加载错误与降级

async function loadWithFallback(primary, fallback) {
  try {
    return await load(primary)
  } catch (err) {
    console.warn(`Primary failed, trying fallback`, err)
    return await load(fallback)
  }
}

const model = await loadWithFallback(
  '/models/character-hd.glb',     // 4K 贴图 PBR 高模
  '/models/character-lo.glb'      // 简化版兜底
)

所有跨网络资产都应该有降级方案—— 弱网用户、移动端、断点续传失败都会触发。

十一、源码地标

文件角色
examples/jsm/loaders/GLTFLoader.jsGLTF 解析器,3000+ 行,是所有 loader 中最复杂的
examples/jsm/loaders/DRACOLoader.jsDraco 解码器封装
examples/jsm/loaders/KTX2Loader.jsKTX2 / Basis 加载器
src/loaders/LoadingManager.js进度统计
src/loaders/Loader.js所有 loader 基类
src/loaders/TextureLoader.js单张纹理加载

GLTFLoader 源码读起来比想象的有意思—— 它需要处理 GLTF 标准里所有的扩展、降级、动画通道映射、材质转换…… 是学 "怎么写一个工业级解析器" 的极好材料。

十二、实战:完整的加载流程

把这一篇所学拼成"真实项目里加载一个 PBR 模型"的代码:

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'

// 1. LoadingManager 统一管理
const manager = new THREE.LoadingManager()
manager.onProgress = (url, loaded, total) => {
  document.querySelector('#progress').textContent =
    `${((loaded / total) * 100).toFixed(0)}%`
}
manager.onLoad = () => document.querySelector('#loading').remove()

// 2. 准备 GLTFLoader + 三件套
const draco = new DRACOLoader(manager).setDecoderPath('/draco/')
const ktx2  = new KTX2Loader(manager)
  .setTranscoderPath('/basis/').detectSupport(renderer)

const gltf = new GLTFLoader(manager)
  .setDRACOLoader(draco)
  .setKTX2Loader(ktx2)
  .setMeshoptDecoder(MeshoptDecoder)

// 3. 加载模型
gltf.load('/scene.glb', (data) => {
  // 调整接收阴影 / 投射阴影
  data.scene.traverse(obj => {
    if (obj.isMesh) {
      obj.castShadow = true
      obj.receiveShadow = true
    }
  })

  scene.add(data.scene)

  // 4. 如果有动画
  if (data.animations.length > 0) {
    const mixer = new THREE.AnimationMixer(data.scene)
    data.animations.forEach(clip => mixer.clipAction(clip).play())
    // 别忘了在 render loop 里 mixer.update(delta)
  }
})

这套骨架几乎覆盖了 90% 的工业项目加载需求

十三、作业

1. 从 Sketchfab 下载一个免费 GLTF

SketchfabPoly HavenQuaternius 都有大量免费 PBR 模型。 下载一个,配 OrbitControls + HDR 环境,看到效果—— 这是 Three.js 学习里最有成就感的一步

2. 把它压缩一遍

npx gltf-transform optimize input.glb output.glb --texture-compress webp --compress meshopt

对比压缩前后的文件大小、加载速度、显存占用。 你会立刻理解"为什么 Web 3D 工程压缩链路这么重要"。

3. 做一个"进度条 + loading 界面"

Part 04 展厅场景 的模型换成 GLTF 加载, 配上 LoadingManager 的进度条—— 进入产品级 3D 应用的样子。

十四、下一篇预告

Part 07:着色器入门 —— ShaderMaterial + GLSL 会回答:

  • vertex shader 和 fragment shader 在 GPU 上各干什么
  • uniforms / attributes / varyings 区别
  • ShaderMaterial 怎么写、为什么大多数情况比 RawShaderMaterial
  • 怎么把自定义 shader 嵌进现有 PBR 材质(onBeforeCompile + chunk inject)
  • 实战:流光、波浪、溶解、卡通描边、屏幕扭曲

这是 Three.js 通往"高级特效"的钥匙——也是和 Babylon / PlayCanvas / R3F 拉开差距的部分。

一句话总结

Geometry 是你写的,Loader 是你装的。 行业资产经过 Blender → GLTF → Draco/Meshopt/KTX2 → Web 这套流水线, 已经是高度工业化的工程。学好 Loader 不是学几行 API, 是学这条流水线里哪一步可以省带宽、省显存、省解码时间—— 这决定一个 Web 3D 应用能不能从 "本地能跑" 变成 "上线能扛"。

延伸阅读

官方文档

资产工具链

免费资产站

  • Poly Haven —— PBR 模型 + HDR + 贴图,CC0 免费
  • Sketchfab —— 海量模型(注意筛选 CC 协议)
  • Quaternius —— 低多边形角色与场景
  • Kenney.nl —— 游戏资产合集
  • Mixamo —— 角色动作捕捉,Adobe 免费

深入压缩与优化

系列内文章