ADX LipSync应用/Unity篇

LipSync说明

LipSync是最新推出的,针对口型匹配的新解决方案。能够实时以及预先分析声音素材,得到声音素材所包含的信息后,将其运用于模型上,通过变更模型的变形效果,综合控制口型的变化,以达到口型和语音匹配的目的。

LipSync主要有两种应用模式,实时解析和预先分析。

  1. 实时解析可以通过话筒录入实时分析录入的声音内容,进行分析最终得到相关的口型数据,进行模型变形匹配声音口型。
  2. 预先分析是通过将已经录音处理好的wav文件进行分析,得到相关的分析数据,当播放相关的wav文件时,可以使用事先分析好的数据进行模型的变形以匹配声音口型。

LipSync也拥有两种应用方法,单独使用和与ADX2联合使用

  1. 单独使用是LipSync单独应用,直接使用LipSync进行声音分析,最终得到口型数据和效果。
  2. 与ADX2联合使用,这种方式LipSync作为ADX2的插件使用,能够分析ADX2中Cue中Track轨道上的波形文件,生成相应的声音数据。分析相应的数据也可以事先分析或者实时分析,这种应用方式称为ADX LipSync也是本文中介绍的重点内容。
模型数据创建指南

由于口型同步需要声音数据和模型的变形效果,因此模型的变形效果制作的好坏也是决定口型同步效果的一大因素。

LipSync主要通过声音数据,驱动模型的变形效果,综合的变更各个模型变形的参数来集体的把控口型的变化自然和效果。

在模型的数据创建中,我们通过使用3D Max或者3D Maya等3d模型工具,创建相关的变形数据如图:

可以从上图看到,我们创建了相关变形器,并且添加了相关的变形数据,当我们变更变形数据的数值时就能看到变形效果:

这样一来我们的模型变形效果就处理完成,可以通过声音数据驱动变形效果的数值发生变化,同步口型。

ADX LipSync

ADX LipSync是将LipSync结合在ADX2中进行使用的方式,这种方式主要分为两种,预先分析和实时分析。本文会介绍如何在ADX2中进行预先分析,同时使用分析的结果在Unity中同步口型。

ADX LipSync的预先分析

当使用ADX LipSync时,将素材拖入到Track后,就能够看到声音文件对应的口型数据分析结果。

如上图所示,右下角显示的就是分析后的结果。

分析数据

经过LipSync的分析后,主要产出两种分析数据

  1. 宽度高度模式,这种分析结果数据,对应于口型变化的宽度和高度结果,通过对声音进行分析,得到对应声音时刻变化的宽度和高度曲线,同时可以在ADX2中进行高度和宽度曲线的调整,以应对自动分析不满意的效果。
  2. 音素混合量分析模式,这种分析结果数据是分析语音中各个音素量的占比,通过占比决定各个音素口型的权重从而动态的变化模型变形效果,同样如果对于自动分析结果并不满意,则可以手动修改。

通过这两种数据,我们可以变更口型变化效果,来实现口型同步。

注意:

  1. 对于宽度和高度模式,没有任何语言限制,所有语种都可以使用,但是解析精度相对低。
  2. 对于因素量混合分析模式,和语种相关,目前提供日语的AEUIO几个元音进行解析,因此对日语的匹配度较高。
ADX LipSync在Unity中的应用

ADX LipSync在Unity中库可以直接进行集成后添加,集成方式与ADX2集成在Unity中相同,实际上在新版的SDK中,ADX LipSync的库就包含在集成包中,直接将package包导入Unity后即可完成

 模型场景添加

为了测试我们口型同步的效果,首先要将模型添加到场景中。我们直接拖拽相关模型到场景中即可。

拖拽的模型包含我们之前所述创建了相关的变形参数,可以在Inspector面板中看到相关参数内容:

我们可以更改blendShape看到参数变化对模型变形的效果影响。

声音信息创建

我们需要创建相关声音信息用于调用和播放,因此创建一个空的object并且添加Cri Atom组件:

添加完成后填写相关acf,acb以及awb文件信息以备使用。

控制脚本创建

准备好我们的声音和模型信息后,我们创建相关控制脚本内容。

首先在场景中创建一个空的object并且命名为ModelController。在其上添加Cri Atom Source脚本组件,Cri Lips Shape For Atom Source脚本组件,以及创建一个新的脚本:

