mcpField
T
threejs-asset-pipeline

threejs-asset-pipeline

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

关于 threejs-asset-pipeline

面向 Three.js 的 glTF 资源管线技能,介绍文件检查、验证、压缩与优化。内容涵盖命令行和 SDK 工作流、对应解码器配置,以及 React Three Fiber 组件生成。

主要功能

  1. 01检查并验证 glTF 文件
  2. 02优化几何体与纹理数据
  3. 03使用 Draco 或 Meshopt 压缩模型
  4. 04生成 KTX2、WebP 或 AVIF 纹理
  5. 05配置匹配的 Three.js 加载器
  6. 06用 gltfjsx 创建 React 组件

适用场景

  1. 01优化前检查模型内容
  2. 02准备更适合浏览器交付的轻量资源
  3. 03为压缩 glTF 配置加载器
  4. 04在 CI 中自动执行资源转换

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/cloudai-x/threejs-skills --skill threejs-asset-pipeline

安装后在 Claude Code 中输入/threejs-asset-pipeline确认已加载。

详细介绍

Three.js 资产处理流程

面向 three.js r186。示例使用 WebGLRenderer;WebGPU 和 TSL 部分使用 WebGPURenderer。

快速开始

在命令行中压缩,然后加载结果。运行 inspect 查看处理前后节省了多少:

npx @gltf-transform/cli inspect model.glb
npx @gltf-transform/cli optimize model.glb model.opt.glb --compress draco --texture-compress webp --texture-size 2048
npx @gltf-transform/cli inspect model.opt.glb

Draco 输出需要附加 DRACOLoader;WebP 纹理会在浏览器中解码:

import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { DRACOLoader } from "three/addons/loaders/DRACOLoader.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();
const sun = new THREE.DirectionalLight(0xffffff, 2); // glTF PBR materials render black without lights or an environment
sun.position.set(3, 5, 2);
scene.add(sun, new THREE.HemisphereLight(0xffffff, 0x444444, 1.5));

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()
  .setDRACOLoader(new DRACOLoader()) // the Draco WASM decoder loads from the three package
  .loadAsync("model.opt.glb");
scene.add(gltf.scene);

function animate() {
  renderer.render(scene, camera);
}

renderer.setAnimationLoop(animate);

检查与验证

不要猜测字节数据在哪里。inspect 会打印概览(生成器、extensionsUsed、extensionsRequired),并分别为场景、网格、材质、纹理和动画打印一张表,因此几何体占比高的文件和纹理占比高的文件一目了然。

npx @gltf-transform/cli inspect model.glb
npx @gltf-transform/cli inspect model.opt.glb --format csv
npx @gltf-transform/cli validate model.glb --ignore ACCESSOR_WEIGHTS_NON_NORMALIZED

extensionsRequired 是运行时契约:KHR_draco_mesh_compression、EXT_meshopt_compression 和 KHR_texture_basisu 分别对应下文的一项加载器配置,而 KHR_mesh_quantization、EXT_mesh_gpu_instancing、EXT_texture_webp 和 EXT_texture_avif 在运行时无需额外处理。validate 会运行官方 glTF 验证器,并报告错误、警告和提示;发现错误时会以非零状态码退出,因此可用作 CI 门禁。本工具中会写入文件的命令在完成时会记录输入和输出大小;对输出运行 inspect,即可查看节省量来自哪张表。

optimize 命令

optimize 会在一次运行中串联常见处理流程:去重、实例化、调色板化、扁平化、合并、焊接、简化、重采样、清理和稀疏数据处理,然后压缩纹理,最后压缩几何体。不带标志时会应用 meshopt 压缩,并以纹理原格式重新压缩。默认设置较为激进但通用——先运行 inspect,再进行调整:

# draco geometry + webp textures, capped at 2048px
npx @gltf-transform/cli optimize model.glb model.opt.glb --compress draco --texture-compress webp --texture-size 2048
# GPU textures instead of download size, keep every triangle
npx @gltf-transform/cli optimize model.glb model.opt.glb --texture-compress ktx2 --simplify false

主要标志(布尔值必须显式赋值,例如 --simplify false):

  • --compress draco|meshopt|quantize — 几何体和动画压缩(默认 meshopt)。
  • --texture-compress ktx2|webp|avif — 纹理编码格式(默认 auto,以纹理原格式重新压缩)。
  • --texture-size <px> — 限制纹理尺寸,默认 2048。
  • --simplify <bool>、--simplify-ratio <0-1>、--simplify-error <fraction>、--simplify-lock-border <bool> — 网格简化;默认比例为 0,误差为 0.0001。
  • --instance <bool>、--instance-min <count> — 对重复网格进行 GPU 实例化。
  • --join <bool>、--join-meshes <bool>、--join-named <bool>、--flatten <bool> — 减少绘制调用;--join 需要 --flatten。
  • --weld <bool> — 合并重复顶点;即使设置 --weld false,draco 也会自行执行焊接,而 simplify 只会焊接未索引图元,因此简化时应保留 --weld true。
  • --prune <bool>、--resample <bool>、--palette <bool>、--sparse <bool>、--meshopt-level medium|high。

