【可视化学习】56-3D动画实现讲解
发表于:2023-12-09 |

前言

大家好啊,12月了,这个月我想着尽量多学一点,多写一点,多分享一点,能够将2023年更加充实一点,不留遗憾。本文主要详细讲解如何使用three.js实现3D动画,这个我在之前很多的文章中都有提到,但是都是一笔带过,这次我想着详细讲解一下,方便大家更好地理解我之前文章中的内容。话不多说,我们开始吧。

小试牛刀-动画模型导入

其实我们很多的动画都是可以通过我们的建模软件来进行完成,之后再导入模型之后,我们进行使用即可,比如这样一个动画,我们在blender中制作好关键的动画帧之后就可以使用,下面我们先带大家一起来导入这个模型,再进行动画的播放。

blender动画

首先我们先来看一下我们的动画效果,这些动画一般都是建模小姐姐完成的,我们只需要调用即可。

初始化代码导入模型

这块就不具体说了,能看我博客的都是大佬,有3d基础的,直接上代码吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
// 导入threejs
import * as THREE from "three";
// 导入轨道控制器
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
// 导入lil.gui
import { GUI } from "three/examples/jsm/libs/lil-gui.module.min.js";
// 导入hdr加载器
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader.js";
// 导入gltf加载器
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
// 导入draco解码器
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js";
// 创建场景
const scene = new THREE.Scene();

// 创建相机
const camera = new THREE.PerspectiveCamera(
45, // 视角
window.innerWidth / window.innerHeight, // 宽高比
0.1, // 近平面
1000 // 远平面
);

// 创建渲染器
const renderer = new THREE.WebGLRenderer({
antialias: true, // 开启抗锯齿
});
renderer.shadowMap.enabled = true;
renderer.toneMapping = THREE.ReinhardToneMapping;
renderer.toneMappingExposure = 1;
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// 设置相机位置
camera.position.z = 8;
camera.position.y = 2.5;
camera.position.x = 3;
camera.lookAt(0, 1.2, 0);

// 添加世界坐标辅助器
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);

// 添加网格辅助器
const gridHelper = new THREE.GridHelper(50, 50);
gridHelper.material.opacity = 0.3;
gridHelper.material.transparent = true;
scene.add(gridHelper);

// 添加轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
// 设置带阻尼的惯性
controls.enableDamping = true;
// 渲染函数
function animate() {
controls.update();
requestAnimationFrame(animate);
// 渲染
renderer.render(scene, camera);
}
animate();

// 监听窗口变化
window.addEventListener("resize", () => {
// 重置渲染器宽高比
renderer.setSize(window.innerWidth, window.innerHeight);
// 重置相机宽高比
camera.aspect = window.innerWidth / window.innerHeight;
// 更新相机投影矩阵
camera.updateProjectionMatrix();
});

// rgbeLoader 加载hdr贴图
let rgbeLoader = new RGBELoader();
rgbeLoader.load("./texture/Alex_Hart-Nature_Lab_Bones_2k.hdr", (envMap) => {
// 设置球形贴图
// envMap.mapping = THREE.EquirectangularReflectionMapping;
envMap.mapping = THREE.EquirectangularRefractionMapping;
// 设置环境贴图
// scene.background = envMap;
scene.background = new THREE.Color(0xcccccc);
// 设置环境贴图
scene.environment = envMap;
});
// rgbeLoader 加载hdr贴图
// 实例化加载器gltf
const gltfLoader = new GLTFLoader();
// 实例化加载器draco
const dracoLoader = new DRACOLoader();
// 设置draco路径
dracoLoader.setDecoderPath("./draco/");
// 设置gltf加载器draco解码器
gltfLoader.setDRACOLoader(dracoLoader);
// 加载模型
gltfLoader.load(
// 模型路径
"./model/huawei.glb",
// 加载完成回调
(gltf) => {
console.log(gltf);
scene.add(gltf.scene);
}
);

gltf内容介绍

gltf详情
gltf详情
从上面这张图我们可以看到我们的gltf导入之后有一个animation,这个就是动画的数组,可以放置多个动画,AnimationClip就是我们的动画剪辑对象,tracks是我们的轨道集,里面含有的每一个对象VectorKeyframeTrack都是我们的向量关键帧,times是我们的时间点,values是我们对应时间这个点位该在的位置,因为我们的是三维坐标所以是三个值,times和values的长度刚好是1:3的关系

动画播放

创建动画混合器-获取动画-播放动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 加载模型
gltfLoader.load(
// 模型路径
"./model/huawei.glb",
// 加载完成回调
(gltf) => {
console.log(gltf);
scene.add(gltf.scene);
//创建动画混合器
mixer = new THREE.AnimationMixer(gltf.scene);
//获取动画
const action = mixer.clipAction(gltf.animations[0]);
//播放动画
action.play();
}
);

动画更新

