Three.js 简介

动画与渲染循环

用 requestAnimationFrame 搭建渲染循环,用 Clock 消除帧率差异,让 3D 物体平滑地动起来。

🎯 引言

前面几篇的 renderer.render() 只调用了一次,拍的是静态照片。3D 场景的魅力在于“动”:旋转的产品、漂浮的粒子、巡场的镜头。学完这篇文章,你能用渲染循环让物体连续动起来,并用 Clock 解决不同设备上动画快慢不一致的问题。


🧱 渲染循环:连拍模式

renderer.render() 是拍一张照片。要动起来,就得让相机每秒连拍几十张,每张之间物体动一点点,连续播放就是动画。

浏览器提供的“连拍开关”是 requestAnimationFrame(简称 rAF):你告诉它“下一帧画什么”,浏览器会在每次屏幕刷新前调用你的函数,大约每秒 60 次。

const animate = () => {
    requestAnimationFrame(animate); // 先预约下一帧,形成循环

    cube.rotation.y += 0.01; // 每帧转一点点

    renderer.render(scene, camera); // 拍当前这一帧
};

animate(); // 启动循环

理解成翻页动画书就很好懂:每页(帧)上的人物姿势差一点,快速翻页(每秒 60 帧)人物就动起来了。requestAnimationFrame(animate) 放在函数开头,意思是“画完这页立刻预约下一页”。


🛠 让立方体转起来

把第 1 篇的静态场景改造成动画,只需要加上渲染循环:

views/SpinCube.vue
<template>
    <div ref="containerRef" class="container"></div>
</template>

<script setup>
import { onMounted, ref } from 'vue';
import * as THREE from 'three';

const containerRef = ref(null);

onMounted(() => {
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(75, 320 / 240, 0.1, 1000);
    camera.position.z = 5;

    const cube = new THREE.Mesh(
        new THREE.BoxGeometry(1.5, 1.5, 1.5),
        new THREE.MeshStandardMaterial({ color: 0x44aa88 })
    );
    scene.add(cube);

    // 标准材质需要灯光才能显出立体感
    scene.add(new THREE.AmbientLight(0xffffff, 0.5));
    const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5);
    directionalLight.position.set(3, 5, 4);
    scene.add(directionalLight);

    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(320, 240);
    containerRef.value.appendChild(renderer.domElement);

    // 渲染循环:每帧转一点再渲染
    const animate = () => {
        requestAnimationFrame(animate);
        cube.rotation.x += 0.01;
        cube.rotation.y += 0.01;
        renderer.render(scene, camera);
    };
    animate();
});
</script>

<style scoped>
.container {
    width: 320px;
    height: 240px;
}
</style>

转动起来之后,立方体各个面的明暗跟着角度变化,立体感比静态时强很多。


🧱 Clock:让不同设备一样快

cube.rotation.y += 0.01 这种写法有个隐藏问题:“每帧加多少”不代表“每秒加多少”

  • 60Hz 屏幕:每秒 60 帧 × 0.01 ≈ 每秒转 0.6 弧度。
  • 120Hz 屏幕:每秒 120 帧 × 0.01 ≈ 每秒转 1.2 弧度。

同一个动画,高刷屏上快了一倍!正确做法是按时间流逝驱动动画THREE.Clock 就是干这个的:

const clock = new THREE.Clock();

const animate = () => {
    requestAnimationFrame(animate);

    const elapsed = clock.getElapsedTime(); // 从启动到现在过了多少秒

    cube.rotation.y = elapsed * 0.6; // 每秒固定转 0.6 弧度,与帧率无关

    renderer.render(scene, camera);
};

思路从“每帧加多少”变成“现在应该转到哪”:用总时间 × 速度直接算出当前角度。不管屏幕每秒刷 60 次还是 120 次,1 秒后的角度都是一样的。


🛠 综合示例:旋转 + 上下浮动

elapsed 配合三角函数,几行代码就能做出“边转边上下漂浮”的效果:

const clock = new THREE.Clock();

const animate = () => {
    requestAnimationFrame(animate);

    const elapsed = clock.getElapsedTime();

    cube.rotation.y = elapsed * 0.8; // 匀速自转
    cube.position.y = Math.sin(elapsed * 2) * 0.4; // 在 -0.4 ~ 0.4 之间来回浮动

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

Math.sin() 的值在 -1 到 1 之间往复,正好拿来写“来回动”的动画:浮动、摆动、呼吸缩放都是这个套路。

下面是这个效果的真实渲染演示


🧾 小节总结

  • 动画 = 渲染循环:requestAnimationFrame 每秒调用约 60 次,每次更新状态再渲染。
  • requestAnimationFrame(animate) 写在函数开头,循环才不会断。
  • 固定增量写法(+= 0.01)在不同刷新率的设备上速度不一致。
  • 正确姿势:clock.getElapsedTime() 拿到总时间,用“总时间 × 速度”算当前状态。
  • Math.sin() 适合写一切“来回动”的动画。

❓ 知识问答

Q1:为什么不用 setInterval 做动画循环?

setInterval 不关心屏幕刷新时机,可能出现掉帧、画面撕裂,切到别的标签页还会空转浪费性能。requestAnimationFrame 和屏幕刷新同步,标签页不可见时自动暂停,是浏览器动画的标准方案。

Q2:组件销毁后,动画循环会自己停吗?

不会!循环还在后台空跑,白白消耗性能。必须用 cancelAnimationFrame 手动停止,具体写法在“Vue3 实战”篇里讲。

Q3:想让动画中途暂停怎么办?

简单做法:用一个变量当开关,if (paused) return 跳过后面的更新(循环本身继续跑,只是不改状态)。需要精确暂停时,可以用 clock.stop()clock.start()

Q4:场景里多个物体,要用多个 Clock 吗?

不需要。一个 Clock 拿到的 elapsed 是所有物体共用的时间轴,每个物体按自己的公式计算状态即可。


🧪 小练习

做一个“跳动的球”:

  1. 创建一个球体,用渲染循环让它绕 y 轴自转。
  2. Math.sin() 让球体上下跳动,幅度 0.5。
  3. 进阶:调整速度参数,让球体跳到高点时刚好转满整数圈,观察节奏感。
views/BounceBall.vue
<template>
    <div ref="containerRef" class="container"></div>
</template>

<script setup>
import { onMounted, ref } from 'vue';
import * as THREE from 'three';

const containerRef = ref(null);

onMounted(() => {
    // 请在这里编写代码
});
</script>

<style scoped>
.container {
    width: 320px;
    height: 240px;
}
</style>

🎉 恭喜你已经让场景动起来了!下一篇我们给场景装上鼠标交互,让用户可以拖拽着看。