39
29

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

【Unity】Face Tracking with ARKit + Visual Effect Graph(VFX Graph)で遊んでみる

Last updated at Posted at 2019-04-22

はじめに

ふと表題にもあるFace Tracking with ARKitとVFX Graphを組み合わせて何かエモいことができないかと思い、以下の様なデモを作ってみました。 STGのラスボスに使えそう感。

56461455-705cb180-63ee-11e9-97bb-0f8c157beeaf.GIF

シンプルな演出ではありますが、上記の実装でやっている事を簡単に纏めるとARKitから取得したFaceGeometryをVFX Graph側で読み込んで頂点位置にパーティクルを発生させている感じです。

演出と言うよりは内部実装メインのお話にはなりますが、今回はこちらを実装した際の技術解説について簡単に纏めてみようと思います。

サンプルプロジェクトについて

サンプルプロジェクトはGitHubにて公開済みです。

mao-test-h/FaceTracking-VFX

※注意点 : VFX Graph自体はまだPreviewなので将来的に変更が入る可能性があります。ご了承ください。

シーンの方はAssets/FaceTracking-VFX/Scenes/FaceTracking-VFX.unityを御覧ください。
今回はこちらを主軸に解説していきます。

ARKitについて

今回Unity上でARKitを使うにあたっては、「Unity-ARKit-Plugin」と言う公式で用意されているパッケージを利用することにしました。
※一応最新版辺りであれば、PackageManagerからARKit XR Pluginと言うパッケージを取得して利用する事が出来るみたいですが...こちらについては情報を調べきれていないので今回は敢えて未使用。

リモート機能について

後は前提としてFace Tracking with ARKitを使用しているので、動かすにはiPhoneXに相応するスペックを有するiOS端末が必要となります。
その上で端末のカメラやセンサーなどを使うので動作確認するなら必然的に実機上で動かす必要が出てきますが、値を調整する度にいちいちビルドしてトライアンドエラーを行うのは非常にイテレーションが悪くなります。

その為に今回は「Unity-ARKit-Plugin」に含まれるリモート機能を用いた上で値の調整を行いました。
覚えておくと便利な機能ではあるので簡単に触れておきます。

実行手順

実行手順及び詳細についてはこちらの公式ブログにほぼ載ってます。
一応手順を記載。

※ちなみにリモート機能自体は以下の1~2の手順で作成するリモート用アプリのビルド/インストールまで対応出来れば、後はiOS端末をWindowsマシンに繋げる形でリモート実行することも可能。

    1. EditorのBuildSettingsを開いて以下の設定を行う。
    • PlatformをiOSに設定。
    • 既に登録されているであろうFaceTracking-VFX.unityをビルドに含めないようにチェックを外した上で、新規にAssets/UnityARKitPlugin/ARKitRemote/UnityARKitRemote.unityをビルド対象に含める。
    • Development Buildにチェックを入れる。
    1. ビルドして実機に入れる。
    • ※アプリのIDが競合しないようにビルドする際には予めBundleIdに.remoteと言う文字列を含めるなりして既存の物とは別物として管理しておくと良いかもしれない。
    1. FaceTracking-VFX.unityを開く。
    1. 上記のビルドを入れたiOS端末とPCをLightningケーブル等で接続してリモートアプリを実行。
    1. Editor上でConsole Windowの検索ボックスの左にある項目を選択 → 接続先を4の手順で繋げたiOS端末に変更。
    1. Editor上でFaceTracking-VFX.unityを実行。
    1. 画面中に「Start ARKit Face Tracking Session」と記載されたボタンが表示されているはずなので押下。成功するとリモート接続される。

ちなみにリモート接続時はプラグイン側によるカメラ向き調整の影響か、 縦持ちだとカメラがZ軸に90℃回転している様に見受けられました。 1

これが影響してくる点で言えば、例えば以下の様にVFX Graphでパーティクルに重力をかけて下に落下していくような処理を入れたとすると、リモート時にはパーティクルが横に動いてしまうと言ったことが発生します。

