这些天有些忙,导致一个多星期没有更新文章,群里的小伙伴也天天催我更。为了表示歉意,我决定在现在的基础上面增加一节​​Three.js​​​核心类的相关介绍,来让让小伙伴们能更清楚的了解相关的基础内容。下面我们开始这一节的内容,动画。
动画一般可以定义两种:一种是变形动画,另一种是骨骼动画。下面,我们先介绍一下变形动画。

变形动画

变形动画的实现就是通过修改当前模型的顶点位置来实现动画。就比如,一个动画需要变动十次才可以实现,那么我们就需要为当前模型的每一个顶点定义每一次所在的位置,​​Three.js​​​通过每一次修改实现最后的一个动画的整个流程。
为了更好的理解变形动画,我们创建了一个案例,查看地址为:​​​点击这里​​ 这个案例是为了让我们更好的了解变形动画的实现以及使用。在右上角,我们能发现两个可切换的拖拽条,这两个拖拽条对应的是两个变形目标数组,拖拽范围是0-1,就是当前的变形目标对本体的影响程度。我们进行拖拽就可以发现,界面中的立方体也会跟随着变动,影响当前的立方体。接下来我讲解一下,当前案例是如何实现的:

  • 首先,在我们创建模型的几何体时,给几何体​​morphTargets​​​赋值了两个变形目标,​​morphTargets​​​是一个数组,这意味着我们可以增加很多的变形目标,在给​​morphTargets​​​添加的数组,我们需要自己定义一个名称和相关的顶点,这个顶点数据必须和默认的模型的顶点数据保持一致,设置完后,我们需要调用​​geometry​​​的​​computeMorphNormals()​​来更新:
var cubeGeometry = new THREE.BoxGeometry(4, 4, 4);

// 创建两个影响立方体的变形目标
var cubeTarget1 = new THREE.BoxGeometry(2, 10, 2);
var cubeTarget2 = new THREE.BoxGeometry(8, 2, 8);

// 将两个geometry的顶点放入到立方体的morphTargets里面
cubeGeometry.morphTargets[0] = {name: 'target1', vertices: cubeTarget1.vertices};
cubeGeometry.morphTargets[1] = {name: 'target2', vertices: cubeTarget2.vertices};
cubeGeometry.computeMorphNormals();
  • 然后,当前模型使用的材质必须设置可以使用变形目标变形:
var cubeMaterial = new THREE.MeshLambertMaterial({morphTargets: true, color: 0x00ffff});
  • 当我们创建好网格模型添加到场景内后,可以在​​mesh​​​对象上面找到​​morphTargetInfluences​​​这一个配置项,这里面也是一个数组,是和​​geometry​​​的​​morphTargets​​相对应的,主要就是用于设置当前的变形目标对本体的影响度,默认值为0-1,0为不影响本体,1为完全影响本体:
gui = {
influence1:0.01,
influence2:0.01,
update : function () {
cube.morphTargetInfluences[0] = gui.influence1;
cube.morphTargetInfluences[1] = gui.influence2;
}
};

通过上面我们手动实现了一个变形动画,就会发现,其实变形动画是一直在修改变形目标对本体的影响尺度。我们可以通过这个原理实现一些变形动画。
当前案例代码查看地址:​​​点击这里​

骨骼动画

骨骼动画是需要生成一个与模型相关的骨架,骨架中的骨骼也会存在对应关系,模型的每一个需要动画的顶点需要设置影响它的骨骼以及骨骼影响顶点的程度。骨骼动画和变形动画相比会比较复杂一些,但是它又有更多的灵活性。我们可以想象一下人体的骨骼,如果使用变形动画,需要把所有的每一次的变动都存一个顶点数组,而骨骼动画,只需要设置骨骼的相关信息,就可以实现更多的动画。下面我们看一下骨骼动画的简单案例:​​点击这里​

11 Animation动画_数据


