Azure Kinect を使ったAR表現で魔法っぽいものを出す試み

2021/12/22に公開

はじめに

こんにちは。普段はとあるライブ配信サービスでサーバーサイドエンジニアをしている @AquaLampです。

個人ではこういう感じのものを作ったりしています。

この記事は STYLYアドベントカレンダー Advent Calendar 2021 の22日目の記事です。STYLY本体からは遠い内容となり恐縮ですが、「VR/AR/MRにまつわることならなんでもOK」というお言葉に甘えて記事を書かせていただきます。

扱う内容

この記事では、Azure Kinectを使ってOcclusionの表現をする私なりのアプローチの概要を紹介したいと思います。アセット Azure Kinect Examples for Unity を使う前提の内容となっています。

気持ちとしては「私はこうやってみた!もっといい方法があったら知りたい!」なので、各アプローチとしては最適解ではないかもしれませんが、それでも誰かの役に立てば嬉しいと思い公開します。

また、実務ではサーバー側を主戦場とする人間が書いていることをご留意の上、不完全な部分はお目溢しください。

Azure Kinect とは

Azure KinectとはMicrosoftから発売されている開発者向けのセンサーです。RGBカメラとDepthカメラだけでなく、IMUやマイクアーレイも搭載していてさまざまな用途に使うことができます。
https://azure.microsoft.com/ja-jp/services/kinect-dk/#overview

Azureという名前を冠しているので、Microsoftのクラウド Microsoft Azure の利用が必須なのかと思われがちだそうですが、そんなことはありません。
ただ、その気になればAzure Cognitive Servicesと接続しいい感じに利用でるとは謳われています。

この記事を書いている時点でお値段 $399.00。最近売り切れの場合が多いですが、急に在庫が復活したりするので、欲しい方は毎日覗いてみるといいかもしれません。

仕組み的にはiPhone LIDARセンサーと同じようなものなのでAR表現にも使えるはずだと思い昨年末に1台目を購入したのですが、結論それっぽいものをいろいろ作れて楽しめています。

余談ですが、2021/11に追加で1台購入したところ、翌日に届いてAmazon並の発送の速さに驚愕しました。

Azure KinectとUnityでARしたい

Azure KinectにはSDKがあるため、これを使えば好きなように開発ができるはずです。
一方、ARを表現をしたいがために、比較的低いレイヤーを意識したセンサーの取り回しから作っていくのはややツライものがあります。

そのあたりをだいたい面倒見てくれるアセットAzure Kinect Examples for Unityが出ていて、これを使うことで表現の部分に集中することができました。有料アセットなのでお値段 $27.50 ですが、個人的には「これだけできてこんな安くていいんですか..??」みたいな気持ちです。

コードも整理されて読めるようになっているので、すでに行われている実装自体から学ぶことも数多くありました。

導入

Azure KinectをUnityで動かすところまではさまざまな解説記事がありますが、Azure Kinect Examples for Unityを使う前提であれば下記の デモシーンを実行する方法 を確認するのが一番良さそうです。
https://assetstore.unity.com/packages/tools/integration/azure-kinect-examples-for-unity-149700?locale=ja-JP

※Kinect for Windows SDK 2.0 と RealSense SDK 2.0の導入も書いてありますが、この部分はスキップ可なはずです。

ちなみに以前はRTX30xx系のGPUでBodyTraking SDKが動かない問題がありましたが現在では解決しており、私の手元のGTX3080でも動いています。
https://github.com/microsoft/Azure-Kinect-Sensor-SDK/issues/1125

サンプルを動かす

Azure Kinect Examplesのドキュメントはそれなりに簡素なのですが、かなり丁寧なサンプルが入っています。

ARっぽいことをしたい場合、OverlayDemoPointCloudDemoあたりを先に見てみると実装のイメージが湧くかなと思うのでオススメです。

PointCloudDemoを動かすためプロジェクトはURPかHDRPで作成します。

OverlayDemo

これは、RGBカメラで取得した映像の上に、BodyTrackingから取得したJoint(関節)の位置にUnityのゲームオブジェクトを重ねて表示するものです。

映像は反転されているので実際には右手ですが、手の動き追従するGreenBallというゲームオブジェクトを見ることができます。(実際にはマテリアルが反映されず紫ですが...)

オブジェクトにアタッチされている Joint Overlayer のパラメーターを変更したり、コードの中身を読んだりすることでどういう仕組になっているか知ることができます。

Azure KinectのJointが体のどの部分に対応しているかは下記から確認できます。
https://docs.microsoft.com/ja-jp/azure/kinect-dk/body-joints

PointCloudDemo

次に、Azure KinectのDepthからPointCloudを生成するサンプルです。
VfxPointCloudDemo を見てみますが、このサンプルはVisual Effect Graphが必要なので、URPの場合は先にパッケージマネージャーから導入します。

これもまたあっさり動いて、RGBから取得した色をPointCloudに載せた状態で表示してくれます。

