用AnimationMixer制作动画

更新时间: 2021-09-10 09:16:01

变形目标是制作动画最直接的方法。你可以为所有顶点指定每一个关键位置,然后让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 - 混合器播放的动画所属的对象

# 实战

接下来开始实践:

  1. 首先初始化 camera,light,scene,renderer等等,这个不用介绍

  2. 然后加载一个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)

  })
1
2
3
4
5
6
7
8
9
  1. 然后处理动画,通过mesh获取到AnimationMixer对象:
mixer = new THREE.AnimationMixer( mesh )
1
  1. 然后使用 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()
1

全部代码:

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();

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