概述

BlendShapes 是一种在计算机图形学中常用的技术,用于实现模型的形状插值和变形。在 Three.js 中,BlendShapes 通常用于实现模型的面部表情动画或者其他形状变换。

在 Three.js 中,您可以通过使用 THREE.MeshmorphTargetInfluences 属性来控制 BlendShapes。每个 BlendShape 被称为一个 "Morph Target",您可以通过调整每个 Morph Target 的影响度来实现模型的形状变换。

示例

以Three.js官方examples为例(演示地址),这是一张可以不断变换表情的人脸,人类的面部表情是成千上万种的,如果使用预制动画的方式,在计算机模型中来模拟人类的面部表情,这将是一个庞大的工程。

BlendShapes的出现就是要将每一个面部动作单独控制,通过不同的组合来实现不同的表情动画。

打开示例我们可以看到右侧的GUI控制栏上的数据在不停的变化,这就是52个BlendShape的值,每一个点位控制一个部位的微表情。 这些点位的名称不是固定的,是预制于每一个模型里的。

代码示例

打开Three.js的源码(地址:GitHub),首先还是创建基础的场景、灯光、相机,这就不一一赘述,然后就是加载了一个压缩的glb模型:

				new GLTFLoader()
					.setKTX2Loader( ktx2Loader )
					.setMeshoptDecoder( MeshoptDecoder )
					.load( 'models/gltf/facecap.glb', ( gltf ) => {

					} );

加载完成后开始播放模型预制的动画


						const mesh = gltf.scene.children[ 0 ];

						scene.add( mesh );

						mixer = new THREE.AnimationMixer( mesh );

						mixer.clipAction( gltf.animations[ 0 ] ).play();

然后将数字记录到GUI面板,这样我们就能只管的看到每一个点位的数值变化

// GUI

						const head = mesh.getObjectByName( 'mesh_2' );
						const influences = head.morphTargetInfluences;
						const gui = new GUI();
						gui.close();

						for ( const [ key, value ] of Object.entries( head.morphTargetDictionary ) ) {

							gui.add( influences, value, 0, 1, 0.01 )
								.name( key.replace( 'blendShape1.', '' ) )
								.listen();

						}

我们看到,在代码中读取了morphTargetInfluences、morphTargetDictionary 字段,这些字段是什么呢?我们看一下模型加载时的mesh数据:

morphTargetDictionary 是Object格式的数据,他代表的就是BlendShapes的数据字典,每一个key就是对应点位的名称,value就是这个点位在morphTargetInfluences的位置。继续看看morphTargetInfluences的数据:

morphTargetInfluences是个长度为52的数组,每一项的数据初始化都为0,这些数据就是控制BlendShape用的,我们刚刚讲过,morphTargetDictionary的每一项的值就是对应此项在morphTargetInfluences数组中的数组下标。这样我们就能通过morphTargetDictionary数据字典,来定位到每一个BlendShape,修改对应的值就可以修改点位的状态,代码如下:

// 假如我们要实现张嘴的动作,这个点位对应的key就是jawOpen,首先获取到
const jawOpenIndex = morphTargetDictionary.jawOpen;
// 然后修改这个值
meth.morphTargetInfluences[jawOpenIndex] = 1;

将jawOpen点位的值赋为1,那么就会发现,模型的嘴巴张开了。

制作动画

这种将点位直接赋值为1的方式是没有过渡效果的,要实现动画功能,我们要逐渐修改这个值,简单的做法为:

		let value = 0;
        const timer = setInterval(() => {
          value += 0.01;
          const jawOpenIndex = morphTargetDictionary.jawOpen;
          meth..morphTargetInfluences[jawOpenIndex] = value;
          if (value >= 1) {
            clearInterval(timer);
          }
        }, 10);

通过定时器,我们可以缓慢的修改值,来达到一个过渡动画的效果。

当然这只是一个简单的思路,Three.js提供了一个定义数值关键帧动画的类:NumberKeyframeTrack,我们可以用这个类来创建一个关键帧动画:

// animationList是一组点位数据
let animationList = [];
// 生成空的动画数组
let animationData = [];
// 创建身体动画数据集
for (
     let i = 0;
     i < Object.keys(morphTargetDictionary).length;
     i++
) {
     animationData.push([]);
}
// 解析数据
let time = [];
let finishedFrames = 0;
animationList.forEach((item, i) => {
     Object.entries(item).forEach(([key, value]) => {
          if (morphTargetDictionary[key] !== undefined) {
               animationData[morphTargetDictionary[key]].push(value);
          }
     });
     // 时间线
     time.push(finishedFrames / 30);
     finishedFrames++;
});
// 构建动画
let tracks = [];
Object.entries(animationList[0]).forEach(([key, value]) => {
     if (key in morphTargetDictionary) {
          let i = morphTargetDictionary[key];
          let track = new THREE.NumberKeyframeTrack(
               `mesh.morphTargetInfluences[${i}]`,
               time,
               animationData[i]
          );
          tracks.push(track);
     }
});
const clip = new THREE.AnimationClip('animation', -1, tracks);
const animateClipAction = animationMixer.clipAction(clip);
// 播放动画
animateClipAction.play();

这里提供一个animationList 数据给以供测试(注意不同模型morphTargetDictionary的key区别)

F5DD2D01-2E92-467D-9BFA-CE921BC2C43F.json

实用意义

我们通过人工智能大模型,将对话的语音转换为BlendShapes点位数据,就可以实现一个真人对讲的人工智能数字人:

对应开源项目:https://github.com/psyai-net/EmoTalk_release