用AnimationMixer制作动画
变形目标是制作动画最直接的方法。你可以为所有顶点指定每一个关键位置,然后让Three.js将这些顶点从一个关键位置移动到另一个。但这种方法有一个不足,那就是对于大型网格和大型动画,模型文件会变得非常大。原因是在每个关键位置上,所有顶点的位置都要重复一遍。
# 效果
先看一下下面的例子,是最终的实现效果,使用AnimationMixer:
# 关于动画
# 动画片段(Animation Clips)
如果您已成功导入3D动画对象(无论它是否有骨骼或变形目标或两者皆有都不要紧)—— 例如使用glTF Blender exporter(glTF Blender导出器) 从Blender导出它并使用GLTFLoader将其加载到three.js场景中 —— 其中一个响应字段应该是一个名为“animations”的数组, 其中包含此模型的AnimationClips(请参阅下面可用的加载器列表)。
每个AnimationClip通常保存对象某个活动的数据。 举个例子,假如mesh是一个角色,可能有一个AnimationClip实现步行循环, 第二个AnimationClip实现跳跃,第三个AnimationClip实现闪避等等。
# 关键帧轨道(Keyframe Tracks)
在这样的AnimationClip里面,每个动画属性的数据都存储在一个单独的KeyframeTrack中。 假设一个角色对象有Skeleton(骨架), 一个关键帧轨道可以存储下臂骨骼位置随时间变化的数据, 另一个轨道追踪同一块骨骼的旋转变化,第三个追踪另外一块骨骼的位置、转角和尺寸,等等。 应该很清楚,AnimationClip可以由许多这样的轨道组成。
假设模型具有morph Targets(变形目标)—— 例如一个变形目标显示一个笑脸,另一个显示愤怒的脸 —— 每个轨道都持有某个变形目标在AnimationClip运行期间产生的Mesh.morphTargetInfluences(变形目标影响)如何变化的信息。
# 动画混合器(Animation Mixer)
存储的数据仅构成动画的基础 —— 实际播放由AnimationMixer控制。 你可以想象这不仅仅是动画的播放器,而是作为硬件的模拟,如真正的调音台,可以同时控制和混合若干动画。
# 动画行为(Animation Actions)
AnimationMixer本身只有很少的(大体上)属性和方法, 因为它可以通过AnimationActions来控制。 通过配置AnimationAction,您可以决定何时播放、暂停或停止其中一个混合器中的某个AnimationClip, 这个AnimationClip是否需要重复播放以及重复的频率, 是否需要使用淡入淡出或时间缩放,以及一些其他内容(例如交叉渐变和同步)。
# 动画对象组(Animation Object Groups)
如果您希望一组对象接收共享的动画状态,则可以使用AnimationObjectGroup。
# 支持的格式和加载器(Supported Formats and Loaders)
请注意,并非所有模型格式都包含动画(尤其是OBJ,没有), 而且只有某些three.js加载器支持AnimationClip序列。 以下几个确实支持此动画类型:
- THREE.ObjectLoader
- THREE.BVHLoader
- THREE.ColladaLoader
- THREE.FBXLoader
- THREE.GLTFLoader
- THREE.MMDLoader
请注意,3ds max和Maya当前无法直接导出多个动画(这意味着动画不是在同一时间线上)到一个文件中。
# AnimationMixer
动画混合器是用于场景中特定对象的动画的播放器。当场景中的多个对象独立动画时,每个对象都可以使用同一个动画混合器。
# 构造器
AnimationMixer(rootObject:Object3D)
rootObject
- 混合器播放的动画所属的对象
# 实战
接下来开始实践:
首先初始化 camera,light,scene,renderer等等,这个不用介绍
然后加载一个GLTF格式的模型
const loader = new GLTFLoader()
loader.load("/daodao-knowledge/models/gltf/Horse.glb",function(gltf) {
console.log(gltf)
mesh = gltf.scene.children[0]
mesh.scale.set(1.5,1.5,1.5)
scene.add(mesh)
})
2
3
4
5
6
7
8
9
- 然后处理动画,通过
mesh
获取到AnimationMixer
对象:
mixer = new THREE.AnimationMixer( mesh )
- 然后使用
clipAction
方法,获取AnimationAction
对象: clipAction (clip : AnimationClip, optionalRoot : Object3D) : AnimationAction 返回所传入的剪辑参数的AnimationAction, 根对象参数可选,默认值为混合器的默认根对象。第一个参数可以是动画剪辑(AnimationClip)对象或者动画剪辑的名称。
如果不存在符合传入的剪辑和根对象这两个参数的动作, 该方法将会创建一个。传入相同的参数多次调用将会返回同一个剪辑实例。
然后使用 AnimationAction 对象的 setDuration ( durationInSeconds : Number ) : AnimationAction 设置单此循环的持续时间(通过调整时间比例(timeScale)以及停用所有的变形)。此方法可以链式调用。
然后调用 play () 让混合器激活动作。
mixer.clipAction( gltf.animations[0] ).setDuration(1).play()
全部代码:
import * as THREE from 'three/build/three.module.js';
import * as dat from '../../@js/dat.gui.js'
import Stats from '../../@js/stats.js'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
let container, stats
let camera, scene, renderer
let mesh, mixer, action
const radius = 600
let theta = 0
let prevTime = Date.now()
const params = {
"startAnimation": true,
"duration":1
}
function init() {
container = document.getElementById('three1')
camera = new THREE.PerspectiveCamera(50, container.clientWidth / container.clientHeight, 1, 10000)
camera.position.y = 300
scene = new THREE.Scene()
scene.background = new THREE.Color(0xf0f0f0)
const light1 = new THREE.DirectionalLight(0xefefff, 1.5)
light1.position.set(1,1,1).normalize()
scene.add(light1)
const light2 = new THREE.DirectionalLight(0xffefef, 1.5)
light2.position.set(-1,-1,-1).normalize()
scene.add(light2)
const loader = new GLTFLoader()
loader.load("/daodao-knowledge/models/gltf/Horse.glb",function(gltf) {
console.log(gltf)
mesh = gltf.scene.children[0]
mesh.scale.set(1.5,1.5,1.5)
scene.add(mesh)
mixer = new THREE.AnimationMixer( mesh )
action = mixer.clipAction( gltf.animations[0] )
action.setDuration(1).play()
})
renderer = new THREE.WebGLRenderer()
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(container.clientWidth, container.clientHeight)
renderer.outputEncoding = THREE.sRGBEncoding
container.appendChild(renderer.domElement)
stats = new Stats()
container.appendChild(stats.dom)
//dat.GUI
const gui = new dat.GUI({}, container)
gui.add(params,'startAnimation').onChange(function() {
if(params.startAnimation) {
action.play()
}else{
action.stop()
}
})
gui.add(params, 'duration',0,10).step(1).onChange(function() {
action.setDuration(params.duration)
})
}
function animate() {
requestAnimationFrame( animate )
render()
stats.update()
}
function render() {
theta += 0.1
camera.position.x = radius * Math.sin( THREE.MathUtils.degToRad(theta) )
camera.position.z = radius * Math.cos( THREE.MathUtils.degToRad(theta) )
camera.lookAt(0,150,0)
if(mixer) {
const time = Date.now()
mixer.update(( time - prevTime ) * 0.001)
prevTime = time
}
renderer.render(scene, camera)
}
init();
animate();
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