这样我们还是无法看到动画的效果,因为我们还没有更新动画,我们需要在渲染函数中更新动画,这样才能看到动画的效果。

1
2
3
4
5
6
7
8
9
10
11
12
let mixer;
let clock = new THREE.Clock();
// 渲染函数
function animate() {
let delta = clock.getDelta();
controls.update();
mixer&&mixer.update(delta);
requestAnimationFrame(animate);
// 渲染
renderer.render(scene, camera);
}
animate();

three.js设置keyframe实现位移动画

当然,一些简单的动画我们完全可以使用threejs自带的关键帧来实现,这样我们就不需要使用建模软件来进行制作了,我们直接使用代码来实现即可,下面我们就来看一下如何使用threejs来实现关键帧动画。

创建立方体

在这里我们之前的代码不动,就改一下导入模型那块代码,改成我们自己写动画即可。

1
2
3
4
5
6
7
8
// 创建立方体
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({
color: 0xff33ff,
});
const cube = new THREE.Mesh(geometry, material);
cube.name = "cube";
scene.add(cube);

创建关键帧动画

这里有三个参数,第一个就是名称的前缀,第二个是时间点,第三个是值,这里我们的时间点是0-4,分别对应的值是【0,0,0】,【2,0,0】,【4,0,0】,【2,0,0】,【0,0,0】,这里的值是三维坐标,所以是15个值,这里的时间点和值的长度是1:3的关系,这里的时间点是秒,所以我们的动画时长是4秒。

1
2
3
4
5
6
7
8
9
10
11
12
// 创建位移动画
const positionKF = new THREE.VectorKeyframeTrack(
"cube.position",
[0, 1, 2, 3, 4],
[0, 0, 0, 2, 0, 0, 4, 0, 0, 2, 0, 0, 0, 0, 0]
);
mixer = new THREE.AnimationMixer(cube);
// 创建动画剪辑
const clip = new THREE.AnimationClip("move", 4, [positionKF]);
// 创建动画动作
const action = mixer.clipAction(clip);
action.play();

实现旋转动画

我们基于上面的位移代码不要动,往上面添加我们的旋转关键帧动画

使用欧拉角实现旋转

这里我定义了三个欧拉角,分别是【0,0,0】,【Math.PI,0,0】,【0,0,0】,这里的欧拉角是弧度制,所以我们的第二个欧拉角是180度。Euler这个有四个参数,分别是x,y,z,order,这里的order是我们的旋转顺序,我们这里使用的是xyz,所以我们的旋转顺序是先绕x轴旋转,再绕y轴旋转,最后绕z轴旋转。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const quaternion1 = new THREE.Quaternion();
quaternion1.setFromEuler(new THREE.Euler(0, 0, 0));
const quaternion2 = new THREE.Quaternion();
quaternion2.setFromEuler(new THREE.Euler(Math.PI, 0, 0));
const quaternion3 = new THREE.Quaternion();
quaternion3.setFromEuler(new THREE.Euler(0, 0, 0));
console.log(quaternion1, quaternion2, quaternion3);
const finQ = quaternion1
.toArray()
.concat(quaternion2.toArray())
.concat(quaternion3.toArray());
console.log(finQ);

// 创建旋转动画帧
const rotationKF = new THREE.QuaternionKeyframeTrack(
"cube.quaternion",
[0, 2, 4],
finQ
);

// 创建动画剪辑
const clip = new THREE.AnimationClip("move", 4, [positionKF, rotationKF]);

使用四元数

当然这里我们还可以用另外一种方式来实现

1
2
3
4
5
6
const quaternion1 = new THREE.Quaternion();
quaternion1.setFromAxisAngle(new THREE.Vector3(1, 0, 0), 0);
const quaternion2 = new THREE.Quaternion();
quaternion2.setFromAxisAngle(new THREE.Vector3(1, 0, 0), Math.PI);
const quaternion3 = new THREE.Quaternion();
quaternion3.setFromAxisAngle(new THREE.Vector3(1, 0, 0), 0);

这样实现起来也是一样的,这里的第一个参数是我们的旋转轴,第二个参数是我们的旋转角度,这里的旋转轴是单位向量,所以我们这里的旋转轴是x轴,旋转角度是0,180,0,这样我们就可以实现旋转了。效果和上面的欧拉角实现是一样的。

布尔关键帧实现闪烁动画

我们接下来导入个模型,然后实现一个闪烁的动画。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 加载模型
gltfLoader.load(
// 模型路径
"./model/moon.glb",
// 加载完成回调
(gltf) => {
console.log(gltf);
// gltf.scene.scale.set(0.01, 0.01, 0.01);
scene.add(gltf.scene);
mixer1 = new THREE.AnimationMixer(gltf.scene);

//布尔关键帧
const boolKF = new THREE.BooleanKeyframeTrack(
"Sketchfab_Scene.visible",
[0, 1, 2, 3, 4],
[true, false, true, false, true]
);
// 创建动画剪辑
const clip = new THREE.AnimationClip("bool", 4, [boolKF]);
// 给模型添加动画
const action1 = mixer1.clipAction(clip);
action1.play();
}
);

