【three.js】动画系统完全指南 - 从事件循环到工业级动画架构
目录
1. 浏览器渲染机制底层原理
1.1 JavaScript事件循环与渲染帧
宏任务/微任务对动画的影响
1.2 帧率控制科学
Delta Time标准化计算:
帧率波动补偿策略:
2. Three.js动画体系深度解析
2.1 原生动画循环优化
性能黑洞检测:
渲染节流技术:
2.2 补间动画工业级解决方案
2.2.1 Tween.js源码级剖析
2.2.2 高级路径动画
2.3 动画系统架构设计
状态机管理:
动画轨道系统:
动画混合与层叠
性能优化:
本篇文章适合基本掌握three.js基础知识的开发者,所以本文不过于重复之前的内容,但是也会适当回顾关键概念
1. 浏览器渲染机制底层原理
1.1 JavaScript事件循环与渲染帧
宏任务/微任务对动画的影响
setTimeout vs requestAnimationFrame(rAF):
setTimeout是基于事件循环的宏任务调度,无法保证精确的动画时间间隔。
requestAnimationFrame是专门为动画设计的API,与浏览器的渲染管线同步,能够提供更加平滑的动画效果。
浏览器渲染管线
解析 → 样式计算 → 布局 → 绘制 → 合成
以上步骤是浏览器渲染页面的关键流程
垂直同步(VSync)
rAF
如何与显示器刷新率同步(60Hz/120Hz):rAF
通常会尝试与显示器的刷新率同步,以减少画面撕裂和卡顿。
1.2 帧率控制科学
Delta Time标准化计算:
const clock = new THREE.Clock();
let delta = 0;
const targetFPS = 60;
function animate() {
delta = clock.getDelta(); // 获取精确到毫秒的时间差
// 基于实际耗时计算运动增量
cube.rotation.y += (Math.PI / 2) * delta; // 每秒旋转90度
requestAnimationFrame(animate);
}
帧率波动补偿策略:
当delta > 0.1
时启用插值算法来平滑动画
丢帧处理:使用累积时间算法避免动画跳跃
let accumulator = 0;
const fixedStep = 1 / 60; // 固定步长
function animate() {
accumulator += clock.getDelta();
while (accumulator >= fixedStep) {
updatePhysics(fixedStep); // 物理模拟需固定步长
accumulator -= fixedStep;
}
render(accumulator / fixedStep); // 插值渲染
}
2. Three.js动画体系深度解析
2.1 原生动画循环优化
性能黑洞检测:
在动画循环中创建对象(如new THREE.Vector3()
)
频繁修改几何体顶点数据(应使用geometry.verticesNeedUpdate = true
标记)
渲染节流技术:
通过setTimeout
实现非活动状态降频渲染
let isActive = true;
function conditionalAnimate() {
if (isActive) {
animate();
setTimeout(conditionalAnimate, 1000 / 30); // 降为30FPS
}
}
2.2 补间动画工业级解决方案
2.2.1 Tween.js源码级剖析
时间函数算法实现:
自定义三次贝塞尔缓动函数
function cubicBezier(t, p1x, p1y, p2x, p2y) {
// 详细实现贝塞尔公式...
}
对象池优化:
复用Tween实例避免垃圾回收(GC)压力
const tweenPool = [];
function createTween(target) {
return tweenPool.length > 0
? tweenPool.pop().reset(target)
: new TWEEN.Tween(target);
}
2.2.2 高级路径动画
贝塞尔曲线运动:
const curve = new THREE.CubicBezierCurve3(
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(2, 3, 1),
new THREE.Vector3(-1, 2, 0),
new THREE.Vector3(5, 0, 3)
);
new TWEEN.Tween({ t: 0 })
.to({ t: 1 }, 3000)
.onUpdate(({ t }) => {
const point = curve.getPoint(t);
object.position.copy(point);
})
.start();
跟随路径旋转:
使用curve.getTangent(t)
计算朝向
2.3 动画系统架构设计
状态机管理:
定义IDLE
, PLAYING
, PAUSED
等状态,用于管理动画的生命周期
动画轨道系统:
通过THREE.AnimationClip
实现多轨道融合
const clip = new THREE.AnimationClip('dance', 10, [
new THREE.VectorKeyframeTrack(
'.position',
[0, 3, 6],
[0,0,0, 2,1,0, 2,1,3]
),
new THREE.QuaternionKeyframeTrack(
'.quaternion',
[0, 5],
[0,0,0,1, 0.707,0,0.707,0] // 绕Y轴旋转180度
)
]);
const mixer = new THREE.AnimationMixer(model);
const action = mixer.clipAction(clip);
action.play();
动画混合与层叠
使用 THREE.AnimationMixer
和多个 THREE.AnimationClip
实现复杂的动画混合效果。
通过调整 AnimationMixer
的权重来控制不同动画的混合比例。
案例:
性能优化:
避免不必要的渲染和计算,使用 requestAnimationFrame
进行动画循环。
案例:
// 初始化场景、相机和渲染器
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 加载一个带有多个动画的GLTF模型
const loader = new THREE.GLTFLoader();
loader.load('path/to/your/model.glb', function (gltf) {
const model = gltf.scene;
scene.add(model);
// 创建AnimationMixer
const mixer = new THREE.AnimationMixer(model);
// 获取动画剪辑
const clipWalk = gltf.animations[0];
const clipRun = gltf.animations[1];
// 创建动画动作
const actionWalk = mixer.clipAction(clipWalk);
const actionRun = mixer.clipAction(clipRun);
// 播放动画并设置权重
actionWalk.play();
actionRun.play();
// 初始化权重
let walkWeight = 1.0;
let runWeight = 0.0;
// 动画循环
function animate() {
requestAnimationFrame(animate);
// 更新权重(这里只是示例,你可以根据需求动态调整权重)
walkWeight = Math.cos(Date.now() * 0.001) * 0.5 + 0.5;
runWeight = 1.0 - walkWeight;
// 设置权重
actionWalk.setWeight(walkWeight);
actionRun.setWeight(runWeight);
// 更新mixer
mixer.update(0.01);
// 渲染场景
renderer.render(scene, camera);
}
animate();
}, undefined, function (error) {
console.error(error);
});
// 设置相机位置
camera.position.z = 5;
尽量减少对DOM的操作,使用CSS动画或WebGL进行高效的渲染。
利用GPU加速,减少CPU的负担。
案例:
// 创建一个几何体和材质
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
// 创建一个InstancedMesh
const count = 1000;
const instancedMesh = new THREE.InstancedMesh(geometry, material, count);
// 设置每个实例的变换矩阵
for (let i = 0; i < count; i++) {
const matrix = new THREE.Matrix4().makeTranslation(
Math.random() * 10 - 5,
Math.random() * 10 - 5,
Math.random() * 10 - 5
);
instancedMesh.setMatrixAt(i, matrix);
}
// 更新InstancedMesh的变换矩阵
instancedMesh.instanceMatrix.needsUpdate = true;
// 将InstancedMesh添加到场景中
scene.add(instancedMesh);
// 动画循环
function animate() {
requestAnimationFrame(animate);
// 更新每个实例的变换(这里只是简单的旋转示例)
for (let i = 0; i < count; i++) {
const matrix = instancedMesh.getMatrixAt(i);
matrix.premultiply(new THREE.Matrix4().makeRotationY(0.01));
instancedMesh.setMatrixAt(i, matrix);
}
instancedMesh.instanceMatrix.needsUpdate = true;
// 渲染场景
renderer.render(scene, camera);
}
animate();
码字不易,各位大佬点点赞呗