Three.js 与 glTF Web管线 是做什么的?
本技能涵盖将 Blender 或 vcad 模型转换为小巧 .glb 文件的步骤。该文件必须加载快速且在 Three.js 中易于动画化。
1. Blender 与 vcad 的导出核对清单
节点命名
- 为每个可动部件赋予清晰且稳定的名称:
Head,Visor,Eye_L,Eye_R,Torso_Door,Wheel_FL,Wheel_FR,Steering_Wheel。 - Three.js 随后通过
model.getObjectByName("Torso_Door")找到该部件。
轴心
- 将根部原点置于模型底座中心。在 Three.js 中,模型将因此坐在地面的
Y = 0处。 - 将每个关节部件的轴心放在其旋转轴上。例如:门铰链、车轮中心、颈部底座、天线底座。
变换
- 导出前应用所有变换。
- 缩放必须为
1, 1, 1。静止旋转必须为0, 0, 0。 - 此规则可防止程序化动画中出现偏移和剪切。
几何体预算
- 合并共享同一材质的静态网格。此步骤可减少绘制调用。
- 仅当脚本需要单独动画化某个网格时才保持其分离。对于柔体变形,请使用带有骨骼的
SkinnedMesh。 - 主要资产目标为数千个三角形。背景道具目标低于 1000 个三角形。
2. 使用 @gltf-transform/cli 进行命令行优化
项目使用 Bun。通过 bunx 运行该工具。npx 在安装了 Node.js 的机器上同样有效。
# 显示网格、纹理和动画
bunx @gltf-transform/cli inspect model.glb
# 针对 Web 进行全面优化:Meshopt + WebP + 清理
bunx @gltf-transform/cli optimize model.glb model-optimized.glb \
--compress meshopt \
--texture-compress webp \
--texture-size 2048
# 备选:Draco 几何压缩
bunx @gltf-transform/cli draco model.glb model-draco.glb
Meshopt 或 Draco
- Meshopt (
EXT_meshopt_compression) 解码极快。它保留顶点顺序,同时压缩动画关键帧和形态目标。适用于动画资产。 - Draco (
KHR_draco_mesh_compression) 对密集静态网格提供最小文件体积。例如:汽车车身和扫描模型。解码器在启动时消耗更多时间。
其他有用的步骤
# 移除重复网格和未使用数据
bunx @gltf-transform/cli dedup model.glb model.glb
# 将纹理转换为 KTX2 (BasisU)
bunx @gltf-transform/cli etc1s model.glb model-ktx2.glb
每一步后检查文件。确认文件大小、三角形数量和纹理尺寸。
3. Three.js 集成
支持 Draco 和 Meshopt 的健壮加载器
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js';
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.7/');
const gltfLoader = new GLTFLoader();
gltfLoader.setDRACOLoader(dracoLoader);
gltfLoader.setMeshoptDecoder(MeshoptDecoder);
const gltf = await gltfLoader.loadAsync('/models/bender-optimized.glb');
const root = gltf.scene;
scene.add(root);
两种动画方法
- 使用节点变换的程序化动画。 此方法适用于机器人、车辆和其他刚性部件。例如:车轮旋转、开合门、头部跟踪、天线摇摆。
const head = root.getObjectByName('Head');
const door = root.getObjectByName('Torso_Door');
const antenna = root.getObjectByName('Antenna');
function animate(time) {
if (antenna) {
antenna.rotation.z = Math.sin(time * 0.005) * 0.15;
}
if (head) {
head.rotation.y = THREE.MathUtils.lerp(head.rotation.y, targetYaw, 0.1);
}
}
- 使用
THREE.AnimationMixer的预烘焙片段。 此方法适用于来自 Blender 的复杂循环:行走、舞蹈和面部变形。
const mixer = new THREE.AnimationMixer(root);
const idleClip = THREE.AnimationClip.findByName(gltf.animations, 'Idle');
if (idleClip) {
mixer.clipAction(idleClip).play();
}
// 在渲染循环中:
// mixer.update(clock.getDelta());
金属部件的 PBR 光照
当周围没有任何物体时,金属材质(metalness > 0.8)在 Three.js 中会显示为黑色。设置一个环境贴图:
import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
const pmremGenerator = new THREE.PMREMGenerator(renderer);
scene.environment = pmremGenerator.fromScene(new RoomEnvironment(), 0.04).texture;
使用 scene.environmentIntensity 调整反射强度。对于金属资产,保持色调映射为 THREE.ACESFilmicToneMapping。
性能规则
- 每个网格组使用一种材质。每种材质对应一次绘制调用。
- 重用纹理。将小纹理打包到一个图集中。
- 设置
renderer.outputColorSpace = THREE.SRGBColorSpace。glTF 加载器会设置纹理色彩空间。 - 当同一资产出现多次时,使用
THREE.InstancedMesh。 - 移除模型时释放其资源:遍历场景,然后调用
geometry.dispose()和material.dispose()。