takanakahiko’s blog

多分三日坊主で辞めます。

美少女勤務を支える技術( Airpods Pro でモーキャプをする )

f:id:takanakahiko:20201213181447p:plain

皆さん、 Working From Home していますか? 私はしばしば美少女としてオンライン通話に参加しています。 今回は、それを実現するための技術要素について解説していこうと思います。

この記事は Akatsuki Advent Calendar 2020 の 13 日目の記事です。 会社では、ゲーム内仮想通貨管理やAppStore レシート検証を行うマイクロサービスの実装・運用に携わっていて、主に Go を書いています。 が、今回は違うことをお話しようと思います。

adventar.org

前回の記事は @yunon_phys の「ボトルネックマネージャーを抜け出す方法」でした。 良い記事。

hackerslab.aktsk.jp

WFHですね

世間では COVID-19 も流行し、新たな働き方が模索されています。 私なんかは新卒で入社してから5日ぐらいしか会社に行っていないという、いわゆる Working From Home 形式で働いていますので、正直社会人になったという事実があまりピンときていません(とはいえ、出社頻度はチームや役割や各々のライフスタイルによって違いますので、会社の全員がこの頻度だという話ではないですが)。 そういった状況下で直接的に顔を合わせることが少なくなった我々は、オンラインで通話するアプリケーションを使用してコミュニケーションを図ってきました。

世間ではオンライン通話がデメリットとして受け入れられがちなことが多いですが、せっかくならメリットに目を向けていきたいところです。 個人的には、新たに Swift や Unity を学ぶ機会になると思い、美少女勤務にチャレンジすることにしました。 こういうのは、自分の中で腑に落ちるような「手を動かす理由」を見つけさえしてしまえば勝ちです。

なお mzp氏の先行研究があり、大いに参考にさせていただきました。

mzp.hatenablog.com

構成

まずは構成を紹介していきます。

今回は iOS 14 で初めて公開された AirPods の3次元ジャイロセンサへのアクセスができる API に興味があったので、それを用いることにしました。 これの良いところは、私のように iPhone 8 しか持っていないような人間でも AirPods Pro を所持していれば試せるという点にあります(これを作っている間に実は iPad Pro 買っちゃいましたが)。

AirPods Pro のモーションデータは現時点では Mac から直接取得することが出来ません。 そのため iPhone アプリとして取得する必要があります。 しかし Unity の動作は Mac 上で行いたいため、今回は UDPMac にセンサーデータを送信し続ける iOS アプリを作ることにしました。

f:id:takanakahiko:20201213022848p:plain

このような構成にすることで、 Swift 自体の記述を最小限にした上で Unity 側の美少女に本腰を入れて作業をすることが可能になりそうです。 また、ゆくゆくは iOS 用のアプリを公開しようと思っているので、他の皆さまにもある程度使ってもらいやすいかなと思っています。

iPhone アプリの実装

iPhone アプリでは、 AirPods Pro からのモーションデータの取得と UDP での通信を担います。 それぞれの実装を要点のみ紹介していきます。

CMHeadphoneMotionManager を利用することで AirPods Pro のモーションデータを取得する事ができます。 ちなみに Mac Catalyst 14.0+ でも動作すると記載されていますが、クラス自体は使えてもモーションデータの取得は出来ません。

developer.apple.com

実際にモーションデータを取得するには startDeviceMotionUpdates というメソッドを使いました。 ハンドラからモーションを受け取れるため、そこでよしなにすることが出来ます。

import CoreMotion

var hmm = CMHeadphoneMotionManager()
hmm.startDeviceMotionUpdates(to: .main) { (receivedMotion, error) in
  if let receivedMotion = receivedMotion {
    // ここでよしなに receivedMotion を使う
  }
}

あとは、UDPでの情報の送信になります。 Swiftでは標準ライブラリを使うと TCP と同じようなインターフェースで取り扱う必要があるので connection を作る必要があるので注意です。( TCP で connection って何? という気持ちが若干ある )

import Network

var connection = NWConnection(
  host: NWEndpoint.Host(setting.address),
  port: NWEndpoint.Port(rawValue: setting.port)!,
  using: NWParameters.udp
)

var connectionQueue = DispatchQueue(label: "hoge")
connection.start(queue: connectionQueue)

connection.send(content: content)

上記は本記事に書くためのサンプル実装ですので Force Unwrap を使っていますが、実際には if let 等を使う必要があると思います。 ですが Swift は初めて書くので、あまり突っ込んだ話はしないことにしますが、自分は以下のように実装しました。

