AR FoundationでPeople Occlusionを試しました。
実装方法にたどり着くまでに結構遠回りしたので、知見として共有しておきます。
People Occlusionとは
ARKit 3の新機能の1つとして公開されたもので、カメラ映像から人を検出してオクルージョンを行うことができます。
従来のARでは、カメラに映り込んだ人は常にコンテンツの全面に表示されてしまうため、不自然な表示になっていました。People Occlusionを行うことで手前にいる人にコンテンツが隠れ、前後関係が正しく見えるようになります。
なるほど pic.twitter.com/nsXAUV2ejN
— jyuko (@jyuko49) October 7, 2019
今まで実装が難しかった"人と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 Camera
にAR Occlusion Manager
を付けるだけでピープルオクルージョンが有効になります。
Best
に設定するとかなりキレイにオクルージョンの境界が取れます。
最高画質に設定したときの境界キレイ✨
— jyuko🐼 (@jyuko49) 2020年2月11日
これ今すぐアプデすべきでは?#ARFaundation pic.twitter.com/5ZmKbHr0wv
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で提供されており、本サンプルを改変して開発したアプリの公開も可能です。
People Occlusionの機能に該当するのは、HumanSegmentationImagesというサンプルシーンなのですが、Native Appのサンプルと異なり、オクルージョン処理は実装されていません。
ただ、前提知識を頭に入れて理解を深めるには良いので一度動かしてみましょう。
HumanStencilを表示する
HumanSegmentationImagesをそのままビルドして実機で動かすと以下のような結果になります。
画面左上にカメラ映像とは別のイメージが表示され、人が写っている部分が赤くなっています。これはARHumanBodyManager
から取得できるhumanStencilTexture
になります。取得方法は後述します。
人を検知した部分が赤(r:1, g:0, b:0)
、それ以外が黒(r:0, g:0, b:0)
の二値画像になっています。
Unityでの実装を見てみると、AR Session Origin
にAR Human Body Manager
がアタッチされており、Human Segmentation Stencil Mode
とHuman Segmentation Depth Mode
にStandard Resolution
が設定されています。
また、別のGameObjectにTest Depth Image
というスクリプトがあり、AR Session Origin
のAR Human Body Manager
がアタッチされています。こちらのスクリプトが画面左上のイメージ(Raw Image
)、画面中央のテキスト(Image Info
)を更新しているようです。
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
をアタッチすれば、ポストエフェクトがかかります。
シェーダーの動作を理解する
とりあえず、やりたいことはできました。やったね!
なのですが、ここで終わると応用が効かないので、頑張ってシェーダーを読みましょう。
一番重要なフラグメントシェーダーがこちら。
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での実装はよ