使用 --texture-compress ktx2 时,optimize 会自行选择位流:法线、遮挡和金属粗糙度插槽使用 UASTC,其他所有插槽使用 ETC1S。对于本指南范围之外的替代方案,gltfpack 涵盖了类似的处理;此处使用 gltf-transform 即可。

几何压缩

三种编解码器各有取舍——按模型选择,而非按项目选择:

  • Draco(--compress draco、draco 命令):仅压缩三角形网格几何体——不会压缩动画、变形目标、纹理、点和线。运行时:DRACOLoader。
  • Meshopt(--compress meshopt、meshopt 命令):压缩点、线和三角形几何体、变形目标及动画轨道,且解码速度很快;在线路传输时搭配无损 gzip 或 brotli。运行时:MeshoptDecoder。
  • Quantize(quantize 命令):根据 KHR_mesh_quantization,将浮点属性存储为 8 位或 16 位整数——减小磁盘和 GPU 内存占用,且无需接入解码器;GLTFLoader 原生读取量化属性。
npx @gltf-transform/cli draco model.glb model.opt.glb --method edgebreaker --quantize-position 14
npx @gltf-transform/cli meshopt model.glb model.opt.glb --level high
npx @gltf-transform/cli quantize model.glb model.opt.glb
npx @gltf-transform/cli gzip model.opt.glb

gzip 会在输入文件旁生成 model.opt.glb.gz;使用 Content-Encoding: gzip 提供该文件,浏览器便会自动解压。它对 Draco 或 JPEG 纹理的额外压缩效果有限,而与 meshopt 和 quantize 搭配时收益最大。

将 draco、meshopt 或 optimize 设为命令链中的最后一条命令:其他所有会重写模型的命令在读取文件时都会解码 Draco 和 meshopt 几何体(并警告 "Decoded KHR_draco_mesh_compression. Further compression will be lossy."),随后以未压缩形式写回。

纹理压缩

这是两个不同的问题:KTX2(Basis Universal)只要 GPU 支持其某种转码目标,就会在 GPU 内存中保持压缩状态(否则 KTX2Loader 会回退到未压缩的 RGBA),并能快速上传——它优化的是 GPU 内存占用和性能;而 WebP 和 AVIF 会缩小下载体积,但解码后会以原始像素存储在 GPU 内存中,占用量可能是压缩前的 4-8 倍。

# KTX2: ETC1S for color (smaller, lossier), UASTC for normal maps (larger, cleaner)
npx @gltf-transform/cli etc1s model.glb model.opt.glb --quality 128
npx @gltf-transform/cli uastc model.glb model.opt.glb --level 4
# Download size: WebP or AVIF, quality 1-100
npx @gltf-transform/cli webp model.glb model.opt.glb --quality 80
# Cap dimensions of PNG or JPEG textures; never enlarges them
npx @gltf-transform/cli resize model.glb model.opt.glb --width 1024 --height 1024

KTX2 输出——etc1s、uastc 或 optimize --texture-compress ktx2——会调用 KTX-Software 中的 ktx 命令,因此请先安装它(版本为 4.4.0 或更高);WebP 和 AVIF 则在进程内编码。单独使用 etc1s 和 uastc 命令时,所有纹理都会转换为同一种位流;optimize --texture-compress ktx2 会按纹理槽拆分(法线、遮挡和金属度-粗糙度使用 UASTC,其余使用 ETC1S),如需自定义拆分,可用 --slots 按名称选择纹理角色。运行时:KTX2 需要搭配 detectSupport 使用 KTX2Loader;WebP 和 AVIF 由浏览器解码。

场景优化命令

当 inspect 显示问题在于绘制调用或顶点数量,而不是字节数时,运行以下命令:

npx @gltf-transform/cli weld model.glb model.opt.glb
npx @gltf-transform/cli simplify model.glb model.opt.glb --ratio 0.5 --error 0.001
npx @gltf-transform/cli join model.glb model.opt.glb
npx @gltf-transform/cli instance model.glb model.opt.glb --min 5
npx @gltf-transform/cli dedup model.glb model.opt.glb
npx @gltf-transform/cli prune model.glb model.opt.glb
  • weld 合并按位完全相同的顶点,并为图元编制索引(跳过点)。draco 会自行执行 weld;simplify 仅对未编制索引的图元执行 weld,meshopt 则不会执行;此外,meshopt 的重排步骤会跳过未编制索引的图元(如果没有任何图元编制了索引,它会发出警告),因此请在 simplify 和 meshopt 之前运行 weld。
  • simplify 移除三角形,在满足 --error(网格范围的比例)限制的前提下,尽量达到 --ratio(保留顶点的比例)目标;--lock-border true 可保留开放边缘。这是有损操作。
  • join 合并兼容的图元以减少绘制调用;它会先自行运行 dedup 和 flatten。在大量重复使用同一网格的场景中,这会增加顶点数量——此时请改用 instance。
  • instance 为多个节点引用的网格写入 EXT_mesh_gpu_instancing(--min 默认为 5);GLTFLoader 会将这些批次渲染为 InstancedMesh,无需运行时设置。它会跳过包含动画的文件,且实例化网格必须共用材质。
  • dedup 合并导出器遗留的冗余访问器、材质、网格、蒙皮和纹理。
  • prune 删除任何 Scene 都不引用的属性,包括目标不属于任何场景对象的动画。

各种输出的加载器设置

流水线选择决定加载器的连接方式。Draco 解码器和 Basis 转码器默认从 three 软件包加载——只有在自行托管这些文件时,才调用 setDecoderPath 或 setTranscoderPath,并将路径固定为你随版本发布的 three 版本。为应用创建一个 KTX2Loader,并在 GLTFLoader 之间共享:每个活动实例都会加载自己的转码器和工作线程,three.js 会在多个实例同时活动时发出警告。

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";

// Wire every codec the pipeline may emit; a missing one throws at load time
const gltfLoader = new GLTFLoader()
  .setDRACOLoader(new DRACOLoader()) // KHR_draco_mesh_compression
  .setMeshoptDecoder(MeshoptDecoder) // EXT_meshopt_compression
  .setKTX2Loader(new KTX2Loader().detectSupport(renderer)); // KHR_texture_basisu

const gltf = await gltfLoader.loadAsync("model.opt.glb");
scene.add(gltf.scene);

各缺失组件会引发的错误:Draco 文件没有 DRACOLoader 时("No DRACOLoader instance provided")、meshopt 文件没有解码器时("setMeshoptDecoder must be called before loading compressed files")、KTX2 文件没有 KTX2Loader 时("setKTX2Loader must be called before loading KTX2 textures"),以及 KTX2Loader 从未运行 detectSupport 时("Missing initialization with .detectSupport( renderer )")。WebP 和 AVIF 纹理、量化属性以及 EXT_mesh_gpu_instancing 都不需要这些设置。

脚本化流水线

当 CLI 不再适用时——例如需要按项目设置默认值、用于 CI 或处理大量文件——可以使用 SDK 脚本化实现相同的转换。使用 node pipeline.mjs 运行 Node 脚本:

// deps: @gltf-transform/core @gltf-transform/extensions @gltf-transform/functions draco3dgltf
import { NodeIO } from "@gltf-transform/core";
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
import { dedup, prune, weld, draco } from "@gltf-transform/functions";
import draco3d from "draco3dgltf";

const io = new NodeIO()
  .registerExtensions(ALL_EXTENSIONS) // an extension left out is dropped on read, or throws if required
  .registerDependencies({
    "draco3d.decoder": await draco3d.createDecoderModule(), // reads Draco input
    "draco3d.encoder": await draco3d.createEncoderModule(),
  });

const document = await io.read("model.glb");
await document.transform(
  dedup(),
  weld(), // shared vertices, before compression
  prune(), // drop what no Scene references
  draco({ method: "edgebreaker", quantizePosition: 14 }),
);
await io.write("model.opt.glb", document);

压缩会在 I/O 类写入文件时执行,因此应始终放在最后一步。注册所有扩展:I/O 只会写入已注册的扩展,因此没有注册对应扩展的 WebP 纹理或量化属性会导致文件无效。若改用 meshopt,则在 await MeshoptEncoder.ready 之后,将 "meshopt.encoder": MeshoptEncoder 添加到 registerDependencies(MeshoptEncoder 来自 meshoptimizer 软件包),并传入 meshopt({ encoder: MeshoptEncoder });脚本中的纹理压缩使用 textureCompress({ encoder, targetFormat: "webp" }),其中 sharp 作为编码器。

使用 gltfjsx 输出 React 组件

gltfjsx 会将模型转换为 React Three Fiber 组件——可编辑的声明式 JSX,而不是加载后无法编辑的黑盒:

npx gltfjsx model.glb --transform

