はじめに
前回は uLipSync で、リップシンクの解析結果のベイク機能の追加とタイムラインのサポートを行いました。
今回は、このベイク結果を元に、ブレンドシェイプを動かすアニメーションへと変換する仕組みを作りました。アニメーションとして保存できれば他のアニメーションとの組み合わせもしやすく、既存のワークフローにも組み込め、また後でキーを動かして調整もしやすくなります。本記事では使い方や仕組みなどを紹介します。
デモ
07. Animation Bake というシーンがサンプルシーンになります。
変換ウィンドウ
変換には、Animator
コンポーネント、どの母音がどのブレンドシェイプに対応しているかセットアップした uLipSyncBlendShape
、そして Bake したデータのリストが必要です。
作成されたアニメーション
サンプリングレートや変化として記録するしきい値が指定できるので、キーを間引いた形のアニメーションも作成できます。
再生
生の解析結果を再生するよりも適当に間引いた結果ローパスがかかったような形になり滑らかになります。
© Unity Technologies Japan/UCL
ダウンロード
v2.1.0 で機能追加を行いました。
使い方
Window > uLipSync > Animation Clip Generator を選択し、uLipSync Animation Clip Generator ウィンドウを開きます。
アニメーションベイクをするには、uLipSync のセットアップを行ったシーンを開いておく必要があります。その上で、シーンにあるコンポーネントをセットしていきます。
- Animator
- シーンに存在する
Animator
コンポーネントを選択します。 - この
Animator
を起点としたアニメーションクリップが作成されます。
- シーンに存在する
- Blend Shape
- Baked Data List
- アニメーションクリップに変換したい
BakedData
アセットを選択します。
- アニメーションクリップに変換したい
- Sample Frame Rate
- キーを打つサンプリングレートを指定します。
- 60 なら 60 フレームで、30 なら 30 フレームでサンプリングを行います。
- Threshold
- ウェイトがこの値だけ変化したときのみキーを打ちます
- ウェイトの最大値は 100 になりますので、10 なら 10% 変化したら、という意味合いになります。
- Output Directory
- ベイクしたアニメーションクリップを出力するディレクトリを指定します。
- 空の場合は Assets 以下に作成します。
セットアップ例です。
Threshold を 0、10、20 と変化させると次のようになります。
仕組み
以下の記事を参考にさせていただきました。
Unity でエディタ拡張からアニメーションを作成するには、次のような手順をたどります。
AnimationClip
を作成EditorCurveBinding
でどの項目にキーを打つかを指定AnimationCurve
にキーを打ち必要に応じて傾きも調整- 2 と 3 をセットにして
AnimationClip
に登録、必要な数だけこれを繰り返す AssetDatabase
を使ってアセットとして保存
となります。具体的に本アセットでアニメーションクリップを作成する流れは以下のような形になります(わかりやすいように一部改変)。
internal class AnimationCurveBindingData { public EditorCurveBinding binding; public AnimationCurve curve; } public class AnimationWizard : ScriptableWizard { ... // 書き込みには / 区切りでルート要素からのオブジェクト名が入る // 例えばユニティちゃんなら Character1_Reference/Character1_Hips/...(略).../MTH_DEF といった具合 // これをルート要素(Animtor)を指定して得るためのメソッド string GetRelativeHierarchyPath(GameObject target, GameObject parent) { if (!target || !parent) return ""; string path = ""; var gameObj = target; while (gameObj && gameObj != parent) { if (!string.IsNullOrEmpty(path)) path = "/" + path; path = gameObj.name + path; gameObj = gameObj.transform.parent.gameObject; } if (gameObj != parent) return ""; return path; } void OnWizardOtherButton() { ... var bindingBase = new EditorCurveBinding(); bindingBase.path = GetRelativeHierarchyPath( blendShape.skinnedMeshRenderer.gameObject, animator.gameObject); bindingBase.type = typeof(SkinnedMeshRenderer); ... foreach (var bakedData in bakedDataList) { ... // 最終的に保存されるアセットとなる // ここにキーを登録したカーブを登録していく var clip = new AnimationClip(); // 各カーブの情報を一時保存しておくリスト var dataList = new Dictionary<int, AnimationCurveBindingData>(); // 登録されているブレンドシェイプ foreach (var bs in blendShape.blendShapes) { if (bs.index < 0) continue; // ブレンドシェイプ毎の EditorCurveBinding を作成 var mesh = blendShape.skinnedMeshRenderer.sharedMesh; var name = mesh.GetBlendShapeName(bs.index); var binding = bindingBase; binding.propertyName = "blendShape." + name; // カーブと組み合わせておく dataList.Add(bs.index, new AnimationCurveBindingData() { binding = binding, curve = new AnimationCurve(), }); } blendShape.OnAnimationBakeStart(); // サンプリング / 間引き用の情報 float dt = 1f / sampleFrameRate; for (float time = 0f; time <= bakedData.duration; time += dt) { // BakedData からブレンドシェイプの値を取得 var frame = bakedData.GetFrame(time); var info = BakedData.GetLipSyncInfo(frame); blendShape.OnAnimationBakeUpdate(info, dt); var weights = blendShape.GetAnimationBakeBlendShapes(); // 登録しておいた各ブレンドシェイプ毎に見ていく // 実際はここに threshold に応じた間引き処理が含まれるので // 興味ある方はコードを覗いてみてください foreach (var kv in dataList) { var index = kv.Key; var curve = kv.Value.curve; curve.AddKey(time, weights[index]); } } blendShape.OnAnimationBakeEnd(); foreach (var kv in dataList) { var data = kv.Value; var binding = data.binding; var curve = data.curve; for (int j = 0; j < curve.length; ++j) { var key = curve[j]; key.weightedMode = WeightedMode.Both; key.inWeight = key.outWeight = 1f / 3f; // 端点や上下ではカーブの傾きは 0 にする if (j == 0 || j == curve.length - 1 || key.value < 1f || key.value > 99f) { key.inTangent = key.outTangent = 0f; } else { var prevKey = curve[j - 1]; var nextKey = curve[j + 1]; // 山や谷となっている点の傾きは 0 にする if ((key.value - prevKey.value) * (nextKey.value - key.value) < 0f) { key.inTangent = key.outTangent = 0f; } // それ以外は前後のキーを見て傾きを求める else { var a = (nextKey.value - prevKey.value) / (nextKey.time - prevKey.time); key.inTangent = key.outTangent = a; } } curve.MoveKey(j, key); } // 作成したカーブを指定したバインディング(どのブレンドシェイプか)と紐付けて // アニメーションクリップに登録する AnimationUtility.SetEditorCurve(clip, data.binding, data.curve); } // アセットとして保存 var path = $"{outputDirectory}/{bakedData.name}.anim"; path = AssetDatabase.GenerateUniqueAssetPath(path); AssetDatabase.CreateAsset(clip, path); } ... AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } ... }
おわりに
これで一通り考えていた機能がとりあえず揃いました。あとは MFCC の類似度判定部分の改善や、併せてキャリブなしでもそれなりに動くプロファイル作成などを行いたいですが、またしばらくして気力が出てきたらやろうかな、と思います。