sample_.gif

今回は大雑把な動きを見られればよかったので特に対応はしておりませんが、必要であればリモート時には何かしらの補正を入れてみても良いかもしれません。

実装について

実装について解説していきます。
先に要点だけ簡潔に纏めておくと全体の流れとしては以下の手順となります。

  1. Unity-ARKit-PluginからFaceGeometryを取得。(主に使うのは頂点の位置)
  2. ↑で得たFaceGeometryの頂点情報をRenderTextureに書き込み。
  3. VFX Graph側ではTextureに書き込まれたFaceGeometryの情報をSet From Position Mapと言うBlockを経由して取得。得た座標情報を元にパーティクルを表示。

後は前提として上記「2」の手順ではComputeShaderを利用しているので、こちらの知識が多少必要となるかもしれません。。2
こちらの記事中では詳細な解説については割愛しますが、以下に参考リンクの幾つかを載せておきます。

後は少し古い記事となりますが、ComputeShaderの基礎知識については以下の記事等も分かりやすいかと思われます。

FaceGeometryの取得

FaceGeometryの取得についてはUnity-ARKit-Pluginの中に「顔の頂点情報などを取得して動的にMeshを構築して表示する」サンプルがあったので、今回はこちらをベースにして実装の方を進めました。
※サンプルのシーンはAssets/UnityARKitPlugin/Examples/FaceTracking/FaceMeshScene.unityが該当。

実行すると以下のようにシンプルな顔のメッシュが表示されるかと思われます。(シーン自体はリモート実行可能)

ss1.png

顔のMeshを構築する処理についてはシーン中にあるARFaceMeshManagerと言うGameObjectが担っており、以下のように同じGameObjectに所属しているMeshRendererに対しプラグイン側から動的に頂点情報を流し込んで構築しているように見受けられました。

Art001.png

処理の方もそんなに複雑ではなく、プラグインが持つARFaceAnchor***Eventと言うイベントに更新したいメソッドを登録し、後はイベント経由で流れてくるARFaceAnchorの値を元にMeshを構築しているみたいです。
以下にコードの一部を載せておきます。

UnityARFaceMeshManager.cs

    // Use this for initialization
    void Start()
    {
        m_session = UnityARSessionNativeInterface.GetARSessionNativeInterface();

        Application.targetFrameRate = 60;
        ARKitFaceTrackingConfiguration config = new ARKitFaceTrackingConfiguration();
        config.alignment = UnityARAlignment.UnityARAlignmentGravity;
        config.enableLightEstimation = true;

        if (config.IsSupported && meshFilter != null)
        {
            m_session.RunWithConfig(config);

            UnityARSessionNativeInterface.ARFaceAnchorAddedEvent += FaceAdded;
            UnityARSessionNativeInterface.ARFaceAnchorUpdatedEvent += FaceUpdated;
            UnityARSessionNativeInterface.ARFaceAnchorRemovedEvent += FaceRemoved;
        }
    }

    void FaceAdded(ARFaceAnchor anchorData)
    {
        gameObject.transform.localPosition = UnityARMatrixOps.GetPosition(anchorData.transform);
        gameObject.transform.localRotation = UnityARMatrixOps.GetRotation(anchorData.transform);

        faceMesh = new Mesh();
        faceMesh.vertices = anchorData.faceGeometry.vertices;
        faceMesh.uv = anchorData.faceGeometry.textureCoordinates;
        faceMesh.triangles = anchorData.faceGeometry.triangleIndices;

        // Assign the mesh object and update it.
        faceMesh.RecalculateBounds();
        faceMesh.RecalculateNormals();
        meshFilter.mesh = faceMesh;
    }

    void FaceUpdated(ARFaceAnchor anchorData)
    {
        if (faceMesh != null)
        {
            gameObject.transform.localPosition = UnityARMatrixOps.GetPosition(anchorData.transform);
            gameObject.transform.localRotation = UnityARMatrixOps.GetRotation(anchorData.transform);
            faceMesh.vertices = anchorData.faceGeometry.vertices;
            faceMesh.uv = anchorData.faceGeometry.textureCoordinates;
            faceMesh.triangles = anchorData.faceGeometry.triangleIndices;
            faceMesh.RecalculateBounds();
            faceMesh.RecalculateNormals();
        }
    }

    void FaceRemoved(ARFaceAnchor anchorData)
    {
        meshFilter.mesh = null;
        faceMesh = null;
    }

