※本記事は、NorthDetail Advent Calendar 2020の一環として投稿しています
トップVTuber間違いなし😉
・MacBook Pro (Retina, 15-inch, Mid 2014) : Catalina 10.15.7
・Unity : 2019.4.16f1
・google/MediaPipe : v0.8.0
・MediaPipeUnityPlugin : v0.2.1
※2020/12/19現在 本記事の内容はLinux, macOS, Androidのみ対応
また、GPU対応はLinux, Androidのみで、MacではCPUモードのみなので注意
詳しくは上記プラグインの「Platforms」を参照
以下のような技術で、深度カメラ搭載デバイス(iPhone Xなど)向けにビルドや、
デバイスからUnityに顔データを送信する方法などがあります
・ARKit Face Tracking
・Facial AR Remote
・・・が、実装やテストが結構大変です。。。
Unity単体で簡単に実装できるAssetなどもあります
・OpenCV for Unity : $95
・Dlib FaceLandmark Detector : $40
・CV VTuber Example : 無料 (別途 上の2アセットが必要)
実際に動かすとこんな感じ
© Unity Technologies Japan/UCL
OpenCV、DlibをUnityで使えるようにしたもので、画像認識とかいろいろできる
3つのアセットを入れるだけで簡単にユニティちゃんを動かせます
・・・が、値段がめちゃくちゃ高い。。。(私はバンドルセールで購入しました)
そして動作が結構重かったり、横顔を認識できなかったり…
視線検出にも対応していないため、カメラ目線固定などの対応となります
(Pythonで実現している人はいるので頑張ればできそう?)
言わずと知れたゲームエンジン
ダウンロードページからUnity Hubをダウンロード
2019.4.10f1以上をインストール
(2020では動かないかもしれないので2019の近いバージョンがよいかも)
Googleが開発している機械学習パイプライン構築フレームワーク
顔認識や物体の識別、ハンドトラッキングとかいろいろできる
全体的に精度が高く、フェイストラッキングでは視線の検出までできる
昨年のアドカレでも記事にしているので興味ある方は是非↓
・スマホカメラで手のモーションを記録してUnityでピアノ演奏したかった
昨年に比べて、CPUでもサクサク動いたり、WEBブラウザで実行できたり進化してる
でもまだまだ開発途上
インストールページの手順に従ってセットアップ
おそらくエラー連発で手順通りに進まないと思うので
各種バージョンを合わせたり、追加でnumpyをインストールしたりで解決しました
バージョンは古いですが昨年の記事も参考になるかも
MediaPipeをUnityで実行できるようにする神プラグイン
日本人の方が作成しているっぽい、マジ感謝 (。-人-。)
以下をインストール
・MediaPipe
・OpenCV
・.NET Core
上2つは前述の手順でインストールされているはず
.NET Core は Visual Studio for Mac をインストールしていれば自動で入っています
手動で入れる場合はこちら↓
https://dotnet.microsoft.com/download
$ git clone https://github.com/homuler/MediaPipeUnityPlugin.git
$ cd MediaPipeUnityPlugin
$ make cpu
# ---------------------
# Linuxの場合はGPUモードも可
$ make gpu
# Androidで動かしたい場合
$ make android_arm
# ライブラリ・モデルファイルをアセットフォルダに配置してくれる
$ make install
これで準備完了
UnityでMediaPipeが実行できました!
3Dモデルを動かすには、
などの方法があります
VTuberアプリなどはこれのあわせ技で、
口の動きはメッシュ、まばたきはアニメーションで表現したりします
今回は2の「メッシュを動かす」のみで実装します
クローンしたMediaPipeリポジトリに3Dモデルが用意されています
mediapipe/mediapipe/modules/face_geometry/data/canonical_face_model.obj
Blenderで開くとこんな感じ
468個の頂点で構成されたMeshになっています
先程Unityで実行した際の点 (Landmark) の数が478個でした
これは顔468個に加えて、左目5個、右目5個で構成されています
まずは顔のLandmarkに対応したMeshの頂点を同期させて動かします
UnityのProjectウィンドウにファイルをドラッグ&ドロップ
さらにProjectウィンドウからHierarchyウィンドウにドラッグ&ドロップ
Unityは3Dオブジェクトを読み込むと自動でMeshが最適化されます
これにより、頂点の数が増えたり順番がバラバラになったりするため設定を変更します
Projectウィンドウからオブジェクトを選択し、
InspectorウィンドウのModelタブから以下を修正
以下のファイルを修正していきます
Assets/MediaPipe/Examples/Scripts/IrisTracking/IrisTrackingAnnotationController.cs
namespace Mediapipe {
public class IrisTrackingAnnotationController : AnnotationController {
[SerializeField] GameObject irisPrefab = null;
[SerializeField] GameObject faceLandmarkListPrefab = null;
[SerializeField] GameObject faceRectPrefab = null;
[SerializeField] GameObject faceDetectionsPrefab = null;
/* ------------------- 追加 ------------------- */
private MeshFilter meshFilter; // オブジェクトのMeshFilter
private Mesh faceMesh; // オブジェクトのMesh
private List<Vector3> vertextList = new List<Vector3>(); // Meshの頂点の座標リスト
/* ------------------------------------------- */
/* ~ 省略 ~ */
void Awake() {
leftIrisAnnotation = Instantiate(irisPrefab);
rightIrisAnnotation = Instantiate(irisPrefab);
faceLandmarkListAnnotation = Instantiate(faceLandmarkListPrefab);
faceRectAnnotation = Instantiate(faceRectPrefab);
faceDetectionsAnnotation = Instantiate(faceDetectionsPrefab);
/* ------------------- 追加 ------------------- */
meshFilter = GameObject.Find("default").GetComponent<MeshFilter>(); // defaultオブジェクトからMeshFilterを取得
faceMesh = meshFilter.mesh; // Meshをセット
vertextList.AddRange(faceMesh.vertices); // Meshから頂点座標リストを取得
/* ------------------------------------------- */
/* ~ 省略 ~ */
public void Draw(Transform screenTransform, NormalizedLandmarkList landmarkList,
NormalizedRect faceRect, List<Detection> faceDetections, bool isFlipped = false)
{
if (landmarkList == null) {
Clear();
return;
}
UpdateFaceMesh(landmarkList); // 追加
/* ~ 省略 ~ */
/* ------------------- 追加 ------------------- */
private int meshScale = -5; // サイズ調整用の変数
private void UpdateFaceMesh(NormalizedLandmarkList landmarkList) {
// 顔の頂点分だけ実行(478 - 10 = 468)
for (var i = 0; i < landmarkList.Landmark.Count - 10; i++)
{
var landmark = landmarkList.Landmark[i];
// 検出したLandmarkをMeshの頂点座標にセット
vertextList[i] = new Vector3(meshScale*landmark.X, meshScale*landmark.Y, meshScale*landmark.Z);
}
// 座標リストをMeshに適用
faceMesh.SetVertices(vertextList);
}
/* ------------------------------------------- */
これで[▶]実行すると
見事3Dモデルを動かすことができました!
球体オブジェクトの目を用意して、傾きを計算して回転させるのが理想なのですが
大変なので同じくメッシュで実装していきます
こんな感じ↓
先程のコードを修正します
private MeshFilter meshFilter;
private Mesh faceMesh;
private List<Vector3> vertextList = new List<Vector3>();
/* ------------------- 追加 ------------------- */
private MeshFilter leftEyeMeshFilter;
private Mesh leftEyeMesh;
private List<Vector3> leftEyeVertextList = new List<Vector3>();
private List<Vector2> leftEyeUvList = new List<Vector2>();
private List<int> leftEyeIndexList = new List<int>();
private MeshFilter rightEyeMeshFilter;
private Mesh rightEyeMesh;
private List<Vector3> rightEyeVertextList = new List<Vector3>();
private List<Vector2> rightEyeUvList = new List<Vector2>();
private List<int> rightEyeIndexList = new List<int>();
/* ------------------------------------------- */
/* ~ 省略 ~ */
void Awake() {
/* ~ 省略 ~ */
meshFilter = GameObject.Find("default").GetComponent<MeshFilter>();
faceMesh = meshFilter.mesh;
vertextList.AddRange(faceMesh.vertices);
/* ------------------- 追加 ------------------- */
leftEyeMeshFilter = GameObject.Find("LeftEye").GetComponent<MeshFilter>();
leftEyeMesh = CreateEyeMesh(leftEyeVertextList, leftEyeUvList, leftEyeIndexList, true);
leftEyeMeshFilter.mesh = leftEyeMesh;
rightEyeMeshFilter = GameObject.Find("RightEye").GetComponent<MeshFilter>();
rightEyeMesh = CreateEyeMesh(rightEyeVertextList, rightEyeUvList, rightEyeIndexList, false);
rightEyeMeshFilter.mesh = rightEyeMesh;
/* ------------------------------------------- */
/* ~ 省略 ~ */
private void UpdateFaceMesh(NormalizedLandmarkList landmarkList) {
for (var i = 0; i < landmarkList.Landmark.Count - 10; i++)
{
var landmark = landmarkList.Landmark[i];
vertextList[i] = new Vector3(meshScale*landmark.X, meshScale*landmark.Y, meshScale*landmark.Z);
}
faceMesh.SetVertices(vertextList);
/* ------------------- 追加 ------------------- */
for (var i = landmarkList.Landmark.Count - 9; i < landmarkList.Landmark.Count - 5; i++)
{
var landmark = landmarkList.Landmark[i];
leftEyeVertextList[i-469] = new Vector3(meshScale*landmark.X, meshScale*landmark.Y, meshScale*landmark.Z);
}
leftEyeMesh.SetVertices(leftEyeVertextList);
for (var i = landmarkList.Landmark.Count - 4; i < landmarkList.Landmark.Count; i++)
{
var landmark = landmarkList.Landmark[i];
rightEyeVertextList[i-474] = new Vector3(meshScale*landmark.X, meshScale*landmark.Y, meshScale*landmark.Z);
}
rightEyeMesh.SetVertices(rightEyeVertextList);
/* ------------------------------------------- */
}
/* ------------------- 追加 ------------------- */
private Mesh CreateEyeMesh(List<Vector3> eyeVertextList, List<Vector2> eyeUvList, List<int> eyeIndexList, bool isLeftEye)
{
var mesh = new Mesh();
eyeVertextList.Add(new Vector3(1, 0, 0));
eyeVertextList.Add(new Vector3(0, -1, 0));
eyeVertextList.Add(new Vector3(-1, 0, 0));
eyeVertextList.Add(new Vector3(0, 1, 0));
eyeUvList.Add(new Vector2(1f, 0.5f));
eyeUvList.Add(new Vector2(0.5f, 1f));
eyeUvList.Add(new Vector2(0f, 0.5f));
eyeUvList.Add(new Vector2(0.5f, 0f));
if(isLeftEye){
eyeIndexList.AddRange(new []{0,3,1,1,3,2});
} else {
eyeIndexList.AddRange(new []{2,3,1,1,3,0});
}
mesh.SetVertices(eyeVertextList);
mesh.SetUVs(0,eyeUvList);
mesh.SetIndices(eyeIndexList.ToArray(),MeshTopology.Triangles, 0);
return mesh;
}
/* ------------------------------------------- */
これで[▶]実行すると
視線に合わせて目を動かすことができました!
MediaPipe 昨年に比べてだいぶ進化していました
去年はCPUモードではフリーズして使い物になりませんでしたから……
Unityで利用できるプラグインまで作成され、
実用レベルもかなり上がって来ています
GPUモードでの実行やUnityとの連携など、更なる発展に期待が高まります
来年のアドベントカレンダーが楽しみですね〜笑