这是一个官方提供的案例,我经过一些简单的修改,也将当前一个柱形图形的骨骼显示的出来,这个实现比较复杂,我们需要做的就是先理解它是怎么实现的:

  • 首先, 我们创建了一个圆柱几何体,然后通过圆柱的几何体每一个顶点的y轴坐标来设置需要绑定的骨骼的下标和影响的程度:
//遍历几何体所有的顶点
for (var i = 0; i < geometry.vertices.length; i++) {

//根据顶点的位置计算出骨骼影响下标和权重

var vertex = geometry.vertices[i];
var y = (vertex.y + sizing.halfHeight);

var skinIndex = Math.floor(y / sizing.segmentHeight);
var skinWeight = (y % sizing.segmentHeight) / sizing.segmentHeight;

geometry.skinIndices.push(new THREE.Vector4(skinIndex, skinIndex + 1, 0, 0));
geometry.skinWeights.push(new THREE.Vector4(1 - skinWeight, skinWeight, 0, 0));

}

几何体的​​skinIndices​​​属性和​​skinWeights​​属性就是来设置相关的绑定下标和权重(骨骼影响程度)。

  • 相应的,我们需要设置一组相关的骨骼,骨骼具有嵌套关系,这样才能实现一个骨架,由于圆柱体比较简单,我们就创建一条骨骼垂直嵌套的骨骼:
bones = [];

var prevBone = new THREE.Bone();
bones.push(prevBone);
prevBone.position.y = -sizing.halfHeight;

for (var i = 0; i < sizing.segmentCount; i++) {

var bone = new THREE.Bone();
bone.position.y = sizing.segmentHeight;
bones.push(bone); //添加到骨骼数组
prevBone.add(bone); //上一个骨骼定义为父级
prevBone = bone;

}
  • 创建纹理时,我们还需要设置当前纹理需要受到骨骼的影响,将材质的​​skinning​​​属性设置为​​true​​:
var lineMaterial = new THREE.MeshBasicMaterial({
skinning: true,
wireframe: true
});
  • 最后,我们需要创建骨骼材质,并将模型绑定骨骼:
mesh = new THREE.SkinnedMesh(geometry, [material, lineMaterial]);
var skeleton = new THREE.Skeleton(bones); //创建骨架
mesh.add(bones[0]); //将骨骼添加到模型里面
mesh.bind(skeleton); //模型绑定骨架

这样,我们就实现了使用​​Three.js​​​创建一个简单的骨骼动画。使用​​dat.gui​​​我们能够修改每一个骨骼的​​poisition​​​、​​rotation​​​和​​scale​​​并查看对当前模型的影响。案例的源码地址:​​点击这里​

两种动画的区别

变形动画主要用于精度要求高的动画,比如人物的面部表情。优点是动画表达会很到位,缺点就是扩展性不强,只能执行设置好的相关动画。
骨骼动画主要用于那种精度要求低,而且需要丰富多样的动画,就比如人物的走动,攻击防御等动画,我们可以通过一套骨骼,修改相应骨骼的位置的信息直接实现相应的效果。确定是没有变形动画的精度高,但是可以实现多种多样的效果。
总结:我们可以根据项目的需求来设置不同的动画,就比如一个人物模型,说话我们使用变形动画去实现,而肢体动作使用骨骼动画去实现。

导入模型动画

在​​Three.js​​​的动画系统中,你可以为模型的各种属性设置动画:骨骼动画,变形动画,材质的相关属性(颜色,透明度, 是否可见)。动画属性可以设置淡入淡出效果以及各种扭曲特效。也可以单独的改变一个对象或者多个对象上的动画的影响程度和动画时间。
为了实现这些,​​​Three.js​​​动画系统在2015年修改为了一个类似于​​Unity​​和虚幻引擎4的架构。接下来我们了解一下这套动画系统的主要组件以及它们时如何协同工作。

动画片段(Animation Clips)

