じゅころぐAR

ARのブログ

AR FoundationでPeople Occlusionを実装する

AR FoundationでPeople Occlusionを試しました。
実装方法にたどり着くまでに結構遠回りしたので、知見として共有しておきます。

People Occlusionとは

ARKit 3の新機能の1つとして公開されたもので、カメラ映像から人を検出してオクルージョンを行うことができます。
従来のARでは、カメラに映り込んだ人は常にコンテンツの全面に表示されてしまうため、不自然な表示になっていました。People Occlusionを行うことで手前にいる人にコンテンツが隠れ、前後関係が正しく見えるようになります。

今まで実装が難しかった"人とAR空間"、"人とバーチャルキャラクター"がコラボする映像表現がやりやすくなります。

ただし、対象機種がまだ少なく、AR Foundationを使って実装してもARKitの一部デバイスのみで利用できる機能になります。ARCore(Android)はもちろん、iPhone X以前のデバイスでも使えません。

詳細は以下のページにまとめています。
ARKit/ARCore対応デバイス、対応機能一覧(2020/10) - じゅころぐAR

なお、私はARKit 3の開発目当てでiPhone SEからiPhone 11に機種変更しました。

Native App(Swift)のサンプルを動かしてみる

Apple Developerにサンプルコードがあり、すぐに試すことができます。後述しますが、AR Foundationにはそのものズバリのサンプルがまだ無いので、先にこちらを動かしておくと機能がイメージしやすくなります。

リンク先から"DOWNLOAD"を行うと、Xcodeプロジェクトを含んだzipファイルが取得でき、以下の手順ですぐに実機で試せます。

  • zipを解凍
  • Xcodeプロジェクトを起動
  • Signing & Capabilities > Teamを設定
  • Run(再生ボタン)

ダウンロードしたプロジェクトにはLICENSE.txtが同梱されており、文面からMIT Licenseのようです。
LICENSE.txtに記載されているコピーライト(例:Copyright © 2019 Apple Inc.)の著作権表示さえ行えば、サンプルコードを利用・改変してアプリを作っても問題はありません。

AR Foundationで試す

【2020/4追記】AR Foundation 3.1.0のアップデートにて、より簡単にピープルオクルージョンが利用できるようになりました。

AR Foundation 3.1以降の場合

デフォルトのカメラ背景として使用されているシェーダーARKitBackground.shaderにピープルオクルージョンに対応した処理が記述されています。

  • Packages/com.unity.xr.arkit/Assets/Shaders/ARKitBackground.shader

そのため、AR CameraAR Occlusion Managerを付けるだけでピープルオクルージョンが有効になります。

f:id:jyuko49:20200404104701p:plain

Bestに設定するとかなりキレイにオクルージョンの境界が取れます。

AR Foundation 3.0.1の場合

このバージョンにはピープルオクルージョン用の実装が入っていません。
動作確認済み(verified)の安定版としてリリースするために一旦削除されています。

AR Foundation 2.2〜3.0.0の場合

ARKit3を扱うには、AR Foundation 2.2.x以上が必要です。AR Foundation 3.0が公開されているため、こちらを試してみることにします。

  • Unity 2019.2
  • AR Foundation 3.0.0 - Previrew.3
  • ARKit XR Plugin 3.0.0 - Previrew.3

AR Foundation Samplesを見てみる

現状でAR Foundationの実装方法を学ぶ方法としては、AR Foundation Samplesのサンプルを動かして、ソースコードを読むのが定石です。AR Foundation SamplesはGitHubにてUnity Companion Licenseで提供されており、本サンプルを改変して開発したアプリの公開も可能です。

github.com

People Occlusionの機能に該当するのは、HumanSegmentationImagesというサンプルシーンなのですが、Native Appのサンプルと異なり、オクルージョン処理は実装されていません。
ただ、前提知識を頭に入れて理解を深めるには良いので一度動かしてみましょう。

HumanStencilを表示する

HumanSegmentationImagesをそのままビルドして実機で動かすと以下のような結果になります。

画面左上にカメラ映像とは別のイメージが表示され、人が写っている部分が赤くなっています。これはARHumanBodyManagerから取得できるhumanStencilTextureになります。取得方法は後述します。
人を検知した部分が赤(r:1, g:0, b:0)、それ以外が黒(r:0, g:0, b:0)の二値画像になっています。