纏めると、今回のアプローチとしては上記のイベント経由でARFaceAnchorを取得し、そこから得られた情報を元にして後述のComputeShader経由でTextureに頂点情報を書き込むと言った流れとなります。

Mesh情報のTextureへの書き込み

次に表題のTextureへの書き込みについて解説していきます。

VFX GraphでMeshの情報を参照するには

そもそもとして「何故Meshの情報をTextureに書き込まなくてはならないのか?」について簡単に触れておくと、VFX Graph自体はMeshの形状に沿ったパーティクルを出すと言った事は出来るものの、データとしてMeshを直接参照することは出来ないので、一度ポイントキャッシュという形でデータを出力して読み込ませる必要があります。

その為に「静的なMesh」に対してならVFX Graph側の機能として予め「Meshからポイントキャッシュ形式にデータを出力するツール3」が付属されているので、こちらを用いることで事前に変換して読み込ませる事が可能となりますが、SkinnedMeshRendererと言った動的に変形するMeshについては対応しておらず、仮に使うとした場合には何かしらの工夫が必要となるように思われました。

ポイントキャッシュについて

ポイントキャッシュがVFX Graph側でどう読み込まれているのかについても解説しておきます。
生成されたポイントキャッシュはVFX Graph上でPoint Cacheと言うノードを経由して読み込むことができ、Attribute Mapと言う名のTexture形式として出力することが可能です。(※正確に言うとTextureにポイントキャッシュの情報を焼き込んでいるとのこと。)

その上で出力されたAttribute MapはSet Position from Mapと言ったノードを介して読み込むことでパーティクルに適用する事が可能となります。

ここらのお話については以前開催された「Unity道場」と言うイベントにて「Visual Effect GraphとRealSenseを組み合わせると 簡単に派手なことができてすごく面白いという話」と言う講演があり、こちらの方で具体的に解説されております。

その時の講演資料の模式図の一部を引用すると以下のような流れとなります。

aaa.jpg

ポイントキャッシュ自体は上記の様に読み込まれる際の形式としてはTextureとなっているので、今回の実装のアプローチとしては「Meshの情報を動的にTexture(Attribute Map)に書き込むことでVFX Graph側で参照する事が出来ないか?」を検証してみた物となります。

Textureへの書き込みについて

当初は上述の「Meshからポイントキャッシュ形式にデータを出力するツール」を参考に処理を書いていこうかと思いましたが、調べてみると既にkeijiroさんがSkinnedMeshRendererの情報を動的にTextureに書き込んでVFX Graph側で参照する「Smrvfx」と言うリポジトリを公開されていたので、こちらの方を参考にさせて頂きました。
内容としては、まさに今回やろうとしていることそのものです。

Smrvfxの内容を簡単に纏めると、「SkinnedMeshRendererからBakeMeshと言うメソッドを呼び出してMeshを生成」 → 「頂点/法線/モーションベクターと言った情報をComputeShaderでTextureに書き込み」 → 「VFX Graphで参照」と言った流れとなっているように見受けられました。

こちらもやろうとしている事はほぼ同様のものであり、纏めると以下の手順となります。

  1. Unity-ARKit-PluginからFaceGeometryの情報を取得
  2. 取得したFaceGeometryの頂点情報をComputeShaderでTextureに書き込み
  3. 書き込んだTextureをVFX Graphで参照。

順に詳細について解説していきます。

1. FaceGeometryの取得

