🤪

パーフェクトシンク対応アプリになってさらに表情豊かにしよう

2022/12/26に公開

hatracker というアバターアプリを作っています。これを パーフェクトシンク に対応してみます。

https://twitter.com/noir_neo/status/1606966805364633600

環境は以下です。

  • Unity 2021.3.3f1
  • AR Foundation 4.2.3
  • UniVRM 0.62.0

パーフェクトシンク対応アバターを用意する

これが最初にして最も重要なステップです。私は BOOTH でこちらのアバターを購入しました。

https://booth.pm/ja/items/2223325

パーフェクトシンク対応の サンプルアバター もありますし、VRoid製アバターなどをご自身で パーフェクトシンク対応 してみてもよいと思います。

enum に表情種類の key を追加する

hatracker では DomainElement として表情 key の互換レイヤーを持っています。 ARKit の BlendShape 種類の enum も持っていますが、これとは別に定義します (理由は後述)。

// VRM の表情種類の互換レイヤー
public enum BlendShapeKey
{
    // VRM 1.0 準拠 (実際には 0.x 系だが表情は 1.0 準拠で定義している)
    // https://github.com/vrm-c/vrm-specification/blob/master/specification/VRMC_vrm-1.0-beta/expressions.ja.md
    Aa,
    Ih,
    Ou,
    /// ... (略)

    // PerfectSync
    // https://docs.unity3d.com/Packages/com.unity.xr.arkit-face-tracking@1.0/api/UnityEngine.XR.ARKit.ARKitBlendShapeLocation.html
    BrowDownLeft,
    BrowDownRight,
    BrowInnerUp,
    /// ... (略)
}

トラッキングしたデータの左右を反転する

ARKit の Face Tracking で取得できるデータの左右は鏡になっています。つまり、eyeBlinkRight は実際には左目の閉じ具合です。
cf. https://developer.apple.com/documentation/arkit/arfaceanchor/blendshapelocation

たとえば、アバターをそのままカメラに向かい合うように配置する場合は、このデータをそのまま流し込めばよいです。一方で hatracker は アバターの左右を正しく動かす ので、 Blendshape の key としては Left は Right に、 Right は Left に置き換える必要があります。
以下のような Dictionaly を用意し、 mapping し直します。

static readonly Dictionary<ARKitBlendShapeLocation, BlendShapeKey> ARKitBlendShapeLocationsMap = new()
{
    { ARKitBlendShapeLocation.BrowDownLeft, BlendShapeKey.BrowDownRight },
    { ARKitBlendShapeLocation.BrowDownRight, BlendShapeKey.BrowDownLeft },
    { ARKitBlendShapeLocation.BrowInnerUp, BlendShapeKey.BrowInnerUp },
    /// ... (略)
}

static IEnumerable<BlendShapeCoefficient> Convert(IReadOnlyDictionary<ARKitBlendShapeLocation, float> blendShapes)
{
    return blendShapes.Select(x => new BlendShapeCoefficient(ARKitBlendShapeLocationsMap[x.Key], x.Value));
}

hatracker で表情 key の enum をあえて別に持っているのは、このような変換前後を明確にする意味もあります。

VRM.BlendShapeKey との map を定義する

変換に使う他、パーフェクトシンクで使う VRM.BlendShapeKey の一覧としても使います。

static readonly Dictionary<BlendShapeKey, VRM.BlendShapeKey> BlendShapeKeys = new()
{
    { BlendShapeKey.BrowDownLeft, VRM.BlendShapeKey.CreateUnknown("BrowDownLeft") },
    { BlendShapeKey.BrowDownRight, VRM.BlendShapeKey.CreateUnknown("BrowDownRight") },
    { BlendShapeKey.BrowInnerUp, VRM.BlendShapeKey.CreateUnknown("BrowInnerUp") },
/// ... (略)
};

ToString なり Enum.GetName なりを使っても良いとは思います (cache する前提であれば / この辺の関数は 遅い ことが知られている) が、私はもう何も考えずに矩形編集でガッと Dictionary なり switch なりを書いてしまうことが多いです。

パーフェクトシンク対応アバターかを判定する

厳密にはすべての key に対応している必要はないらしいのですが、UIで切り替えるのは (UIを作るのが) めんどくさいので、対象となる BlendShape 52個全てを備えていた場合に対応アバターと判定することにします。

1)ARKitで定義された52個のBlendShapeを全部または一部備えていること
 アプリはBlendShapeClipを参照して動作するため、BlendShape自体は52個全部そろっていなくてもOKです
https://hinzka.hatenablog.com/entry/2020/08/15/145040

public static bool IsSupported(VRMBlendShapeProxy vrmBlendShapeProxy)
{
    return vrmBlendShapeProxy.BlendShapeAvatar.Clips.Select(x => x.Key).Intersect(BlendShapeKeys.Values).Count() ==
        BlendShapeKeys.Count;
}

アバターに適用する

実際には補間をかけたりもしていますがここでは割愛します。ざっくりとこんな感じのコードになります。

var dict = blendShapeCoefficients.ToDictionary(b => b.BlendShapeKey, b => b.Coefficient);
var blendShapeValues = BlendShapeKeys.Select(kv => new KeyValuePair<VRM.BlendShapeKey, float>(kv.Value, dict.TryGetValue(kv.Key, out var v) ? v : 0f));
vrmBlendShapeProxy.SetValues(blendShapeValues);

最初の変換時点で VRM.BlendShapeKey を key にしてしまえばよいといえばまあそうなのですが、 hatracker では最も view 寄りの層でのみ VRM のことを知れるような設計にしているためにこのようになっています。これは、VRM への依存を最小にすることでアップデート時の影響範囲を最小にすることを狙っています。

おわりに

というわけで、ひたすらデータの詰め替えでしたね。hatracker は iOS版 app とPC版の通信もあるので、更に serialization のための詰め替えも発生します。
パーフェクトシンクという規格がそもそも ARKit の仕様に強く依存しているので、元々の互換レイヤーの厚さだけ冗長になるかと思いきや、実際には左右反転の問題なども考えると妥当に機能していると言えるのではないでしょうか (とはいえ記事中では省略していますが、何度も登場する52行の定義はなかなかヘビーですね…)。

なお、この機能を実際にリリースするかは未定です。

Discussion