Unityでの実装を見てみると、AR Session OriginAR Human Body Managerがアタッチされており、Human Segmentation Stencil ModeHuman Segmentation Depth ModeStandard Resolutionが設定されています。

f:id:jyuko49:20191009083716p:plain

また、別のGameObjectにTest Depth Imageというスクリプトがあり、AR Session OriginAR Human Body Managerがアタッチされています。こちらのスクリプトが画面左上のイメージ(Raw Image)、画面中央のテキスト(Image Info)を更新しているようです。

f:id:jyuko49:20191009083733p:plain

TestDepthImage.csで行われている処理は複雑ではなく、humanStencilTextureの取得部分は以下で実装できます。

ARHumanBodyManager m_HumanBodyManager;

void Update()
{
    Texture2D humanStencil = m_HumanBodyManager.humanStencilTexture;
    Texture2D humanDepth = m_HumanBodyManager.humanDepthTexture;

    m_RawImage.texture = humanStencil;
}

HumanDepthを表示する

HumanSegmentationImagesでは、スクリプトでhumanDepthTextureも取得していますが、画面にはhumanStencilTextureしか表示していません。

humanDepthTexture は、TestDepthImage.csの末尾でコメントアウトされている処理を修正すると表示できます。

  • 修正前

m_RawImage.texture にはhumanStencilがセットされています。

// To use the stencil, be sure the HumanSegmentationStencilMode property on the ARHumanBodyManager is set to a
// non-disabled value.
m_RawImage.texture = humanStencil;
// To use the depth, be sure the HumanSegmentationDepthMode property on the ARHumanBodyManager is set to a
/// non-disabled value.
// m_RawImage.texture = eventArgs.humanDepth;
  • 修正後

m_RawImage.texture = humanStencil; の行をコメントアウトして、m_RawImage.texture = humanDepth;のコメントアウトを外します。
eventArgsはundefinedになってしまうため、コメント内のBugと思われます。

// To use the stencil, be sure the HumanSegmentationStencilMode property on the ARHumanBodyManager is set to a
// non-disabled value.
// m_RawImage.texture = humanStencil;
// To use the depth, be sure the HumanSegmentationDepthMode property on the ARHumanBodyManager is set to a
/// non-disabled value.
m_RawImage.texture = humanDepth;

上記の修正を行なってビルド・実行すると以下のようになります。

一見すると同じようにも見えますが、赤く表示された部分に明暗が付いた距離画像になっています。

動かしてみると、カメラから遠い箇所は赤(r:1, g:0, b:0)に近い色、カメラに近い箇所は段々と暗くなり黒(r:0, g:0, b:0)に近い色になることがわかります。
rの値がカメラからの距離を表しており、値が小さいほど距離が近く、値が大きいと距離が遠いことを示しています。

シェーダーを利用してPeople Occlusionを実装する

HumanStencil、HumanDepthが取得できることがわかったので、人の有無とその距離に応じて、ARコンテンツを描画するかどうかを決めればよいことになります。
実装方法としては、シェーダーを利用したポストエフェクトが有力です。

シェーダーの記述は敷居が高いと思いますが、本手法についてはUnity Forumで議論がなされており、サンプルコードも共有されているので、そちらを参考に実装していくのが最短だと思います。

https://forum.unity.com/threads/how-to-setup-people-occlusion.691789/

とりあえず動かしてみる

Unity Forumのサンプルと同じ内容ですが、動作が確認できたShaderとC#スクリプトをgistに登録しましたので、参考にしてみてください。

C#スクリプト(PeopleOcclusionPostEffect.cs)はAR Session Originの子オブジェクトになっているAR Cameraにコンポーネントとして追加します。
このスクリプトにPeopleOcclusion.shaderをアタッチすれば、ポストエフェクトがかかります。

f:id:jyuko49:20191011073638p:plain

f:id:jyuko49:20191009084635p:plain
実行結果

シェーダーの動作を理解する

とりあえず、やりたいことはできました。やったね!
なのですが、ここで終わると応用が効かないので、頑張ってシェーダーを読みましょう。

一番重要なフラグメントシェーダーがこちら。