此命令会写入 Model.jsx(用于 @react-three/fiber 的组件;使用 --types 可生成 .tsx),并且由于指定了 --transform,还会重新处理资源:model-transformed.glb 会应用调色板材质、合并网格、焊接并精简几何体、进行 Draco 压缩,并将纹理重新编码为 WebP,最大尺寸为 --resolution(默认为 1024)——法线贴图会重新编码为 JPEG,最大尺寸为 2048 px 或 --resolution,取其中较大者——生成的组件会加载该文件。添加 --simplify,并搭配 --ratio 和 --error,即可进行简化。组件部分(Canvas、Suspense、useGLTF)属于 threejs-react-three-fiber skill。

WebGPU 和 TSL

上述加载器和附加组件在 WebGPURenderer 下也完全相同;WebGPURenderer 会自行将已加载的材质渲染为节点材质(mesh.material 仍是 GLTFLoader 创建的类)。有一条顺序要求:KTX2Loader.detectSupport(renderer) 需要渲染器的后端,因此应在 await renderer.init() 之后创建加载器。

import * as THREE from "three/webgpu";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { KTX2Loader } from "three/addons/loaders/KTX2Loader.js";
// renderer is an initialized WebGPURenderer (its init() was awaited)
const gltfLoader = new GLTFLoader()
  .setKTX2Loader(new KTX2Loader().detectSupport(renderer));
const gltf = await gltfLoader.loadAsync("model.opt.glb");
scene.add(gltf.scene);

常见错误

  • import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js" → import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js":three.js 文档中的导入映射无法解析 three/examples/jsm 说明符。
  • npx @gltf-transform/cli optimize model.glb out.glb --compress ktx2 → npx @gltf-transform/cli optimize model.glb out.glb --compress draco --texture-compress ktx2:纹理编解码器应使用 --texture-compress;--compress 接受 draco、meshopt 或 quantize。
  • npx @gltf-transform/cli draco model.glb a.glb && npx @gltf-transform/cli webp a.glb out.glb --quality 80 → npx @gltf-transform/cli webp model.glb a.glb --quality 80 && npx @gltf-transform/cli draco a.glb out.glb:除 draco、meshopt 和 optimize 外,每个命令读取文件时都会解码 Draco 和 meshopt 几何数据,并以未压缩形式写出,因此几何压缩应最后执行。
  • new GLTFLoader().loadAsync("draco.glb") → new GLTFLoader().setDRACOLoader(new DRACOLoader()).loadAsync("draco.glb"):加载 KHR_draco_mesh_compression 文件时若未提供 DRACOLoader,会抛出 "No DRACOLoader instance provided"。
  • new GLTFLoader().loadAsync("meshopt.glb") → new GLTFLoader().setMeshoptDecoder(MeshoptDecoder).loadAsync("meshopt.glb"):EXT_meshopt_compression 文件会抛出 "setMeshoptDecoder must be called before loading compressed files"。
  • new GLTFLoader().loadAsync("ktx2.glb") → new GLTFLoader().setKTX2Loader(new KTX2Loader().detectSupport(renderer)).loadAsync("ktx2.glb"):KHR_texture_basisu 文件会抛出 "setKTX2Loader must be called before loading KTX2 textures"。
  • setKTX2Loader(new KTX2Loader()) → setKTX2Loader(new KTX2Loader().detectSupport(renderer)):未调用 detectSupport 的 KTX2Loader 会抛出 "Missing initialization with .detectSupport( renderer )"。
  • dracoLoader.setDecoderPath(cdnPathFromAnotherRelease) → new GLTFLoader().setDRACOLoader(new DRACOLoader()):默认情况下,Draco 解码器从 three 包加载;固定到其他版本的路径会与加载器不匹配。
  • dracoLoader.setDecoderConfig({ type: "js" }) → new DRACOLoader():setDecoderConfig 在 r185 中已弃用;不设置时,只要存在 WebAssembly,DRACOLoader 就会使用 WASM 解码器。
  • ktx2Loader.detectSupportAsync(renderer) → await renderer.init(); ktx2Loader.detectSupport(renderer);:detectSupportAsync 在 r181 中已弃用;渲染器初始化后调用 detectSupport。
  • npx @gltf-transform/cli dequantize model.glb out.glb → npx @gltf-transform/cli quantize model.glb out.glb:GLTFLoader 无需运行时设置即可读取 KHR_mesh_quantization 属性,因此仅当自己的代码需要模型单位下的浮点顶点数据时才进行反量化:量化后的属性是归一化整数,量化后的位置会根据其节点的变换缩放回原值。

另请参阅

  • threejs-loaders - 运行时部分:加载进度、缓存、蒙皮模型克隆,以及释放流水线生成的内容。
  • threejs-react-three-fiber - 围绕 gltfjsx 组件构建的 React 应用:Canvas、useGLTF、Suspense 和 drei 辅助工具。
  • threejs-performance - 衡量流水线带来的收益:绘制调用、GPU 内存和 renderer.info。

评论

支持 Markdown