在 Three.js 里做模型动画,最容易混淆的是两套方案:一套是直接用 GSAP 改模型某个部件的旋转或位移,另一套是播放模型文件里已经带好的动画轨道。两者都能动,但适合的场景完全不同。简单说:
- 交互驱动的局部动作,优先考虑 GSAP
- 设计师已经烘焙好的完整动画,优先保留
AnimationMixer
如果你把这两件事混成一个问题,后面很容易在命名、状态同步和动画回放控制上越写越乱。
模型设计师与程序员的协作
在 3D 项目开发中,模型设计师和程序员需要紧密合作。设计师有时会将动画直接嵌入到模型中,而有时他们仅提供动画的基本框架,让程序员来控制动画的细节。这种做法通常取决于项目需求和开发进度。
-
设计师嵌入动画:设计师可以在模型制作时就将动画直接加入(例如,车门开关、人物动作等)。这些动画会被打包进模型文件中,程序员只需通过 Three.js 的 AnimationMixer 来控制动画的播放。
-
程序员控制动画:如果设计师没有在模型中预设动画,或者需要程序员根据用户交互来控制动画,程序员可以通过 GSAP 或 Three.js 的动画系统,直接控制模型的各个部件(如门、车轮等)的旋转、平移等动画效果。
无论哪种方式,程序员都可以利用 GSAP 来进一步优化和控制动画的播放,增强用户体验。
什么时候直接用 GSAP 控制部件
有时我们不需要播放整个模型的动画序列,而是希望单独控制模型中的某个部件(例如车门)的动画。在这种情况下,我们可以直接通过 GSAP 来控制目标元件的属性变化,通常是旋转、平移等。
示例:控制车门开关
假设我们有一个包含多个车门动画的模型,但我们只希望控制其中一个车门的开关。通过 GSAP,我们可以控制车门的旋转或平移,以实现开门和关门的动画效果。
import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import gsap from 'gsap';
// 初始化场景、相机和渲染器
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 2, 5);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
// 添加环境光
const light = new THREE.AmbientLight(0xffffff, 1);
scene.add(light);
// 加载 GLTF 模型
const loader = new GLTFLoader();
let car; // 存储整个车模型
let doorLF; // 左前门模型
let isDoorLFOpen = false; // 车门开关状态
loader.load(
"path/to/your/car_model.glb", // 替换为你的模型路径
(gltf) => {
car = gltf.scene;
scene.add(car);
// 假设左前门的模型名字为 "Door_LF"(根据你的模型结构)
doorLF = car.getObjectByName("Door_LF");
// 确保门处于关闭状态
doorLF.rotation.y = 0;
// 设置默认动画状态
doorLF.visible = true;
},
undefined,
(error) => {
console.error("加载模型失败", error);
}
);
// 渲染循环
function animate() {
requestAnimationFrame(animate);
controls.update(); // 更新控制器
renderer.render(scene, camera);
}
animate();
// 按钮事件控制
document.getElementById("toggleLF").addEventListener("click", () => {
if (!doorLF) return; // 如果门尚未加载,跳过
if (isDoorLFOpen) {
// 使用 GSAP 反向动画打开车门
gsap.to(doorLF.rotation, {
y: 0, // 关门,y轴旋转回原始位置
duration: 1,
ease: "power2.out"
});
} else {
// 使用 GSAP 动画打开车门
gsap.to(doorLF.rotation, {
y: Math.PI / 2, // 打开门,y轴旋转 90 度
duration: 1,
ease: "power2.out"
});
}
// 切换车门状态
isDoorLFOpen = !isDoorLFOpen;
});
关键点
-
获取目标元件:
使用car.getObjectByName("Door_LF")获取车门的 3D 对象。你需要根据你的模型实际结构修改Door_LF为你想控制的部件名称。 -
GSAP 动画控制:
gsap.to(doorLF.rotation, { y: Math.PI / 2, duration: 1, ease: "power2.out" }):控制车门沿 Y 轴旋转 90 度,以模拟车门打开。gsap.to(doorLF.rotation, { y: 0, duration: 1, ease: "power2.out" }):将车门旋转回原始位置,模拟车门关闭。
-
控制状态:
使用布尔变量isDoorLFOpen来跟踪车门的开关状态,每次点击按钮时反转该状态,并触发动画。
GSAP 的优势是:你可以把动画和交互状态绑得很紧,比如点击按钮、拖动进度条、根据滚动位置开门或关门。这一点和普通 CSS 动画、Framer Motion 的心智很接近,区别只是这里操作的是 3D 对象。
如果你正在处理前端动画的层次和性能边界,也可以对照看 重排与重绘:CSS 动画为什么会卡。
什么时候应该保留模型自带动画
有时我们需要控制整个模型的动画序列,比如车门的开闭、车轮的转动等,通常这些动画已经预设在模型文件中。我们可以使用 AnimationMixer 来播放这些动画,但如果你想通过 GSAP 来控制这些动画的播放状态、速率或进度,也完全可以实现。
示例:播放模型内置动画
假设模型中已经包含了车门开关的动画序列,我们希望通过 GSAP 来控制整个动画的播放、暂停或进度。
import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
// 初始化场景和渲染器
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 2, 5);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 加载 GLTF 模型
const loader = new GLTFLoader();
let mixer; // 动画混合器
loader.load(
"path/to/your/car_model.glb", // 替换为你的模型路径
(gltf) => {
const model = gltf.scene;
scene.add(model);
// 创建 AnimationMixer
mixer = new THREE.AnimationMixer(model);
// 获取完整动画片段
const fullAnimationClip = gltf.animations[0]; // 假设第一个动画是车门开闭
const action = mixer.clipAction(fullAnimationClip);
// 控制动画的播放
action.play(); // 播放动画
// 控制动画的暂停
action.paused = true; // 暂停动画
// 修改动画的播放速度
action.timeScale = 2; // 动画加速
},
undefined,
(error) => {
console.error("加载模型失败", error);
}
);
// 渲染循环
function animate() {
requestAnimationFrame(animate);
if (mixer) mixer.update(0.01); // 更新动画混合器
renderer.render(scene, camera);
}
animate();
关键点
-
AnimationMixer控制动画:
使用 AnimationMixer 来管理和控制模型的动画。通过 clipAction 获取动画,并使用 action.play() 开始播放,或者 action.paused = true 来暂停动画。 -
GSAP 控制动画进度和速率:
- 通过
gsap.to()可以控制整个动画的进度,比如改变动画播放速率timeScale或者设置动画的播放时间。
- 通过
-
灵活控制动画:
使用 GSAP 可以更加精细地控制动画效果,包括速度、延迟、反向播放等,增强用户体验。
AnimationMixer 更适合“已经存在完整时间轴”的情况,比如走路、开门、镜头运动、机械臂流程等。它的优势不是写起来更短,而是你能沿用 DCC 工具里做好的动画数据,而不是在前端重建一套逻辑。
两种方案怎么选
| 场景 | 更适合 |
|---|---|
| 点击按钮让某个部件开合 | GSAP |
| 根据滚动位置连续映射动画进度 | GSAP 或混合方案 |
| 播放建模师已经做好的角色动作 | AnimationMixer |
| 一个模型里有完整 clip,需要暂停、恢复、倍速 | AnimationMixer |
不要一开始就把所有动画都硬写成 GSAP。设计师已经给了完整 glTF clip 时,重复实现只会增加维护成本。
小结
- GSAP 适合交互驱动的局部动作。
AnimationMixer适合模型自带的完整动画轨道。- 真正的工程重点不是“哪个 API 更酷”,而是你是否保留了正确的动画数据边界。
如果你下一步是在处理 React 里的组件层级、浮层或动画边界,可以继续看 React Portal 和 Framer Motion 指南。
继续阅读
深入理解 DOM 和 CSSOM:网页渲染的核心
DOM 是浏览器用于表示网页内容和结构的对象模型。它将网页的 HTML 或 XML 文档表示为一个树形结构,每个部分(如元素、文本和属性)都成为一个节点。通过 JavaScript,开发者可以操作这些节点,从而动态地修改页面的内容和结构。CSSOM 是浏览器用来表示 CSS 样式
12 分钟Next.js Image 指南:sizes、fill 和远程图片
基于当前 Next.js Image 官方文档,解释 width/height、fill、sizes、preload、placeholder、remotePatterns 的作用,以及哪些旧教程里的 layout、objectFit 写法已经不该继续照搬。
9 分钟for 循环里 var 和 let 为什么结果不同
用经典的 `for` 循环闭包例子解释为什么 `var` 会得到 `3 3 3`,而 `let` 会得到 `0 1 2`,并说明函数作用域、块级作用域和 IIFE 在这里分别扮演什么角色。
订阅 FreeMac
每周精选:免费 Mac 软件评测、可信来源更新、替代方案和少折腾指南。