颜色关键帧动画

ok,我们先将那个月球给去掉,来看看颜色关键帧动画的效果

1
2
3
4
5
6
7
8
9
10
11
12
//颜色关键帧
const colorKF = new THREE.ColorKeyframeTrack(
"cube.material.color",
[0, 2, 4],
[1, 0, 1, 1, 1, 0, 1, 0, 1]
);
// 创建动画剪辑
const clip = new THREE.AnimationClip("move", 4, [
positionKF,
rotationKF,
colorKF,
]);

数值动画

这里我们修改下物体的透明度,可能效果不是很明显,大家可以仔细看

1
2
3
4
5
6
7
8
9
10
11
12
const opacityKF = new THREE.NumberKeyframeTrack(
"cube.material.opacity",
[0, 1, 2, 3, 4],
[1, 0, 1, 0, 1]
);
// 创建动画剪辑
const clip = new THREE.AnimationClip("move", 4, [
positionKF,
rotationKF,
colorKF,
opacityKF
]);

控制模型丝滑切换动作

导入模型

首先我们导入一个有多个动作的模型

1
2
3
4
5
6
7
8
9
10
11
// 加载模型
gltfLoader.load(
// 模型路径
"./model/hilda_regular_00.glb",
// 加载完成回调
(gltf) => {
console.log(gltf);
let girl = gltf.scene;
scene.add(gltf.scene);
}
);

模型导入

定义动作

这里我导入了模型中的走路,跑步,站立,打招呼,摆pos的动作,设置了当前动作currentAction为站立动作,添加了timeScale的控制,这个是动画的播放速度,我们可以通过这个来控制动画的播放速度,相当于倍速,如果为负数,就是动画反过来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let walkAction, runAction, posAction, greetAction, idleAction;
let currentAction = null;
// 加载模型
gltfLoader.load(
// 模型路径
"./model/hilda_regular_00.glb",
// 加载完成回调
(gltf) => {
console.log(gltf);
let girl = gltf.scene;
scene.add(gltf.scene);
mixer = new THREE.AnimationMixer(gltf.scene);
walkAction = mixer.clipAction(gltf.animations[37]);
runAction = mixer.clipAction(gltf.animations[27]);
idleAction = mixer.clipAction(gltf.animations[6]);
posAction = mixer.clipAction(gltf.animations[23]);
greetAction = mixer.clipAction(gltf.animations[0]);
currentAction = idleAction;
idleAction.play();
gui.add(mixer, "timeScale");
console.log(mixer);
}
);

添加事件切换和gui添加

这里其实很简单,就是添加事件,然后在事件中切换动作即可。
注意点就是我们每个动作都需要设置权重setEffectiveWeight,设置启用enabled,设置动画播放速度setEffectiveTimeScale,crossFadeTo用来过渡切换动作切换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// 创建GUI
const gui = new GUI();

let eventObj = {
stopAll: () => {
mixer.stopAllAction();
},
play: () => {
mixer._actions.forEach((action) => {
action.play();
});
},
playRun: () => {
runAction.enabled = true;
runAction.setEffectiveTimeScale(1);
runAction.setEffectiveWeight(1);
runAction.play();
currentAction.crossFadeTo(runAction, 0.5, true);
currentAction = runAction;
},
playWalk: () => {
walkAction.enabled = true;
walkAction.setEffectiveTimeScale(1);
walkAction.setEffectiveWeight(1);
walkAction.play();
currentAction.crossFadeTo(walkAction, 0.5, true);
currentAction = walkAction;
},
playGreet: () => {
greetAction.enabled = true;
greetAction.setEffectiveTimeScale(1);
greetAction.setEffectiveWeight(1);
greetAction.play();
currentAction.crossFadeTo(greetAction, 0.5, true);
currentAction = greetAction;
},
playIdle: () => {
idleAction.enabled = true;
idleAction.setEffectiveTimeScale(1);
idleAction.setEffectiveWeight(1);
idleAction.play();
currentAction.crossFadeTo(idleAction, 0.5, true);
currentAction = idleAction;
},
playPos: () => {
posAction.enabled = true;
posAction.setEffectiveTimeScale(1);
posAction.setEffectiveWeight(1);
posAction.play();
currentAction.crossFadeTo(posAction, 0.5, true);
currentAction = posAction;
},
};

gui.add(eventObj, "play");
gui.add(eventObj, "playRun");
gui.add(eventObj, "playWalk");
gui.add(eventObj, "playGreet");
gui.add(eventObj, "playIdle");
gui.add(eventObj, "playPos");

结语

好了,本篇关于动画的部分暂时就讲到这里,债见~

上一篇:
使用nvm进行node版本的管理
下一篇:
【可视化学习】55-家具编辑器