立方体动画应用 -- three.js 官网示例

482 阅读7分钟

该示例通过批量生成25个立方体,并添加到统一的动画组 AnimationObjectGroup 中,使用关键帧动画,对这些模型立方体进行动画控制。可以说,这个示例是对关键帧动画的综合应用,示例相对比较简单。通过对该示例的学习,我们可以更好地理解掌握three.js 动画的使用。

关键帧动画.gif

一、AnimationObjectGroup

AnimationObjectGroup 是接收共享状态的一组对象。多个对象形成一个整体,对整体加上动画,则里面元素将会以整齐划一的状态进行运动。

1. 构造函数

AnimationObjectGroup( obj1 : Object, obj2 : Object, obj3 : Object, ... )

obj 是共享同一动画状态的任意数量的网格。

2.属性

属性名说明
isAnimationObjectGroup只读标志,以检查给定对象是否属于 isAnimationObjectGroup 对象
stats一个包含此动画对象组的一些信息的对象(总数,使用中的数量,绑定到每个对象上的数量)
uuid这个动画对象组的UUID。 他是自动分配的,不可被编辑

3.方法

方法名说明
add将任意数量的对象添加到这个动画对象组
remove将任意数量的对象从这个动画对象组中删除
uncache释放此对象组传递的对象的所有内存资源

二、Quaternion

Quaternion 是一个数学相关的类,可以用来计算或表示物体在3D空间中的旋转姿态角度。

1. 构造函数

Quaternion( x : Float, y : Float, z : Float, w : Float )
参数名说明
xx坐标
yy坐标
zz坐标
ww坐标

2. 四元素的一些常用方法

方法名说明
setFromAxisAngle()用来辅助生成表示特定旋转的四元数
setFromAxisAngle(axis, angle)生成的四元数表示绕 axis 旋转,旋转角度是 angle
setFromAxisAngle()可以生成一个四元数,绕任意轴、旋转任意角度,并不局限于x、y、 z轴

3.区别物体角度属性 .rotation 和 四元属性 .quaternion

three.js 模型对象的角度 .ratation 和 四元数 .quaternion 属性都是用来表示物体姿态角度的,只是表达的形式不同而已。 rotationquaternion 两个属性的值,一个改变,另一个也会同步改变。

三、关键帧动画

three.js 关键帧动画和css 的动画有些相似,如果使用过css 关键帧动画然后来学习three动画,相信很快能上手。

1. 创建关键帧动画一般步骤

  • 给需要设置关键帧动画的模型命名
  • 设置关键帧数据,使用 KeyframeTrack
  • 基于关键帧数据 KeyframeTrack, 创建关键帧动画 AnimationClip
// 给需要设置关键帧动画的模型命名
mesh.name = "Box";
const times = [0, 3, 6]; //时间轴上,设置三个时刻0、3、6秒
// times中三个不同时间点,物体分别对应values中的三个xyz坐标
const values = [0, 0, 0, 100, 0, 0, 0, 0, 100];
// 0~3秒,物体从(0,0,0)逐渐移动到(100,0,0),3~6秒逐渐从(100,0,0)移动到(0,0,100)
const posKF = new THREE.KeyframeTrack('Box.position', times, values);
// 从2秒到5秒,物体从红色逐渐变化为蓝色
const colorKF = new THREE.KeyframeTrack('Box.material.color', [2, 5], [1, 0, 0, 0, 0, 1]);
// 1.3 基于关键帧数据,创建一个clip关键帧动画对象,命名"test",持续时间6秒。
const clip = new THREE.AnimationClip("test", 6, [posKF, colorKF]);
1.1 KeyframeTrack 设置关键帧数据

KeyframeTrack 是由时间和相关值的列表组成,用来让一个对象的某个特定属性动起来。

// 构造函数:
KeyframeTrack( name : String, times : Array, values : Array, interpolation : [Constant]() )
参数名说明
name关键帧轨道得标识符
times关键帧得时间数组,被内部转化为Float32Array.
values与时间数组中的时间点相关的值组成的数组,被内部转化为 Float32Array.
inerpolation插值类型(用的比较少)
1.2 AnimationClip 创建关键帧动画
AnimationClip( name : String, duration : Number, tracks : Array )
参数名说明
name名称
duration持续时间(单位秒)。如果传入负数,持续时间将会从传入的数组中计算得到
tracks一个由关键帧轨道(KeyframeTrack) 组成的数组