前述のUnityARFaceMeshManagerを参考に以下のスクリプトを実装しました。
今回はこちら経由で頂点情報を流していってます。

処理自体は参考にしたUnityARFaceMeshManagerとほぼ変わらないものであり、違いを上げるとしたらMeshを生成せずに直接頂点などの情報をComputeShaderに流している点などが上げられます。
その他の補足についてはコード中にコメントを幾つか残してあるので、詳細についてはそちらを御覧ください。

ARFaceMeshBaker.cs
    sealed class ARFaceMeshBaker : MonoBehaviour
    {
        [SerializeField] RenderTexture _positionMap = default;
        [SerializeField] ComputeShader _vertexBaker = default;

        UnityARSessionNativeInterface _session;

        ComputeBuffer _positionBuffer;
        RenderTexture _tmpPositionMap;
        int _vertexCountID, _transformID, _positionBufferID, _positionMapID;

        void Start()
        {
            // ComputeShader側に渡す各種パラメータ
            _vertexCountID = Shader.PropertyToID("VertexCount");
            _transformID = Shader.PropertyToID("Transform");
            _positionBufferID = Shader.PropertyToID("PositionBuffer");
            _positionMapID = Shader.PropertyToID("PositionMap");

            // 以下はARKit周りの初期化
            _session = UnityARSessionNativeInterface.GetARSessionNativeInterface();

            Application.targetFrameRate = 60;
            var config = new ARKitFaceTrackingConfiguration();
            config.alignment = UnityARAlignment.UnityARAlignmentGravity;
            config.enableLightEstimation = true;

            if (config.IsSupported)
            {
                _session.RunWithConfig(config);
                UnityARSessionNativeInterface.ARFaceAnchorAddedEvent += FaceAdded;
                UnityARSessionNativeInterface.ARFaceAnchorUpdatedEvent += FaceUpdated;
                UnityARSessionNativeInterface.ARFaceAnchorRemovedEvent += FaceRemoved;
            }
        }

        // ComputeShaderに渡すバッファの生成
        void FaceAdded(ARFaceAnchor anchorData)
        {
            var vertexCount = anchorData.faceGeometry.vertices.Length;
            _positionBuffer = new ComputeBuffer(vertexCount * 3, sizeof(float));
            _tmpPositionMap = _positionMap.Clone();
        }

        // 取得した頂点情報の書き込み
        void FaceUpdated(ARFaceAnchor anchorData)
        {
            if (_positionBuffer == null) return;

            var mapWidth = _positionMap.width;
            var mapHeight = _positionMap.height;
            var vCount = anchorData.faceGeometry.vertices.Length;

            // FaceGeometryから頂点とtransformの取得を行う
            _positionBuffer.SetData(anchorData.faceGeometry.vertices);
            gameObject.transform.localPosition = UnityARMatrixOps.GetPosition(anchorData.transform);
            gameObject.transform.localRotation = UnityARMatrixOps.GetRotation(anchorData.transform);

            // バッファに確保した頂点とtransformをComputeShaderにセットして実行。
            // ※transformはComputeSahder内で頂点に対するワールド変換を行う際に使用する。
            _vertexBaker.SetInt(_vertexCountID, vCount);
            _vertexBaker.SetMatrix(_transformID, gameObject.transform.localToWorldMatrix);
            _vertexBaker.SetBuffer(0, _positionBufferID, _positionBuffer);
            _vertexBaker.SetTexture(0, _positionMapID, _tmpPositionMap);

            // ComputeShaderの実行
            // 
            // Inspectorから指定される_positionMapのwidthとheightは「512 x 512」となるので、
            // ComputeShaderとしては「group(64, 64, 1)」「numthreads(8, 8, 1)」
            // → 「(64 * 8, 64 * 8, 1) → 512 x 512」のバッファに対する書き込みを行う事となる。
            _vertexBaker.Dispatch(0, mapWidth / 8, mapHeight / 8, 1);

            Graphics.CopyTexture(_tmpPositionMap, _positionMap);
        }

        void FaceRemoved(ARFaceAnchor anchorData)
        {
            _positionBuffer.TryDispose();
            _tmpPositionMap.TryDestroy();
        }
    }