https://github.com/takanakahiko/Kishimen/blob/f158d14bc31b4554ce392f09be2410f9afaf033d/Kishimen/Kishimen/ContentView.swift#L47-L71

実際には更に JSON 文字列への変換を挟んだり、設定のデバイスへの保存を行ったり、UIの制御も行っているのでかなり冗長なコードになってしまいましたので割愛します。 実際の iPhone アプリの実装はこちらにあります。

github.com

ところで

こちらは S&B の3万円福袋で届いた福袋の中身です。

f:id:takanakahiko:20201213155304p:plain

  • 粉わさびどうやって使うんだよこれ
  • 粒胡椒にセットで挽くやつ入ってて便利
  • カレー多すぎて草、誰か食べにきませんか
  • 冷蔵庫必要なやつがひとつもなくて嬉しい
  • 床に並べるのに15分かかったんですが…
  • 胡椒だけで5種類あって笑う
  • 納品書が16ページあってひっくり返った
  • ありがとうS&B

Unity 側の実装

Unity 側では、実際に受け取ったデータをモデルに流し込むスクリプトを書いていきます。

とはいえ、モデルの読み込みや実際に回転させるボーンの指定は、モデルの形式や構造によって異なってきます。 ポイントとしては、以下のように UDP の受け取りは別のスレッドでやる必要があるようです。

public class Sample : MonoBehaviour {
  static UdpClient udp;
  Thread thread;
  JsonNode motion;

  void Start() {
    udp = new UdpClient(LOCA_LPORT);
    thread = new Thread(new ThreadStart(ThreadMethod));
    thread.Start();
  }

  void OnApplicationQuit() {
    thread.Abort();
  }

  private void ThreadMethod() {
    while (true) {
        IPEndPoint remoteEP = null;
        byte[] data = udp.Receive(ref remoteEP);
        string text = Encoding.ASCII.GetString(data);
        JsonNode motion = JsonNode.Parse(text);
    }
  }
  
  private void Update() {
    // ここで motion をよしなに使う
    float x = (float)(motion["motion"]["attitude"]["quaternion"]["x"].Get<double>());
    float y = (float)(motion["motion"]["attitude"]["quaternion"]["y"].Get<double>());
    float z = (float)(motion["motion"]["attitude"]["quaternion"]["z"].Get<double>());
    float w = (float)(motion["motion"]["attitude"]["quaternion"]["w"].Get<double>());
  }
}

ここで、首だけでなく胴体も動作させると良い動きになります。 首だけだと不自然な動きになるのですが、胴体を頭の1/3程度の角度で回転させることで、首の回転に胴体が追従したような動きになり、より自然な動作となってきます。

下の例を見るとわかりやすいかなと思います。

f:id:takanakahiko:20201213172802g:plain

CamTwist を用いてカメラとして認識させる

Unity に美少女を映すだけでは美少女としての勤務は出来ません。 通話時に画面共有し続けることで美少女の姿を他の人に見てもらうことは出来ますが、少々主張が強すぎるでしょう。 そこで、Mac に Unity の画面をカメラであると認識させる必要があります。

CamTwist から Unity を選択して

f:id:takanakahiko:20201213163428p:plain

Zoom から CamTwist を選択することで Zoom にカメラ映像として認識させることが出来ます。

f:id:takanakahiko:20201213163608p:plain

実際の動作

今回はサンプルとして VRoid Studio のプリセットである HairSample_Female ちゃんの体をお借りしています。 各種キーを表情に適用するような実装も行っています。

f:id:takanakahiko:20201213180033g:plain

f:id:takanakahiko:20201213180109p:plain

今後について

今、上記にある iOS 向けアプリですが、現在 AppStore の申請を行っている最中です。 近日中に AppStore よりダウンロードできるようになる見込みですので、ぜひ皆さんお使いになってください。 簡単な検証等の用途にも使いやすいと思われます。

配信されたらこの URL になると思う

https://apps.apple.com/us/app/kishimen-send-airpods-motion/id1540650097

なお、iOSアプリの方は皆さんの contribution を受付中です。 より良いアプリにできたらなと思いますのでよろしくお願いいたします。

github.com

さいごに

この計画には多くの人の助けをいただきました。

Swift も Unity も初心者ですので、会社の優秀なクライアントエンジニアの方々やTwitterのフォロワーの方々に色々と教えてもらいました。 とても助かりました。

また、全社会議とかにふらっとバーチャル参戦しても暖かく迎えてくれるような会社の皆さんには感謝しかありません。 バーチャルが好きか否かという話より、多様な働き方が許容されていることのあらわれだと思いました。

アドベントカレンダーですが、明日は shivashin の記事です。 引き続きお楽しみください。