2. AnimationMixer播放关键帧动画AnimationClip

//包含关键帧动画的模型对象作为AnimationMixer的参数创建一个播放器mixer
const mixer = new THREE.AnimationMixer(mesh);
//AnimationMixer的`.clipAction()`返回一个AnimationAction对象
const clipAction = mixer.clipAction(clip); 
//.play()控制动画播放,默认循环播放
clipAction.play(); 

3. mixer.update()更新播放器AnimationMixer时间

const frameT = clock.getDelta();
// 更新播放器相关的时间
mixer.update(frameT);

4. 动画播放的常用API

  • 是否循环播放(默认是循环播放)
// 设置不循环播放
clipAction.loop = THREE.LoopOnce; 
  • 播放完停在结束状态 (注意: 不循环播放,播放结束默认停在开头那一帧)
// 物体状态停留在动画结束的时候。
clipAction.clampWhenFinished = true;
  • 动画结束 stop
//动画停止结束,回到开始状态
clipAction.stop();
  • 是否暂停动画 paused paused 默认值是false,代表正常执行;如果设为true,暂停。
clipAction.paused = true;//切换为暂停状态
  • 倍速播放
clipAction.timeScale = 1;//默认
clipAction.timeScale = 2;//2倍速

5. 示例使用到的关键帧动画

// create some keyframe tracks

const xAxis = new THREE.Vector3( 1, 0, 0 );
// Quaternion:四元数,在three.js中用于表示旋转
const qInitial = new THREE.Quaternion().setFromAxisAngle( xAxis, 0 );
// Math.PI: 表示一个圆的周长和直径的比例,约为 3.14159
const qFinal = new THREE.Quaternion().setFromAxisAngle( xAxis, Math.PI );
// QuaternionKeyframeTrack: 四元数类型的关键帧轨道
const quaternionKF = new THREE.QuaternionKeyframeTrack( '.quaternion', [ 0, 1, 2 ], [ qInitial.x, qInitial.y, qInitial.z, qInitial.w, qFinal.x, qFinal.y, qFinal.z, qFinal.w, qInitial.x, qInitial.y, qInitial.z, qInitial.w ] );
// ColorKeyframeTrack: 反应颜色变化的关键帧轨道
const colorKF = new THREE.ColorKeyframeTrack( '.material.color', [ 0, 1, 2 ], [ 1, 0, 0, 0, 1, 0, 0, 0, 1 ], THREE.InterpolateDiscrete );
// NumberKeyframeTrack: 数字类型的关键帧轨道
const opacityKF = new THREE.NumberKeyframeTrack( '.material.opacity', [ 0, 1, 2 ], [ 1, 0, 1 ] );

// create clip
// AnimationClip: 动画剪辑,是一个可重用的关键帧轨道集,他代表动画
const clip = new THREE.AnimationClip( 'default', 3, [ quaternionKF, colorKF, opacityKF ] );

// AnimationMixer: 动画混合器是用于场景中特定对象的动画的播放器。 当场景中的多个对象独立动画时,每个对象都可以使用同一个动画混合器
mixer = new THREE.AnimationMixer( animationGroup );
// clipAction: 返回锁传入的剪辑参数的 AnimationAction, 根对象参数可选,默认值为混合器的默认根对象。
// 第一个参数可以是动画剪辑对象或动画剪辑的名称
const clipAction = mixer.clipAction( clip );
clipAction.play();

四、示例实现思路

  1. 初始化场景、相机、动画组
  2. 使用双重循环,生成25个立方体模型,并设置模型的x y z 轴位置,添加到动画组对象中
  3. 对动画组设置关键帧动画,使得这25个立方体按照整齐划一的动起来。动画包括旋转、颜色、和透明度的变化
  4. 处理页面窗口大小改变(resize)

五、源码