fixed4 frag (v2f i) : SV_Target
{
    fixed4 col = tex2D(_MainTex, i.uv);
    fixed4 cameraFeedCol = tex2D(_CameraFeed, i.uv1);
    float sceneDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv));
    float4 stencilCol = tex2D(_OcclusionStencil, i.uv2);
    float occlusionDepth = tex2D(_OcclusionDepth, i.uv2) * 0.625; //0.625 hack occlusion depth based on real world observation
                           
     float showOccluder = step(occlusionDepth, sceneDepth) * stencilCol.r; // 1 if (depth >= ocluderDepth && stencil)
 
     return lerp(col, cameraFeedCol, showOccluder);
}

1行ずつ読み解いていきます。

fixed4 col = tex2D(_MainTex, i.uv);

colにはポストエフェクトをかける前のテクスチャがセットされています。

fixed4 cameraFeedCol = tex2D(_CameraFeed, i.uv1);

cameraFeedColにはスクリプトのARCameraManagerで取得したカメラ映像が入っています。

float sceneDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv));

sceneDepthはUnityでシーンに配置したカメラから取得できるARシーンのデプステクスチャです。
カメラの深度テクスチャ - Unity マニュアル

float4 stencilCol = tex2D(_OcclusionStencil, i.uv2);

stencilColにはスクリプトで取得したHumanStencilが入っています。対象のpixelに人がいるなら(r:1, g:0, b:0, a:0)、それ以外は(r:0, g:0, b:0, a:0)です。

float occlusionDepth = tex2D(_OcclusionDepth, i.uv2) * 0.625; //0.625 hack occlusion depth based on real world observation

occlusionDepthにはスクリプトで取得したHumanDepthが入っています。HumanDepthは0から1の値で深度を表していましたが、そこに現実空間と距離を合わせるための係数として0.625をかけています。

float showOccluder = step(occlusionDepth, sceneDepth) * stencilCol.r; // 1 if (depth >= ocluderDepth && stencil)

showOccluderがこのシェーダーのポイントとなる値です。

step(y, x)x >= yなら1、x < yなら0となる関数です。HumanDepthから取得したocclusionDepthとシーンのカメラから取得したsceneDepthを比較しており、人を検知した距離(y)がシーン上でオブジェクトが配置されている距離(x)よりも近ければ、x >= yで1になります。
step - Win32 apps | Microsoft Docs

つまり、

  • step(occlusionDepth, sceneDepth) = 1:人がオブジェクトの手前にいる
  • step(occlusionDepth, sceneDepth) = 0:人がオブジェクトの後方にいる

となります。

上記に対して、さらにstencilCol.rをかけています。
occlusionDepthは人を検知していないPixelにもr=0が入ります。そうすると、step(occlusionDepth, sceneDepth) = 1となってしまい、人がいない場所なのにオクルージョンの対象となってしまいます。
一方のstencilCol.rは人を検知したPixelのみ1になるので、確実に人がいる場所のみ値が取れます。

整理すると、以下のようになります。

  • showOccluder = 1:人が検知されていて、オブジェクトの手前にいる
  • showOccluder = 0:人が検知されていない、もしくはオブジェクトの後方にいる

試しに表示してみると、人が検知されている場所のみ白(1)、他が黒(0)になっていることがわかります。

return lerp(col, cameraFeedCol, showOccluder);

最後にreturnの値です。lerp(x, y, s)はsの値に応じて線形補間を行う関数です。sが0のときにx、1に近づくにつれてyになります。
lerp - Win32 apps | Microsoft Docs

sはshowOccluderなので、前述の通り、0か1です。
0のときはオブジェクトの手前には人がいないため、colをそのまま描画します。1のときは手前に人がいることになるのでカメラ映像のcameraFeedColで上書きしています。
こうすることで、人が手前にいる場合はカメラ映像に映った人が優先して描画され、結果としてオブジェクトが人の後ろに回り込む形になります。

People Occlusionを行うためにlerp(x, y, s) のyがcameraFeedColとなっていますが、この値を変えることで人が存在する部分にポストエフェクトをかけることもできます。
この用途で使う場合、デプスの比較をせずにsをstencilCol.rとしても良さそうです。

おわりに

  • 先人の知恵に感謝
  • AR Foundation Samplesはよ
  • ARCoreでの実装はよ