ちなみに_positionMapには以下のRenderTextureを設定してます。

hoge

2. ComputeShaderでの書き込み

書き込んでいる処理はCSMainです。
単純にC#側から渡されたTextureに座標情報を書き込んでいるものとなります。4

※補足: group/threadsの指定数について

numthreadsでは(8, 8, 1)を指定しており、これは言い換えると1グループ辺り「8 x 8 x 1 = 64 (pixel)」分処理される事を指します。
その上でDispatch時のグループの指定としては「64 x 64 x 1 = 4096」を指定しており、結果として「64 (1グループ辺りで処理されるpixel数) x 4096 (処理する全グループ数) = 262144 (処理される全ピクセル数)」が処理される事となります。
この結果は言い換えると「512 x 512サイズのテクスチャの総ピクセル数(262144)」分がComputeShaderで処理される事を指します。

// refered to:
// https://github.com/keijiro/Smrvfx
// Smrvfx/Assets/Smrvfx/SkinnedMeshBaker.compute

#pragma kernel CSMain

uint VertexCount;
float4x4 Transform;
StructuredBuffer<float> PositionBuffer;
RWTexture2D<float4> PositionMap;

// Hash function from H. Schechter & R. Bridson, goo.gl/RXiKaH
uint Hash(uint s)
{
    s ^= 2747636419u;
    s *= 2654435769u;
    s ^= s >> 16;
    s *= 2654435769u;
    s ^= s >> 16;
    s *= 2654435769u;
    return s;
}

// ※Dispatch(0, 64, 64, 1) で実行される想定。
[numthreads(8,8,1)]
void CSMain (uint2 id : SV_DispatchThreadID)
{
    uint i = Hash(id.x + id.y * 65536) % VertexCount;

    float3 position = float3(
        PositionBuffer[i * 3],
        PositionBuffer[i * 3 + 1],
        PositionBuffer[i * 3 + 2]);

    position = mul(Transform, float4(position, 1)).xyz;
    PositionMap[id] = float4(position, 1);
}

3. 書き込んだTextureをVFX Graphで参照。

後はVFX GraphのInitializeコンテキストにSet Position from Mapと言うBlockを付け加えて、Attribute Mapの所に書き込まれるRenderTextureを指定することで参照することが可能です。

Art002.png

小さいかもしれませんが...冒頭に乗せているSTGのラスボスとして出てきそうな演出のグラフの一部を載せると以下のようになります。
Set Position from Mapで指定された位置に対しCubeをランダムなサイズで生成 → Updateで重力を掛けて落下させてます。
他にもSet Color over lifeで寿命に応じて色を変えつつSet Angleで回転をかけたりもしてます。

グラフ全体についてはプロジェクト内の「CubeFace」を御覧下さい。

Art005.png

参考/関連サイト

ARKit関連

VFX Graph関連

GitHub

公式ブログ/動画

  1. ※シーン中にARCameraTracker.csと言う「端末上のカメラの動きをUnity上のCamera.transformに反映させるスクリプト」が存在しますが、この中にDebug.Logを仕掛けて値を見た感じだとプラグインから渡ってくる値レベルで既に回転しているように見受けられた。

  2. 詳細な挙動を把握せずに使うだけであれば不要かもしれませんが。。

  3. ※ツール自体はメニューバーの「Window -> Visual Effect -> Utilities -> Point Cache Bake Tool」から起動可能。

  4. 書き込む際にHash関数経由でPositionBufferのindexを求めているが、恐らくはindexの衝突を避けるために設定説。(例えばSV_DispatchThreadIDに(1, 0)と(0, 1)が渡ってきたとしたら、単純に加算するだけだと同じindexに書き込むことになるので)

39
29
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
39
29

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?