<!DOCTYPE html>
<html lang="en">
	<head>
		<title>three.js webgl - animation - groups</title>
		<meta charset="utf-8" />
		<meta
			name="viewport"
			content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"
		/>
		<link type="text/css" rel="stylesheet" href="main.css" />
	</head>
	<body>
		<div id="info">
			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a>
			webgl - animation - groups
		</div>

		<script type="importmap">
			{
				"imports": {
					"three": "../build/three.module.js",
					"three/addons/": "./jsm/"
				}
			}
		</script>

		<script type="module">
			import * as THREE from "three";

			import Stats from "three/addons/libs/stats.module.js";

			let stats, clock;
			let scene, camera, renderer, mixer;

			init();
			animate();

			function init() {
				scene = new THREE.Scene();

				//

				camera = new THREE.PerspectiveCamera(
					40,
					window.innerWidth / window.innerHeight,
					1,
					1000
				);
				camera.position.set(50, 50, 100);
				camera.lookAt(scene.position);

				// all objects of this animation group share a common animation state
				// AnimationObjectGroup: 接收共享动画的一组对象
				const animationGroup = new THREE.AnimationObjectGroup();

				// BoxGeometry: 立方缓冲集合体
				const geometry = new THREE.BoxGeometry(5, 5, 5);
				// MeshBasicMaterial:基础网格材质,这种材质不受光照的影响
				const material = new THREE.MeshBasicMaterial({ transparent: true });

				//

				for (let i = 0; i < 5; i++) {
					for (let j = 0; j < 5; j++) {
						const mesh = new THREE.Mesh(geometry, material);

						mesh.position.x = 32 - 16 * i;
						mesh.position.y = 0;
						mesh.position.z = 32 - 16 * j;

						scene.add(mesh);
						animationGroup.add(mesh);
					}
				}

				// create some keyframe tracks

				const xAxis = new THREE.Vector3(1, 0, 0);
				// Quaternion:四元数,在three.js中用于表示旋转
				const qInitial = new THREE.Quaternion().setFromAxisAngle(xAxis, 0);
				// Math.PI: 表示一个圆的周长和直径的比例,约为 3.14159
				const qFinal = new THREE.Quaternion().setFromAxisAngle(xAxis, Math.PI);
				// QuaternionKeyframeTrack: 四元数类型的关键帧轨道
				const quaternionKF = new THREE.QuaternionKeyframeTrack(
					".quaternion",
					[0, 1, 2],
					[
						qInitial.x,
						qInitial.y,
						qInitial.z,
						qInitial.w,
						qFinal.x,
						qFinal.y,
						qFinal.z,
						qFinal.w,
						qInitial.x,
						qInitial.y,
						qInitial.z,
						qInitial.w,
					]
				);
				// ColorKeyframeTrack: 反应颜色变化的关键帧轨道
				const colorKF = new THREE.ColorKeyframeTrack(
					".material.color",
					[0, 1, 2],
					[1, 0, 0, 0, 1, 0, 0, 0, 1],
					THREE.InterpolateDiscrete
				);
				// NumberKeyframeTrack: 数字类型的关键帧轨道
				const opacityKF = new THREE.NumberKeyframeTrack(
					".material.opacity",
					[0, 1, 2],
					[1, 0, 1]
				);

				// create clip
				// AnimationClip: 动画剪辑,是一个可重用的关键帧轨道集,他代表动画
				const clip = new THREE.AnimationClip("default", 3, [
					quaternionKF,
					colorKF,
					opacityKF,
				]);

				// AnimationMixer: 动画混合器是用于场景中特定对象的动画的播放器。 当场景中的多个对象独立动画时,每个对象都可以使用同一个动画混合器
				mixer = new THREE.AnimationMixer(animationGroup);
				// clipAction: 返回锁传入的剪辑参数的 AnimationAction, 根对象参数可选,默认值为混合器的默认根对象。
				// 第一个参数可以是动画剪辑对象或动画剪辑的名称
				const clipAction = mixer.clipAction(clip);
				clipAction.play();

				// WebGLRenderer: 用 WebGL 渲染出你精心制作的场景
				renderer = new THREE.WebGLRenderer({ antialias: true });
				renderer.setPixelRatio(window.devicePixelRatio);
				renderer.setSize(window.innerWidth, window.innerHeight);
				document.body.appendChild(renderer.domElement);

				//

				stats = new Stats();
				document.body.appendChild(stats.dom);

				//Clock: 用于跟踪时间
				clock = new THREE.Clock();

				window.addEventListener("resize", onWindowResize);
			}

			function onWindowResize() {
				camera.aspect = window.innerWidth / window.innerHeight;
				camera.updateProjectionMatrix();

				renderer.setSize(window.innerWidth, window.innerHeight);
			}

			function animate() {
				requestAnimationFrame(animate);

				render();
			}

			function render() {
				const delta = clock.getDelta();

				if (mixer) {
					mixer.update(delta);
				}

				renderer.render(scene, camera);

				stats.update();
			}
		</script>
	</body>
</html>