Skip to content

43. 实战:丝滑入场动画

Published:

3D 场景直接加载出来有点平平无奇,如果加上一个入场动画会体验更好:

2025-04-06 09.23.11.gif

比如上面是金铲铲的一个棋盘的入场动画。

是不是平平无奇的一个3D棋盘,加上入场动画后瞬间高大上了?

我们也来做一个入场动画。

之前我们做过一个穿梭隧道的效果:

2025-03-23 10.47.25.gif

2025-03-23 11.15.53.gif

穿梭时空隧道后进入 3D 场景,是不是也是一个不错的体验呢?

我们这节就来做这个入场动画。

npx create-vite tube-entry-animation

image.png

进入项目,安装依赖:

npm install
npm install --save three
npm install --save-dev @types/three

改下 src/main.js

import './style.css';
import * as THREE from 'three';
import {
    OrbitControls
} from 'three/addons/controls/OrbitControls.js';
import mesh from './mesh.js';

const scene = new THREE.Scene();

scene.add(mesh);

const directionLight = new THREE.DirectionalLight(0xffffff, 2);
directionLight.position.set(500, 400, 300);
scene.add(directionLight);

const ambientLight = new THREE.AmbientLight();
scene.add(ambientLight);

const width = window.innerWidth;
const height = window.innerHeight;

const helper = new THREE.AxesHelper(500);
// scene.add(helper);

const camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 10000);
camera.position.set(200, 800, 800);
camera.lookAt(0, 0, 0);

const renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height)

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

render();

document.body.append(renderer.domElement);

const controls = new OrbitControls(camera, renderer.domElement);

创建 Scene、Light、Camera、Renderer。

改下 style.css

body {
  margin: 0;
}

然后创建 mesh.js

import * as THREE from 'three';

const planeGeometry = new THREE.PlaneGeometry(1000, 1000);
const planeMaterial = new THREE.MeshLambertMaterial({
    color: new THREE.Color('skyblue')
});

const plane = new THREE.Mesh(planeGeometry, planeMaterial);
plane.rotateX(- Math.PI / 2);
plane.position.y = -50;

const boxGeometry = new THREE.BoxGeometry(100, 100, 100);
const boxMaterial = new THREE.MeshLambertMaterial({
    color: new THREE.Color('orange')
});
const box = new THREE.Mesh(boxGeometry, boxMaterial);

const box2 = box.clone();
box2.position.x = 200;

const  mesh = new THREE.Group();
mesh.add(plane);
mesh.add(box);
mesh.add(box2);

export default mesh;

一个平面,上面放了两个立方体。

跑一下:

npm run dev

image.png

是这样的一个场景:

2025-04-06 10.44.10.gif

然后我们加一个入场动画用的管道:

创建 src/tube.js

import * as THREE from 'three';

const path = new THREE.CatmullRomCurve3([
    new THREE.Vector3(-1000, 200, 900),
    new THREE.Vector3(-400, 800, 1000),
    new THREE.Vector3(0, 0, 0)
]);

const geometry = new THREE.TubeGeometry(path, 100, 50, 30);

const material = new THREE.MeshBasicMaterial({
    color: 'blue',
    wireframe: true
});

const tube = new THREE.Mesh(geometry, material);

export const tubePoints = path.getSpacedPoints(1000);

export default tube;

用三维样条曲线 + TubeGeometry 管道几何体创建一个管道。

注意,曲线的结束点一定是 0,0,0 这样管道出来相机才会正对着 3D 场景。

引入看下效果:

image.png

2025-04-06 11.25.17.gif

然后调一下位置:

image.png

tube.position.set(0, 500, 800);

export const tubePoints = path.getSpacedPoints(1000).map(item => {
    return new THREE.Vector3(item.x, item.y + 500, item.z + 800)
});

管道位置变了,拿到的点也得改一下位置。

2025-04-06 11.29.03.gif

接下来做一下穿梭隧道的相机动画:

image.png

image.png

let i = 0;
function render() {
    if(i< tubePoints.length - 1) {
        camera.position.copy(tubePoints[i]);
        camera.lookAt(tubePoints[i + 1]);
        i += 3;
    }
    renderer.render(scene, camera);
    requestAnimationFrame(render);
}

就是修改 camera.position 和 lookAt 为曲线上的一个个点。

2025-04-06 15.37.32.gif

用 wireframe 渲染线框不是很好看,我们渲染下点模型:

image.png

用 material.visible 设置 false 的属性把管道隐藏。

然后用同一个 geometry 渲染点模型。

material.visible = false;
const pointsMaterial = new THREE.PointsMaterial({
    color: 'orange',
    size: 3
});
const points = new THREE.Points(geometry, pointsMaterial);
tube.add(points);

2025-04-06 15.47.16.gif

好看多了。

但只是相机位置运动到展示 3D 场景的位置后,还是有点太单调了。

我们再用 Tween.js 给 3D 场景加一个旋转 rotation 的动画。

安装下 tweenjs

npm install --save @tweenjs/tween.js

本来管道的出口位置是 0,0,0,但我们改了下位置,所以现在管道出口在 0,500,800

image.png

这个是初始位置,运动到目标相机位置,并且加上 rotaion 的变化:

image.png

const tween = new Tween({
  x: 0,
  y: 500,
  z: 800,
  rotation: 0
}).to({
  x: 200,
  y: 800,
  z: 800,
  rotation: 180
}).repeat(0).easing(Easing.Quadratic.InOut).onUpdate((obj) => {
  camera.position.copy(new THREE.Vector3(obj.x, obj.y, obj.z));
  camera.lookAt(0, 0, 0);

  mesh.rotation.y = obj.rotation / 180 * Math.PI;
});

指定初始值和变化到的值,不重复,先加速后减速。

在 onUpdate 的时候修改相机位置、lookAt,还有 mesh.rotation

然后跑一下这个 tween.js 动画:

image.png

let started = false;
let i = 0;
function render(time) {
    if(i< tubePoints.length - 1) {
        camera.position.copy(tubePoints[i]);
        camera.lookAt(tubePoints[i + 1]);
        i += 4;
    } else {
      if(!started) {
        scene.remove(tube);
        tween.start();
        started = true;
      }
    }

    tween.update(time);
    renderer.render(scene, camera);
    requestAnimationFrame(render);
}

加一个 flag,当到达管道末尾的时候,把管道删除,然后开启动画。

每一帧都用 update 更新数值。

2025-04-06 16.06.37.gif

这样,入场动画就完成了。

相比直接展示这个 3D 场景,加一个入场动画是不是效果好很多呢?

这个入场动画可以加到任意 3D 场景上,比如之前的林海雪原:

2025-04-06 16.10.12.gif

案例代码上传了小册仓库

总结

直接展示 3D 场景有些单调,我们可以在打开网页的时候加一个入场动画效果。

我们首先画了一个管道,用相机动画穿梭这个管道,最后相机到达 3D 场景的位置。

然后用 tween.js 来做场景的 ratation 和 camera.position 的缓动动画。

当然,这只是一个实现入场动画的思路,你完全可以做别的入场动画。

入场动画对于提升 3D 场景的体验,还是很有用的。

评论