ちなみに、体の影になる部分はPointCloudが表示されないのが見て取れると思いますが、この領域は2台のAzure Kinectを使って死角となる部分を減らすことで対応できたりします。

サンプルに手を加えてOcclusionを実現するアプローチ

現状、現実の映像の上にUnityのゲームオブジェクトを表示することまではできているので、ここからさらにARっぽい表現を作ってみます。

個人的な感覚ですがOcclusionを作るとARとしての説得力が作れるかなと思うので

  • ゲームオブジェクトが現実の物体を遮る表現
  • 現実の物体がゲームオブジェクトを遮る表現

ができる状態を目指していきます。

OverlayDemoで見たとおり ゲームオブジェクトが現実の物体を遮る はサンプルで実現されているのので、現実の物体がゲームオブジェクトを遮るためにVfxPointCloudDemoで見たPointCloudを使っていきます。

現実の物体がゲームオブジェクトを遮る表現

VfxPointCloudDemoはUnityの3D空間上に表示されるパーティクルなので、カメラから見てパーティクルの後ろ側にゲームオブジェクトが隠れればOcclusionしているように見えます。

そこで、VfxPointCloudDemoとOverlayDemoを組み合わせることで現実の物体がゲームオブジェクトを遮る表現を作り出していきます。

VfxPointCloudDemoに新しいCancasを作ってレンダーモードを スクリーンスペース - カメラ に設定し、MainCameraをアタッチします。

そのキャンバス配下にRowImageなどを追加し、Azure Kinect ExamplesのスクリプトBackgroundColorImage をアタッチします。RectTransFromは下記のように設定すると全面に表示されます。

そしてMainCameraの環境->背景タイプを スカイボックス にすると背景となるRGBカメラの映像が映るようになります。

この時点では、PointCloudと背景がきれいに揃わないので、愚直に調整します。
私の場合こんな感じに調整して、だいたい合うようになりました。

位置合わせのために、RGBの映像を青色にしてわかりやすくしてみました。
青くなっていない箇所は背景ではなくPointCloudにより描画されている部分です。

この状態で背景となっているCanvasとカメラの間にパーティクルが浮いている状態になっているので、手の動きに追従するゲームオブジェクトを出すようにしてPointCloudの影に隠れればOcclusionが起こります。

ゲームオブジェクトはJoint Overlayerで動かしてもいいのですが、自前で KinectManager.Instance から位置を取得することもできるのでそのコードを置いておきます。

using UnityEngine;
using com.rfilkov.kinect;

public class BodyPosition : MonoBehaviour
{
    public float speed = 10;
    public Vector3 offset;
    private Vector3 velocity;
    private Vector3 targetPosition;
    private KinectManager kinectManager = null;

    void Start()
    {
        kinectManager = KinectManager.Instance;
    }
    
    void Update()
    {
        UpdateTrackPosition();
        Vector3 currentPosition = transform.position;
        velocity += (targetPosition - currentPosition) * speed;
        transform.position += velocity *= Time.deltaTime;
    }

    private void UpdateTrackPosition()
    {
        if (kinectManager && kinectManager.IsInitialized())
        {
            ulong userId = kinectManager.GetUserIdByIndex(0);
            int JointIndex = (int) KinectInterop.JointType.HandLeft;

            if (kinectManager.IsJointTracked(userId, JointIndex))
            {
                targetPosition = kinectManager.GetJointPosition(userId, JointIndex);
                targetPosition += offset;
            }
        }
    }
}

そしてCubuを手の動きで動かしてみます。

手の影に入ると....

Occlusionするようになりました。

あとは過剰にパーティクルなどを焚くと楽しい感じになります。

見た目を良くする追加のアプローチ

この状態でもARっぽくはあるのですが、背景の上に乗っているPointCloudはそれなりに荒いので表現としては工夫が必要そうです。

そこで、いくつかのアプローチが考えられます。

エフェクトを加算合成で乗せる

ひとつのアプローチとして、演出のエフェクトだけ別のカメラからRenderTextureに書き出したあと、レンダーモードを スクリーンスペース-オーバーレイ にしたCanvasに属するRowImageに流し込み、同じくRowImageに表示させたRGBカメラの結果に加算合成するという方法を思いつきました。

この方法では、生成するPointCloudを一律完全な黒で生成します。すると演出となるエフェクトはこの黒で生成したPointCloudに隠れることになるので、Occlusionする箇所は黒く塗りつぶされることになります。
黒く塗りつぶされても、最後に加算合成するので、黒の部分は透過し、Occlusionしていない部分だけ残ります。

したがって黒く塗りつぶされた部分は背景となっているRGBカメラの結果が映ることになり、あたかもOcclusionしているように見えることになります。

この方式で作ったのがこちらです。

この方式にすると演出のエフェクトを単独で書き出すので好きにPostEffectをかけることができます。

また、一度RenderTextureの結果を他のShaderから読み込んで、演出に使う事もできます。

