気ままにI/O

プログラムとかものづくりのインプット・アウトプットのためのブログ。

ARFoundationでデプスバッファを活用してPeopleOcclusionを実現する

概要

2週間くらい前だったか、UnityのARFoundationでPeopleOcclusionするために数日がんばりました。

思いのほか苦戦したので、せっかくなので公開しようと思い、この週末でプロジェクトを整理してGitHubにて公開しました。

github.com

先行事例

PeopleOcclusionの実装方法についてはフォーラムのこちらのスレッドで議論されており、この方法での実装記事もいくつか書かれているようです。

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

 

この方法ではカメラのOnRenderImage関数を使ったポストエフェクト的な手法が取られているのですが、今回のケースではLWRPやURPでも動作させたかったため、別のアプローチとして背景描画と同時にデプスバッファに書き込むという方法で実現しました。

デモ

デプスとステンシル画像をシェーダーでエイっとするとこんなかんじになりました。

https://raw.githubusercontent.com/wiki/KzoNag/ARFoundationPeopleOcclusion/Images/PeopleOcclusionDemo.gif

使い方

リポジトリのREADMEに書いていますが、カメラ映像を描画しているARCameraBackgroundコンポーネントにカスタムマテリアルをセットし、そのシェーダーにテクスチャを流し込むためにARCameraOcclusionコンポーネントを追加すればOKです。

RenderPipeline対応

これもREADMEに書いていることですが、Legacy/LightWeight/Universalのレンダーパイプラインに対応しています。

各レンダーパイプライン用のシェーダーを用意しているので、ARCameraBackgroundにセットしたカスタムマテリアルを適切なシェーダーにすれば各レンダーパイプラインで動作するはずです。

Legacy/LWRPは動作確認済みですが、Universalは一応用意してるもののちゃんと確認はできてません。以前試したときにURPでARFoundationがうまく動かなかったので。

仕組み

背景描画のタイミングで深度情報をデプスバッファに書き込むというアプローチで実装しています。

用意したシェーダーはARFoundationにデフォルトで用意されている背景描画用のシェーダーをベースにしたもので、デプスを書き込む部分だけ追加しています。

ベースにしているシェーダーは、ARKit XR PluginパッケージのAssets/Shadersフォルダに入っています。

背景描画と同一のタイミングでデプスについても処理することで、背景と同一のUV値をが利用でき回転の向き等の考慮を独自に考えずに済んでいます。

 

実装のポイントとしては大きく3つ。

1つは、デプス値を書き込むためにフラグメントシェーダーの返り値用の構造体を用意すること。以下のような構造体を作っています。

 

 struct FragmentOutput
{
    half4 color : SV_Target;
    half depth : SV_Depth;
};

 

2つ目は、ARKitから取得したデプスの値からデプスバッファ用の値に変換すること。

具体的には以下の部分。depthの計算は、深度から位置を計算すると思われるLinearEyeDepthという関数を参考に逆算して出しています。

 

// Calculate 0-1 depth value from meter depth value
// Reverse of LinearEyeDepth function in UnityCG.cginc
float depthMeter = tex2D(_textureDepth, texcoord).r;
float depth = (1.0 - _ZBufferParams.w * depthMeter) / (depthMeter * _ZBufferParams.z);

 

最後に、ステンシルの値を元に計算したデプス値を書き込むべきか判断すること。デプステクスチャで人物領域でない部分の値は0になっています。そのまま使うと距離が0として計算されてしまうので、人物領域の部分だけ計算したデプス値を書き込むようにします。なお、Metalでのデプス値はnear→farで1→0のようなので、人物領域でない部分には0を書き込んで最も遠い位置にあるとみなしています。

ステンシルテクスチャには0か1の値が入ってると思っているのですが、そのまま使うと何故かうまくいかなかったため、step関数を使って0/1の値を作っています。

 

// Get people segmentation
half stencil = tex2D(_textureStencil, texcoord).r;
stencil = step(1, stencil); // Get 0 or 1

o.depth = depth * stencil; // 0 means far plane

 

おわりに

レンダリング周りはあまり詳しくないのでデプス値の計算の部分でかなり苦戦しました。環境によってデプス値が0→1だったり1→0だったりするというのも今回調べてて初めて知った。ARKit以外にも対応が必要になったらこの辺りもっとちゃんと理解して書かないといけないだろうなぁ。

あとはARFoundationが実機でしかテストできない&シェーダーのデバッグ方法がよく分からないのコンボでトライ&エラーにめちゃくちゃ時間がかかりました。特にシェーダーのデバッグ方法は詳しい人がいたら教えてほしいです。

 

最終的にはかなりシンプルな構成になってセットアップも簡単なので、興味がある人はぜひ動かしてみてください。