其中Cri Atom Source脚本组件和Cri Lips Shape For Atom Source脚本组件通过Add Component添加,而Scene_02_Adx Lip Sync直接进行创建脚本。

完成上述内容后,我们在Cri Lips Shape For Atom Source脚本组件中将之前创建的ModelController组件拖拽到Cri Atom Source,将场景模型组件拖拽到SkinnedMeshRenderer中:

如此一来将声音信息和模型信息添加到相关脚本中。

而后我们更改BlendShapeType,对应ADX LipSync的高度宽度模式和音素混合量分析模式,并调整每个模式下数据对应的模型变形数据:

这样一来我们就能够根据实时分析的声音数据来控制模型变形。

完成后我们将开始抒写相关的自定义脚本代码:Scene_02_Adx Lip Sync,代码如下:

using UnityEngine;
using System.Collections;
using System;

public class Scene_02_AdxLipSync : MonoBehaviour {
	#region Variables
	[SerializeField]
	private string cueSheetName = "";
	[SerializeField]
	private CriAtomSource atomSource = null;
	[SerializeField]
	private CriLipsShapeForAtomSource shapeForAtomSource = null;
	private CriAtomExAcb acb = null;
	private CriAtomEx.CueInfo[] cueInfos;

	private int selectedCueIndex = 0;
	const float uiMargin = 10f;
	#endregion

	#region Functions
	private void Start()
    {
		if (!String.IsNullOrEmpty(cueSheetName)) {
			acb = CriAtom.GetAcb(cueSheetName);
			cueInfos = acb.GetCueInfoList();
			atomSource.player.SetCue(acb, cueInfos[selectedCueIndex].name);
		}
	}

	void OnGUI()
    {
#if UNITY_WEBGL
		if (CriAtom.CueSheetsAreLoading) {
			return;
		}
#endif

		if (Scene_00_SampleList.ShowList == true) {
			return;
		}

		Scene_00_GUI.BeginGui("01/SampleMain");

		GUI.skin = Scene_00_SampleList.uiSkin;
		float positionX = 0.0f;
		float positionY = 0.0f;
		float buttonRectWidth = 420;
		float buttonRectHeight = 100;

		positionX = Scene_00_GUI.screenX - uiMargin - buttonRectWidth;
		positionY = Scene_00_GUI.screenY - uiMargin - buttonRectHeight;
        // 界面设置音频混合模式,宽度高度模式或者音素混合量模式
		if (Scene_00_GUI.Button(new Rect(positionX, positionY, buttonRectWidth, buttonRectHeight), "BlendShapeType : " + shapeForAtomSource.blendShapeType.ToString())) {
			switch (shapeForAtomSource.blendShapeType)
            {
				case CriLipsShape.BlendShapeType.WidthHeight:
					shapeForAtomSource.blendShapeType = CriLipsShape.BlendShapeType.JapaneseAIUEO;
					SetBlendShapeWidthHeightAtSilence(shapeForAtomSource);
					break;
				case CriLipsShape.BlendShapeType.JapaneseAIUEO:
					shapeForAtomSource.blendShapeType = CriLipsShape.BlendShapeType.WidthHeight;
					SetBlendShapeJapaneseAIUEOAtSilence(shapeForAtomSource);
					break;
				default:
					break;
			}
            
		}
        // 停止和开始播放Cue
		positionY -= buttonRectHeight + uiMargin;
		if (Scene_00_GUI.Button(new Rect(positionX, positionY, buttonRectWidth / 2 - uiMargin, buttonRectHeight), "Play"))
        {
			if (atomSource.status == CriAtomSource.Status.Playing) {
				atomSource.player.Stop();
			}
			atomSource.player.Start();
		}
		if (Scene_00_GUI.Button(new Rect(positionX + (buttonRectWidth / 2) + uiMargin, positionY, buttonRectWidth / 2 - uiMargin, buttonRectHeight), "Stop"))
        {
			atomSource.player.Stop();
		}
        // 控制Cue的播放内容,用于切换Cue ID
		positionY -= buttonRectHeight + uiMargin;
		if (Scene_00_GUI.Button(new Rect(positionX, positionY, buttonRectWidth / 4 - uiMargin, buttonRectHeight), "<"))
        {
			selectedCueIndex--;
			if (selectedCueIndex < 0) {
				selectedCueIndex = cueInfos.Length - 1;
			}
			atomSource.player.SetCue(acb, cueInfos[selectedCueIndex].name);
		}
		positionX += buttonRectWidth / 4;
		if (Scene_00_GUI.Button(new Rect(positionX, positionY, buttonRectWidth / 2, buttonRectHeight), cueInfos[selectedCueIndex].name)) {
		}
		positionX += buttonRectWidth / 2 + uiMargin;
		if (Scene_00_GUI.Button(new Rect(positionX, positionY, buttonRectWidth / 4 - uiMargin, buttonRectHeight), ">")) {
			selectedCueIndex++;
			if (selectedCueIndex >= cueInfos.Length) {
				selectedCueIndex = 0;
			}
			atomSource.player.SetCue(acb, cueInfos[selectedCueIndex].name);
		}

		Scene_00_GUI.EndGui();
	}