上の作例では、RGBカメラの映像側もエフェクトに合わせて歪みがかかるようになっていますが、これはエフェクト演出で書き出されたRenderTextureをもとにしてRGBカメラ側のテスクチャのUVをpixcel shaderでいじり歪みがかかったように見せています。

エフェクト背景を単色にしてクロマキーで透過させる

魔法のようなものは大体加算合成するといい感じになるので問題ないのですが、透過しない物質的な表現をする場合、加算合成ではうまくいきません。

そこで、透過するべき場所を単色で塗りつぶして、Canvasに重ねて合成する前に特定色をshaderで透過させるという方法を思いつきました。クロマキーをUnityの内部でやってるイメージです。
...が、あまり汎用的にきれいにはいかなそうでした。この方法で作ったものはこちらです。

これはHDRPで作り、前項目の図に表した方法ど概ね同じような原理で作っています。違うのは、カメラの背景とPointCloudを完全な黒にするのではなく、完全な緑にしています。そして後で緑を透過させます。

物質的なものである以上影をつけたいと思い苦心したのですが、見かけ上自分の体と壁に影っぽいものを...ぽいものを落とすことができました。
これは、PointCloudに影を受けられるよう専用のShaderGraphをアタッチして実現しています。

VFX GraphのPerticleは通常のShaderGraphも適用できるようになったようなので、ShaderGraphのSupportVFX GraphShadowMatte にチェックを入れて、影を受けた場合完全な赤に変化するようになっています。
https://www.youtube.com/watch?v=qhaoVFmiNig&t=95s

次の下の画像が中間のRenderTextureで、上の画像がshaderで緑を透過、赤を影として扱うようにした結果のものです。

一瞬はきれいに見えたりするのですが、残念ながら反射の強い箇所や、物理的な実態のない光のグレアの部分は消えてしまっています。クロマキーを抜く部分も緑の範囲をきれいに判断することができず、やや汚くなってしまっています。

ShaderGraphを使って背景を取得する

まだ実戦投入できてないのであまり検証はできていないのですが、おそらくこの方法が大本命....な気がしています。

PointCloudは1パーティクルにつき1つの色しか持っていなかったので、荒く見えてしまっていたのですが、それならばPointCloud対して背景のテクスチャをScreenPositionをUVとして貼ればきれいになるのではという発想です。

背景のテクスチャは愚直に一度Canvasに描画したものをCameraからRenderTextureしています。(もっといい方法はありそうですが...)

そのテクスチャをShaderGraphから拾うのですがPointCloudのScreenPositionをUVとして使うことで、対応する位置の背景のテクスチャを持ってこれます。

やってみたところ、結構きれいにできたので、以後はこちらで作品を作りたいなぁと思っています。

余談:PointCloudを使わないアプローチ

ここまでPointCloudを使ってOcclusionを表現してきたのですが、AzureKinectから取れるDepthTextureと、Unity側のCameraから取れるDepthTextureを比較して、描画するしないをPixcelShaderで決めるアプローチもあるかなと思います。

実際、冒頭で紹介した背中に羽っぽいものを背負うやつは、この方法で作成したのですが、AzureKinectから取れるDepthのキャリブレーションがそれなりにシビアでした、(一方、Azure Kinect EampleではDepthを生成するコードを直接読めるので、なぜDepth値は線形でないのかということなどがわかり勉強になりました。)

もう一つ、Unityの仕様上、半透明なオブジェクトはDetphTextureに乗りません。魔法的な表現をしたいときはAlphaの値を徐々に下げたい気持ちが起こるのですが、これをできるようにするには半透明オブジェクトにしなければならないのでDepthTextureに乗らず、Occlusionの表現ができなくなってしまいました。

また、当たり前といえばその通りなのですが、PostEffectで入ったGlowにもDepthはありません。なので、Kinect側から取得したRGBに乗せるとき、Depthで比較するとGlow部分が消えてしまいます。

羽生やす作例でいうと、背中と重なっている部分にPostEfectで付与したGlowがのっていないのが顕著にわかります。

このあたりの面倒さがPointCloudだと考えないで済むので、位置合わせなど面倒なところを許容しても前述の方法が楽なのでは..?と思っています。

おわりに

ARの開発をしてみて、自分の着想を形にすることが何にも変えられないほど楽しいと思いましたし、現実と作用させてインタラクティブな表現を実装することには表現面でも実装面でも気づきが多いように思います。

これまでは見て面白いと思うような演出の領域に留まってしまいましたが、ARには実用的な役割をもたせたり、ネットワークを介してひつのトリガーを皆で共有したりもできると思うので、サーバーサイドの知見も投入して開発したいな-と思っています。
もっとARに関わっていきたいので、どこかで見かけたときはどうぞよろしくおねがいします。

明日は @iwaken71さんの AR/MRのことでなにか書く (Immersal HoloLensなど) です!お楽しみに!

Discussion