在我们成功导入模型以后,如果模型拥有相关的动画属性,会在返回的模型数据中产生一个名为​​animations​​​的数组,数组的每一个子项都是一个​​AnimationClips​​​对象。
每一个单独​​​AnimationClips​​​对象通常保存的都是模型的一个动画的数据,假如,如果模型网格是一个人物角色,第一个​​AnimationClips​​​对象有可能保存的是人物走动的动画,第二个​​AnimationClips​​对象用于跳跃,第三个用于攻击动画等等。

关键帧轨迹(Keyframe Tracks)

在​​AnimationClips​​对象内部,一般会有四个属性:

  • name:当前的动画的一个名称
  • uuid:一个不会重复的uuid
  • duration:当前动画一个循环所需要的时间
  • tracks:轨迹当前动画每一次切换动作所需要的数据

假设当前的动画是骨骼动画,在关键帧轨迹中存储的数据是在每一帧骨骼随着时间变动的数据(位置,旋转和缩放等)。
如果当前动画是一个变形动画,在关键帧轨迹中将会把顶点数据的变动存储在其中(比如实现人脸的笑以及哭等动作)。

动画混合器(Animation Mixer)

在动画片段中存储的数据仅仅构成了动画实现的基础,实际的播放权力在动画混合器的手中。你可以想象动画混合器其实不仅仅只是作为动画的播放器,它还可以同时控制几个动画,混合它们或者合并它们。

动画播放器(Animation Actions)

这个英文我更乐意将它翻译成动画播放器,因为我们最终需要将数据生成一个动画播放器来操作当前的动画执行,暂停或者停止,是否使用淡入淡出效果或者将动画加快或减慢。

动画对象组(Animation Object Groups)

如果你希望一组模型对象共享当前的动画,我们可以使用动画对象组来实现。

通过导入模型显示动画

11 Animation动画_数组_02

变形动画

我们首先查看一个官方的模型案例,这个案例效果是一匹马奔跑的动画,我们也可以通过下面地址查看:​​点击这里​​ 接下来我们看一下这匹马是如何实现的:

  • 在模型加载成功以后,我们首先将模型创建出来,并将材质的​​morphTargets​​设置为ture,可以使用变形动画:
mesh = new THREE.Mesh(geometry, new THREE.MeshLambertMaterial({
vertexColors: THREE.FaceColors,
morphTargets: true
}));
mesh.castShadow = true;
mesh.scale.set(0.1, 0.1, 0.1);
scene.add(mesh);
  • 然后我们创建了一个针对于该模型的混合器:
mixer = new THREE.AnimationMixer(mesh);
  • 接着使用变形目标数据创建一个动画片段:
var clip = THREE.AnimationClip.CreateFromMorphTargetSequence('gallop', geometry.morphTargets, 30);
  • 使用混合器和动画片段创建一个动画播放器来播放:
var action = mixer.clipAction(clip); //创建动画播放器
action.setDuration(1); //设置当前动画一秒为一个周期
action.play(); //设置当前动画播放
  • 最后,我们还需要在重新绘制循环中更新混合器,进行动作更新:
function render() {

control.update();

var time = clock.getDelta();
//由于模型导入是异步的,所以我们再模型没有加载完之前是获取不到混合器的
if (mixer) {
mixer.update(time);
}

renderer.render(scene, camera);
}

11 Animation动画_数组_03

骨骼动画

骨骼动画模型我们使用的是​​gltf​​​格式,这个模型是在​​Sketchfab​​​网站下载,案例是一个小姐姐跳舞的一个片段,查看地址:​​点击这里​​​​gltf​​格式的模型导入进来以后,我们可以直接通过​​animations​​数组创建播放器:

mixer = new THREE.AnimationMixer(obj); //通过当前模型创建混合器
action = mixer.clipAction(gltf.animations[0]); //通过动画数据创建播放器

直接调用播放器的播放事件让动画播放:

action.play();

最后,我们还是需要在循环渲染中更新混合器,并将每一帧渲染的间隔时间传入

function render() {
control.update();
var time = clock.getDelta();
if (mixer) {
mixer.update(time);
}
renderer.render(scene, camera);
}