	private void SetBlendShapeWidthHeightAtSilence(CriLipsShapeForAtomSource lipsShapeForAtomSource) {
		BlendShapeWeightString(lipsShapeForAtomSource.skinnedMeshRenderer, lipsShapeForAtomSource.nameMapping.WidthHeightName.lipHeightOpenName, 0.0f);
		BlendShapeWeightString(lipsShapeForAtomSource.skinnedMeshRenderer, lipsShapeForAtomSource.nameMapping.WidthHeightName.lipWidthOpenName, 0.0f);
		BlendShapeWeightString(lipsShapeForAtomSource.skinnedMeshRenderer, lipsShapeForAtomSource.nameMapping.WidthHeightName.lipWidthCloseName, 0.0f);
		BlendShapeWeightString(lipsShapeForAtomSource.skinnedMeshRenderer, lipsShapeForAtomSource.nameMapping.WidthHeightName.tonguePosition, 0.0f);
	}

	private void SetBlendShapeJapaneseAIUEOAtSilence(CriLipsShapeForAtomSource lipsShapeForAtomSource) {
		BlendShapeWeightString(lipsShapeForAtomSource.skinnedMeshRenderer, lipsShapeForAtomSource.nameMapping.japaneseAIUEOName.a, 0.0f);
		BlendShapeWeightString(lipsShapeForAtomSource.skinnedMeshRenderer, lipsShapeForAtomSource.nameMapping.japaneseAIUEOName.i, 0.0f);
		BlendShapeWeightString(lipsShapeForAtomSource.skinnedMeshRenderer, lipsShapeForAtomSource.nameMapping.japaneseAIUEOName.u, 0.0f);
		BlendShapeWeightString(lipsShapeForAtomSource.skinnedMeshRenderer, lipsShapeForAtomSource.nameMapping.japaneseAIUEOName.e, 0.0f);
		BlendShapeWeightString(lipsShapeForAtomSource.skinnedMeshRenderer, lipsShapeForAtomSource.nameMapping.japaneseAIUEOName.o, 0.0f);
	}

	private void BlendShapeWeightString(SkinnedMeshRenderer skinnedMeshRenderer, string blendShapeName, float weight)
    {
		if (string.IsNullOrEmpty(blendShapeName))
        {
			return;
		}
		int index = skinnedMeshRenderer.sharedMesh.GetBlendShapeIndex(blendShapeName);
		if (index < 0)
        {
			return;
		}
		skinnedMeshRenderer.SetBlendShapeWeight(index, weight);
	}
	#endregion
}

上面脚本中我们主要进行以下操作:

  1. 调用acb,并且从acb文件中提取了cueInfos,同时使用了cueInfos中的Cue Index进行Cue的切换和播放。
  2. 通过创建GUI界面,随时更改口型数据分析模式(宽度高度模式和音素混合量分析模式)
  3. 当切换模式时,将口型数据的所有变形效果同时规整为0

当创建完成脚本并添加于object上后,将我们需要的组件拖拽到脚本上:

同时我们需要手动填写Cue Sheet Name,因为我们需要从Cue Sheet也就是acb文件中获取cue index用于切换,播放以及停止。同时需要其中的相关信息更改模型的变形效果参数。

完成上述内容后,我们运行场景,就可以看到相关信息显示,并且点击播放后就能看到口型对应。

Ringo

CRI Middleware Global Expansion Group

One thought on “ADX LipSync应用/Unity篇

  • 2021-07-02 at 11:55
    Permalink

    请问如文中展示,在CriAtomCraft中使用LipSync进行口型数据分析,并对分析数据手动修改,这个环境是怎么搭建的吗?

    Reply

发表评论

邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据