e.blog

主にUnity/UE周りのことについてまとめていきます

ARで影を描画する(影だけをレンダリングするシェーダ)

概要

ARコンテンツを作っているとキャラやオブジェクトの影をつけたくなります。
ただ、通常地面はARKitの機能で認識した平面に対して透明(か半透明)のメッシュを配置して表現します。

そしてUnityの標準のシェーダでは透明なオブジェクトには影が落ちません。

UnityのARKit Pluginには影を落とすためのシェーダが最初から用意されているのでそれを床面として配置したプレーンに設定してやれば影が落ちるようになります。

ただ、AR以外でも透明なオブジェクトに影を落としたいケースもあると思うので、その中で使われているシェーダを見つつ、ARKitではどうやって透明なオブジェクトに影を落としているかをメモしておきたいと思います。

ということで、用意されているシェーダは以下になります。
(英語で書かれているコメントをふんわり翻訳してあります)

コード全容

//This is based on a shader from https://alastaira.wordpress.com/2014/12/30/adding-shadows-to-a-unity-vertexfragment-shader-in-7-easy-steps/

Shader "Custom/MobileARShadow"
{
    SubShader
    {
        Pass
        {
            // 1.) This will be the base forward rendering pass in which ambient, vertex, and
            // main directional light will be applied. Additional lights will need additional passes
            // using the "ForwardAdd" lightmode.
            // see: http://docs.unity3d.com/Manual/SL-PassTags.html
            //
            // 1.) forwardレンダリングパスのアンビエント、頂点とメインディレクショナルライトで利用されます。
            //     追加ライトの場合は、addtionalパス、ForwardAddライトモードが必要です。
            Tags
            {
                "LightMode" = "ForwardBase" "RenderType"="Opaque" "Queue"="Geometry+1" "ForceNoShadowCasting"="True"
            }

            LOD 150
            Blend Zero SrcColor
            ZWrite On
        
            CGPROGRAM
 
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
 
            // 2.) This matches the "forward base" of the LightMode tag to ensure the shader compiles
            // properly for the forward bass pass. As with the LightMode tag, for any additional lights
            // this would be changed from _fwdbase to _fwdadd.
            //
            // 2.) "forward base"ライトモードにマッチし、シェーダがforward baseパスで正常にコンパイルされるようにします。
            //     他の追加ライトの場合は_fwdbaseを_fwdaddに変更する必要があります。
            #pragma multi_compile_fwdbase
 
            // 3.) Reference the Unity library that includes all the lighting shadow macros
            // 3.) すべてのライティングシャドウマクロのUnityライブラリをインクルードします。
            #include "AutoLight.cginc"
 
 
            struct v2f
            {
                float4 pos : SV_POSITION;
                 
                // 4.) The LIGHTING_COORDS macro (defined in AutoLight.cginc) defines the parameters needed to sample 
                // the shadow map. The (0,1) specifies which unused TEXCOORD semantics to hold the sampled values - 
                // As I'm not using any texcoords in this shader, I can use TEXCOORD0 and TEXCOORD1 for the shadow 
                // sampling. If I was already using TEXCOORD for UV coordinates, say, I could specify
                // LIGHTING_COORDS(1,2) instead to use TEXCOORD1 and TEXCOORD2.
                //
                // 4.) LIGHTING_COORDSマクロ(AutoLight.cgincで定義)は、シャドウマップをサンプルするパラメータを定義しています。
                //     (0, 1)はサンプルした値を保持するために、未使用のTEXCOORDを指定します。
                //     このシェーダでは、texcoordsは使用していないため、TEXCOORD0とTEXCOORD1をシャドウのサンプリングのために使用できます。
                //     もしUV座標のためにTEXCOORDを利用していたら、LIGHTHING_COORDS(1, 2)をTEXCOORD1とTEXCOORD2の代わりに使用します。
                LIGHTING_COORDS(0,1)
            };
 
 
            v2f vert(appdata_base v) {
                v2f o;
                o.pos = UnityObjectToClipPos (v.vertex);
                 
                // 5.) The TRANSFER_VERTEX_TO_FRAGMENT macro populates the chosen LIGHTING_COORDS in the v2f structure
                // with appropriate values to sample from the shadow/lighting map
                //
                // 5.) TRANSFER_VERTEX_TO_FRAGMENTマクロは、LIGHTING_COORDSで選択したものをv2f構造体の中で、shadow/lightingマップのサンプル用に適切な値に設定します。
                TRANSFER_VERTEX_TO_FRAGMENT(o);
                 
                return o;
            }
 
            fixed4 frag(v2f i) : COLOR {
             
                // 6.) The LIGHT_ATTENUATION samples the shadowmap (using the coordinates calculated by TRANSFER_VERTEX_TO_FRAGMENT
                // and stored in the structure defined by LIGHTING_COORDS), and returns the value as a float.
                //
                // 6.) LIGHT_ATTENUATIONは、シャドウマップからサンプルします。(TRANSFER_VERTEX_TO_FRAGMENTによって計算された座標を使って、LIGHTHING_COORDSによって定義された構造体に保持します)
                //     そしてfloatの値を返します。
                float attenuation = LIGHT_ATTENUATION(i);
                return fixed4(1.0,1.0,1.0,1.0) * attenuation;
            }
 
            ENDCG
        }
    }
     
    // 7.) To receive or cast a shadow, shaders must implement the appropriate "Shadow Collector" or "Shadow Caster" pass.
    // Although we haven't explicitly done so in this shader, if these passes are missing they will be read from a fallback
    // shader instead, so specify one here to import the collector/caster passes used in that fallback.
    //
    // 7.) レシーブまたはキャストシャドウのために、シェーダは"Shadow Collector"か"Shadow Caster"パスを適切に実装しなければなりません。
    //     しかしこのシェーダでは明示的にそれをしていませんが、これらのパスが見つからない場合はフォールバックシェーダが代わりに読み込まれます。
    //     フォールバックで使用されるcollector/casterパスをインポートするには、ここでそれを指定します。
    Fallback "VertexLit"

}

コメントによってだいぶボリュームが増えていますが、実際のシェーダコードはそんなに多くはありません。
このシェーダによってなにが行われているかを簡単に説明すると、通常の頂点変換とライトの強さなどを知るための値を構造体に設定してフラグメントシェーダに送り、その値を元に影の強さを元にフラグメントの色を決める、という処理になります。

基本的に影になっていない部分は影の濃さが0になるため、結果的に「なにも描かない」ことになり、逆に影になっている部分は0以外の値になるため、濃さに応じて色が描かれる、というわけです。

その他の細かな点については、コメントをご覧ください。
元の英語のコメントに加え、自分が英訳したものも追記してあります。

ソフトパーティクルの仕組みを応用した表現

概要

ソフトパーティクルの仕組みを応用した表現について色々とメモしておこうと思います。
(よく利用しようと思うこともあるものの、毎回調べたり、というのがめんどくさいので)

ちなみにソフトパーティクルとは、いわゆるパーティクルが通常のオブジェクトと重なる場合に、深度値を用いて重なり具合に応じてフェードを掛けることで、パーティクルの重なり部分をソフトに見せる(エッジを目立たなくさせる)目的で使われるものです。

これをパーティクル以外に利用することで、オブジェクトが他のオブジェクトに近い場合にフェードさせじんわりと重ねることができるようになります。

なお、今回も下記の凹みさんの記事を大いに参考にさせていただきました。
(いつもありがとうございます)

tips.hecomi.com

実際に実行すると以下のようになります。(右はワイヤーフレームを追加したもの)

f:id:edo_m18:20190730081733p:plain

コード解説

さて、まずはコード全容から。
コード量はそこまで多くありません。

Shader "Unlit/SoftParticle"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _InvFade("Inv fade", Range(0, 1)) = 1
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue"="Transparent" }
        LOD 100

        Pass
        {
            Blend SrcAlpha OneMinusSrcAlpha

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
                float4 projPos : TEXCOORD1;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float _InvFade;

            // デプステクスチャの宣言
            UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture);

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);

                // ワールド空間座標を元に、スクリーンスペースでの位置を求める
                o.projPos = ComputeScreenPos(o.vertex);

                // 求めたスクリーンスペースでの位置のz値からビュー座標系での深度値を求める
                COMPUTE_EYEDEPTH(o.projPos.z);

                o.uv = TRANSFORM_TEX(v.uv, _MainTex);

                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);

                // 深度バッファからフェッチした値を使って、リニアな深度値に変換する
                float sceneZ = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.projPos)));
                float partZ = i.projPos.z;

                // フェード処理
                float fade = saturate(_InvFade * (sceneZ - partZ));
                col.a *= fade;

                return col;
            }
            ENDCG
        }
    }
}

ソフトパーティクルを実現するための処理を、順を追って見ていきましょう。

ComputeScreenPos

ComputeScreenPosの定義は以下。

inline float4 ComputeScreenPos(float4 pos) {
    float4 o = ComputeNonStereoScreenPos(pos);
#if defined(UNITY_SINGLE_PASS_STEREO)
    o.xy = TransformStereoScreenSpaceTex(o.xy, pos.w);
#endif
    return o;
}

// -------------------

inline float4 ComputeNonStereoScreenPos(float4 pos) {
    float4 o = pos * 0.5f;
    o.xy = float2(o.x, o.y*_ProjectionParams.x) + o.w;
    o.zw = pos.zw;
    return o;
}

シングルパスステレオレンダリングかどうかで分岐が入っていますが、今回はソフトパーティクルの説明なのでこちらは割愛します。(基本概念が分かればVR対応も容易いと思うので)

ComputeNonStereoScreenPosの引数に渡ってくるposは、UnityObjectToClipPosでクリップ座標空間に変換された位置、すなわち-w ~ wの範囲(グラフィックスAPIによっては0 ~ w)に変換された値が渡ってきます。

冒頭の計算が行っていることを、細かいことを省いてやっていることだけに着目すると、

float4 o = pos * 0.5f;
o.xy = o.xy + o.w;

ということです。
ここで行っているのは-w ~ wを半分にして(-0.5w ~ 0.5w)、0.5wを足す、つまり0 ~ wの範囲に変換している、ということです。

ちなみに_ProjectionParams.xyに掛けているのはプラットフォームごとの違いを吸収する目的で行っているのみで、計算の本質に違いはありません。

そして最後に、半分にしてしまったzwの値をもとに戻す目的でo.zw = pos.zw;を再代入しています。


余談

wの意味。

3Dグラフィクスでは3D空間を正規化デバイス座標系という座標系に変換し、そののちにビューポート(つまりディスプレイ)座標に変換して画面に映像を出力します。

この正規化デバイス座標系では頂点xyzの値が-1 ~ 1に変換されます。 (グラフィクスAPIによっては0 ~ 1になる) そのために正規化と呼ばれるわけですね。

そしてこの-1 ~ 1に変換するために使われる値がwなのです。
具体的にはwで除算することで-1 ~ 1に変換します。

本来ならこのwで除算の部分はGPU側が自動で行ってくれるため、シェーダ内では-w ~ wの間になる状態までの計算に留めておいています。

ただ今回の例では、さらにそこからシェーダ内で深度値を利用するため自分でwで除算して値を求めているというわけです。

このあたりの詳しい説明はマルペケさんのこちらの記事(その70 完全ホワイトボックスなパースペクティブ射影変換行列)に詳しく書かれているのでご覧ください。

またパースペクティブ行列関連については以前、Qiitaにまとめているので詳しくはそちらをご覧ください。

qiita.com


COMPUTE_EYEDEPTH

次にCOMPUTE_EYEDEPTHの定義を見てみましょう。

#define COMPUTE_EYEDEPTH(o) o = -UnityObjectToViewPos( v.vertex ).z

inline float3 UnityObjectToViewPos( in float3 pos )
{
    return mul(UNITY_MATRIX_V, mul(unity_ObjectToWorld, float4(pos, 1.0))).xyz;
}

COMPUTE_EYEDEPTHはマクロとして定義されており、定義からは引数に指定された変数へ計算結果を格納する形になっています。
そして計算自体はUnityObjectToViewPosによって行われ、その結果のz値が代入されています。

UnityObjectToViewPos

続けてUnityObjectToViewPosを見てみましょう。

こちらはインライン関数になっていて引数にfloat3を受け取ります。
注意してほしいのは、COMPUTE_EYEDEPTHのマクロではv.vertexという変数が宣言されている前提で計算が行われている点です。

これは頂点シェーダに渡ってきた頂点そのものです。
そしてこれを加工した結果のz値を使っているわけです。

計算式を改めて見てみると、

return mul(UNITY_MATRIX_V, mul(unity_ObjectToWorld, float4(pos, 1.0))).xyz;

となっています。
ワールド座標への変換とビュー座標への変換を行っています。
つまり、引数に与えられた頂点位置をビュー座標空間へ変換しているだけですね。

そしてCOMPUTE_EYEDEPTHは「そのz値を格納している」と書きました。

これはカメラから見た頂点のz値を計算しているわけです。

デプスの算出

次に見るのはデプスの算出部分です。
コードを抜粋すると以下の箇所。

float sceneZ = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.projPos)));

UNITY_PROJ_COORD

順番に見ていきましょう。
UNITY_PROJ_COORDの定義は以下です。なにもせずにそのまま値を返しています。
凹みさんの記事によるとPS Vitaのときだけ違う計算が行われるようですが、基本的にここは無視してよさそうです。

#define UNITY_PROJ_COORD(a) a

SAMPLE_DEPTH_TEXTURE_PROJ

次はSAMPLE_DEPTH_TEXTURE_PROJ
これも定義を見てみましょう。

#   define SAMPLE_DEPTH_TEXTURE_PROJ(sampler, uv) (tex2Dproj(sampler, uv).r)

内容はシンプルですね。
tex2Dprojのラッパーになっていて、さらにフェッチしたテクスチャの値のr成分だけを返しています。

元々引数に取るテクスチャはデプステクスチャなので、対象のr成分だけで大丈夫なわけですね。

ちなみにtex2Dprojについてですが、以前書いた以下の記事で少しだけ言及しています。

edom18.hateblo.jp

引用すると以下のような計算を行っています。

tex2Dprojは、該当オブジェクトにテクスチャを投影するような形でテクセルをフェッチします。 つまり、同次座標系で見た場合に、該当のテクセルがどうなるか、を計算しているわけです。

具体的には、以下のように自前で計算することでも同じ結果を得ることができます。

float2 uv = i.uvgrab.xy / i.uvgrab.w;
half4 col = tex2D(_GrabTexture, uv);

要は、Z方向の膨らみを正規化することで2D平面(ディスプレイ)のどの位置に、該当オブジェクトのピクセルがくるのか、を計算しているわけですね。

イメージ的には以下の図のような感じです。

f:id:edo_m18:20190729114442p:plain

これは以前書いた以下の記事から引用したものです。

qiita.com

これでテクスチャからの値を取得することができました。
しかしまだこのままの値では、前段で説明した「ビュー空間でのz値」と組み合わせて計算を行うことができません。

なぜなら、テクスチャからフェッチした値はまだビュー空間での値ではないからです。
そこで利用するのが最後のLinearEyeDepthです。

LinearEyeDepth

こちらもさっそく定義を見てみましょう。

inline float LinearEyeDepth( float z )
{
    return 1.0 / (_ZBufferParams.z * z + _ZBufferParams.w);
}

計算自体はシンプルですが、ぱっと見なにをしているか分かりませんね。
まず_ZBufferParamsがなにかを突き止めましょう。ドキュメントは以下です。

docs.unity3d.com

ドキュメントによると以下のように記述されています。

Z バッファ値をリニア化するために使用します。 x は (1-far/near)、 y は (far/near)、 z は (x/far)、 w は (y/far) です。

これを展開して整理すると以下のような計算を行っていることになります。


\begin{eqnarray}
d &=& \frac{1.0}{\frac{x}{far} * z + \frac{y}{far}}  \\\
&=& \frac{1.0}{\frac{xz + y}{far}}  \\\
&=& \frac{far}{xz + y}
\end{eqnarray}

値の意味は分かりましたが、なぜこんな計算になるのでしょうか。
これを紐解くために、いつもお世話になっているマルペケさんの以下の記事を参考に考えてみます。

marupeke296.com

マルペケさんの記事にも書かれている通り、深度値は線形な変化をしません。むしろ極端な変化をします。
マルペケさんの記事から画像を引用させていただくと、以下のように極端なグラフになります。

深度値グラフ

だいぶ極端なグラフです。
これを線形に戻すということは、このグラフを求める計算の逆関数を求めればいいことになります。

ただ、マルペケさんの記事で紹介されていた計算式の逆関数を求めてもLinearEyeDepthの実装のようにはならず、クロスプラットフォーム対応など他の要素の兼ね合いでこうなっていると思うので詳細までは分かりませんでした;(詳しい方いたらコメントください・・・)

とはいえ、ここで行っているのはこの極端なグラフを線形に変換するのでLinearEyeDepthという名前がついているということですね。


余談

ちなみにマルペケさんの記事で紹介されていた式は以下です。


d = \frac{fZ}{z} \biggl(\frac{z - nZ}{fZ - nZ}\biggr)

これの逆関数は以下になりました。


f^{-1}(x) = \frac{-nZfZ}{x(fZ - nZ) - fZ}

デプスを用いたフェード処理

最後の部分は実際に計算を行っている部分です。
コードを抜粋すると以下の部分ですね。

float sceneZ = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.projPos)));
float partZ = i.projPos.z;

// フェード処理
float fade = saturate(_InvFade * (sceneZ - partZ));
col.a *= fade;

LinearEyeDepthを用いて深度バッファから得た深度値を線形に変換し、さらに今計算中のフラグメントの深度値を利用してフェードする係数として利用しています。(sceneZ - partZ

_InvFadeはどの程度フェードさせるかをプロパティから設定するための変数です。
これを0 ~ 1の範囲にクランプし、アルファに乗算してやることで今回のフェードを実現している、というわけです。

depthTextureModeをオンに

プラットフォームの設定などによってはなにもしないとDepth Textureが取れない場合があるので、その場合は明示的にオンにしてやる必要があります。
具体的には以下のようにスクリプトから設定してやる必要があります。
(なにか適当なスクリプトから、メインカメラに対して以下を実行してやればOKです)

GetComponent().depthTextureMode |= DepthTextureMode.Depth;

最後に

色々と深度値周りの計算について見てきました。
利用するにあたっては以下の2点の違いに注意が必要です。

  • 通常の計算で出た深度値
  • 深度バッファから得られる値

ただ、これが分かってしまえばあとは単純な数値比較のみになるので、実際の計算自体はむずかしくないと思います。

またソフトパーティクル以外でも、深度値を取得してなにかをするというのは利用価値があると思うので覚えておくといいテクニックですね。

Magic Leap One開発入門

概要

Magic Leap Oneの開発を始めたのでそれに対する諸々をメモしておこうと思います。
モバイル開発同様、ちょっとした設定や証明書の設定など初回しかやらないことなどは忘れがちなので。

※ なお、本記事はUnity2019の、Lumin OS向けの開発がデフォルトで採用されたあとの話となります。
※ 今回追記した内奥はUnity2019.2.15f1で実行し、Magic Leapのバージョンは0.98.0です。

まずはCreator Portalにログイン

まずはMagic Leapの開発者サイトであるCreator Portalにログインします。
ドキュメントやMagicLeap向け開発のセットアップ方法などの情報、またSDKのダウンロードなどが行えるので必須です。

creator.magicleap.com

UnityのプロジェクトをMagic Leap向けに設定する

公式のドキュメントは以下を参照ください。

developer.magicleap.com

またUnityに対するドキュメントはこちらにあります。

Unity2019以降をインストール

Magic Leap開発をするにはUnity2019以降のバージョンが必要です。それ以降のバージョンであればサポートプラットフォームにLumin OSが含まれています。

Settingsを調整

プラットフォームをLumin OSに

新規プロジェクトを作成、Unityを起動する際、プラットフォームをLumin OSにします。

ライブラリ・ツールをインストール

The Labをインストール

以前はMagic Leap Package Managerというアプリから諸々インストールなどを行っていたのですが、現在はThe Labというアプリからインストールやダウンロードを行うようになっています。
なのでまずはこのアプリをインストールします。

パッケージをインストール

XR Managementをインストール

まず、Package Manager WindowからXR Managementをインストールします。

f:id:edo_m18:20200128162530j:plain

インストールを行うと「Project Settings」に以下のような項目が追加されます。

f:id:edo_m18:20200128162645j:plain

ここから、Magic Leap Loaderという項目を追加します。(初回はダウンロードが必要です)
また同時に、Input Handlerという項目を選択すると必要なパッケージがまだインストールされていない場合はボタンが表示されるのでそこからインストールを行います。

MLRemote用ライブラリをインポート

もし以前のものを使っている場合は新しくインポートし直したほうがいいかもしれません。
以下のように、Magic Leap > MLRemote > Import Support Librariesから必要なライブラリをインポートします。

f:id:edo_m18:20200128162856j:plain

ここまでのセットアップが終わったら、Launch Zero Iterationを実行することでZero Iterationアプリが起動します。

起動後、実機を持っている人はターゲットを実機に変更する必要があります。以下の図の部分を対象デバイスに変更してください。

f:id:edo_m18:20200128163301j:plain

あとはEditor上でプレイボタンを押すと実機で確認することができるようになります。

Lumin SDKをPreferenceに設定

次に、Preferenceから、Magic Leap向けのSDKを設定します。

f:id:edo_m18:20190710133652p:plain

Build Settingsを設定

Player SettingsColor SpaceLinearに変更します。
次に、XR SettingsVirtual Reality Supportedをオンにし、プラットフォームにLuminを追加します。
またStereo Rendering ModeSingle Pass Instancedに変更します。

パッケージのインポートとPrefabの配置

Magic LeapのUnityパッケージをインポートする

Magic LeapのUnityパッケージは、上記ポータルからDownload / InstallしたMagic Leap Package Managerを起動するとパッケージのDownloadなどができます。

起動し、インストールが済むと以下のような画面にUnityパッケージが保存されている場所が表示 されるので、そこからパッケージをインポートします。

f:id:edo_m18:20190710132640p:plain

Magic Leap向けのカメラPrefabをシーン内に配置する

該当のPrefabはAssets/MagicLeap/CoreComponents/内にあります。

f:id:edo_m18:20190528133825p:plain

アプリに署名する

Magic Leapのアプリをビルドするために、アプリに署名をする必要があります。
署名するためには証明書を作成し、適切に設定します。

Magic Leapのポータルにログインすると、証明書を作成するページがあるのでそこでIDなどを登録します。

f:id:edo_m18:20190710171053p:plain

すると、秘密鍵などがまずダウンロードされます。
その後しばらくしてページをリロードすると、上記画像のように右側のダウンロードボタンから証明書をダウンロードすることができるようになります。

それを最初にダウンロードされたフォルダに入れ、そのフォルダごとUnityのプロジェクトに追加します。
ちなみにAssets配下である必要はないので、同階層などに置いておくといいと思います。

その後、Player SettingsのPublishing Settingsで上記の証明書を設定します。

f:id:edo_m18:20190710171211p:plain

コントローラを使う

コントローラのイベントをトラッキングする

UnityEngine.XR.MagicLeap namespaceにあるMLInputを利用します。
以下は簡単に、トリガーのDown / Upのイベントを購読する例です。

using UnityEngine.XR.MagicLeap;

private void Start()
{
    MLInput.Start();
    MLInput.OnTriggerDown += OnTriggerDown;
    MLInput.OnControllerButtonDown += OnButtonDown;
}

private void OnTriggerDown(byte controllerId, float triggerValue)
{
    // do anything.
}

private void OnButtonDown(byte controllerId, MLInputControllerButton button)
{
    // do anything.
}

コントローラのバイブレーションを利用する

バイブレーションを利用するにはMLInputControllerStartFeedbackPatternVibeメソッドを使います。

private MLInputControllerFeedbackPatternVibe _pattern = MLInputControllerFeedbackPatternVibe.ForceDown;
private MLInputControllerFeedbackIntensity _intensity = MLInputControllerFeedbackIntensity.Medium;

// ... 中略 ...

MLInputController controller = _controllerConnectionHandler.ConnectedController;
controller.StartFeedbackPatternVibe(_pattern, _intensity);

コントローラを使ってuGUIを操作する

MagicLeapのSDKの中にExamplesがあるので、それをベースにセットアップするのが早いでしょう。

CanvasのRender ModeをWorld Spaceに変更する

Magic Leapでは、uGUIを空間に配置する必要があるため、uGUIのCanvasのRender ModeをWorld Spaceに変更する必要があります。

ポイントとしては、uGUIのEventSystemオブジェクトにMLInputModuleコンポーネントを追加します。
またそのコンポーネントに、対象となるCanvasを設定します。
どうやら、Lumin SDK 0.21.0からはこの設定はいらなくなったようです。

ちなみに、0.20.0の場合は以下のように設定項目があります。

f:id:edo_m18:20190529133115p:plain

また、対象シーンにあるControllerオブジェクトをシーン内に配置します。
いちおうこれだけでも動作しますが、レーザーポインタみたいなオブジェクトなどは表示されないのでちょっと操作しづらいです。
なので、同シーンに配置されているInputExampleコンポーネントを利用するとそれらが視覚化されます。

ただ、Exampleと名前がついているので、これを複製して独自にカスタムしたほうがよいでしょう。

複数Canvasを使う場合

前述のように、MLInputModuleCanvasを設定する必要があります。
しかし複数のCanvasがシーン内にある場合は、そららを設定することができません。

こちらも前述のように、Lumin SDK 0.21.0からは不要となりました。

0.20.0時代に調べていたら、Magic Leapのフォーラムでまさに同様なことが語られていました。(フォーラムは以下)

forum.magicleap.com

どうやら、Canvasに対してMLInputRaycasterをアタッチすることで複数Canvasでも問題なく動作させることができるようです。
このMLInputRaycasterをアタッチするのは0.21.0でも同様に必要なようです。

ハンドトラッキングを使う

MLにはハンドトラッキングの機能も標準で搭載されています。

ハンドトラッキングを開始する

まずはハンドトラッキングを開始するためにMLHands.Start();を実行します。
実行に失敗したかをチェックして、問題がなければハンドトラッキングが開始されます。

MLResult result = MLHands.Start();

if (!result.IsOk)
{
    Debug.LogErrorFormat("Error: HandTrackingVisualizer failed starting MLHands, disabling script. Reason: {0}", result);
    enabled = false;
    return;
}

ハンドトラッキングを検知する

まず、(必要であれば)MLHandType型のプロパティを用意し、どちらの手のトラッキングをするかを決められるようにしておきます。

private MLHand Hand
{
    get
    {
        if (_handType == MLHandType.Left)
        {
            return MLHands.Left;
        }
        else
        {
            return MLHands.Right;
        }
    }
}

こんな感じ。

そして、対象の手の状態がenumで取得できるので、以下のように評価します。

if (Hand.KeyPose == MLHandKeyPose.Thumb)
{
    // do something.
}

上の例ではサムズアップの状態になったら呼ばれるようにしています。
こんな感じで、MagicLeapが用意してくれている手の形を検知するとそれを知ることができるので、それに応じて処理を分岐させます。

レンダリング

MagicLeapではStereo Rendering ModeにSingle Pass Instancedを使うことができます。
ただこれを利用すると、自作シェーダなどの場合はSingle Pass Instancedに対応した形にしないと正常に動作しなくなります。

このあたりについては凹みさんの記事に詳細が書かれているのでそれを参考にさせてもらいました。
ここではポイントだけ記述します。詳細について知りたい方は凹みさんの記事をご覧ください。

tips.hecomi.com

tips.hecomi.com

シングルパス対応のためにIDを適切に扱う

そもそもなぜ、Single Pass Instancedにすると正常に描画されないのでしょうか。
その理由は、左右の目用のレンダリングを一度、つまりシングルパスで行うためそれに対応する処理を追加しなければならないためです。

より具体的に言えば、GPU Instancingを利用してオブジェクトを1ドローコールで両目用にレンダリングします。また、レンダーターゲットアレイというものを利用してレンダーターゲットを複数(左右の目分)用意しそれを利用して描画します。

つまり左右の目それぞれのオブエジェクトごとに固有の行列などを利用する必要があり、それを適切にセットアップしないとならないのがその理由です。

シェーダ内部ではunity_InstanceIDというstatic変数経由で、現在レンダリング中のオブジェクトの配列のインデックスを取得します。
つまりはこれを適切にセットアップし、配列から情報を取り出すことができれば正常にレンダリングされるようになる、というわけです。

コードセットアップ

なぜこれらの処理が必要なのかは上で紹介した凹みさんの記事にとてもとても詳しく書いてあるので、内部的にどういうことをやっているのかを知りたい方は凹みさんの記事を参考にしてください。

ここではベースとなるシンプルなシェーダに追記していく形で、ざっくりとだけまとめます。

ということで、まずはUnlitなシンプルなシェーダを載せます。
これは、Unityで「Create > Shader > UnlitShader」として生成されたものから、Fog関連の記述を消したものです。

Shader "Unlit/Sample"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                return col;
            }
            ENDCG
        }
    }
}

これをそのままマテリアルにして適用するとSingle Pass Instancedの設定の場合は片目になにも描画されなくなります。

pragmaを設定する

まず#pragma multi_compile_instancingを追加します。

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_instancing // 追加

これを追加すると、以下のようにマテリアルのインスペクタにEnable GPU Instancingという項目が追加されます。(当然チェックを入れます)

f:id:edo_m18:20190711165358p:plain

これでGPU Instancingを利用する準備ができました。
以下から、このインスタンシングを利用するためのコードに変更していきます。

コードをインスタンシング対応のものにする

手始めに頂点/フラグメントシェーダの入力の構造体にマクロを追加します。

struct appdata
{
    float4 vertex : POSITION;
    float2 uv : TEXCOORD0;

    UNITY_VERTEX_INPUT_INSTANCE_ID // 追加
};

struct v2f
{
    float2 uv : TEXCOORD0;
    float4 vertex : SV_POSITION;

    UNITY_VERTEX_INPUT_INSTANCE_ID // 追加
    UNITY_VERTEX_OUTPUT_STEREO     // 追加
};

これらはGPU Instancingを利用するにあたってインスタンスのIDを適切に扱うためのものになります。

そして次に頂点シェーダにもマクロを加えます。

v2f vert (appdata v)
{
    v2f o;

    UNITY_SETUP_INSTANCE_ID(v);               // 追加
    UNITY_INITIALIZE_OUTPUT(v2f, o);          // 追加
    UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); // 追加

    o.vertex = UnityObjectToClipPos(v.vertex);
    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    return o;
}

インスタンスIDのセットアップとフラグメントシェーダへの出力を設定します。

次に、コンスタントバッファの宣言を追加し、フラグメントシェーダで利用できるようにします。

UNITY_INSTANCING_BUFFER_START(Props)
    UNITY_DEFINE_INSTANCED_PROP(float4, _Color)
UNITY_INSTANCING_BUFFER_END(Props)

ちなみにコンスタントバッファ(定数バッファ)は、GLSLで言うところのuniform変数やuniform blockに相当するものです。(以下の記事を参考に)

docs.microsoft.com

なので、C#側から送る値だったりインスペクタで設定するプロパティだったりは、個別に必要なデータに関してはこのコンスタントバッファの定義方法を用いて適切に設定する必要があります。

具体的には、uniformとして定義する変数はほぼそれで定義しておくと考えるといいと思います。

最後にフラグメントシェーダです。

UNITY_SETUP_INSTANCE_ID(i);

// ... 中略 ...

col *= UNITY_ACCESS_INSTANCED_PROP(Props, _Color);

頂点シェーダから送られてきたインスタンスIDを取り出し、適切にパラメータを扱います。
上記のコンスタントバッファのところでも説明しましたが、通常のシェーダであればuniformな変数_Colorを定義しそれを利用するだけでよかったものを、上記のようにマクロを経由して使う必要がある、というわけです。

余談

ちなみに、凹みさんが記事を書いているときのUnityのバージョンの問題なのか、Unity2019.1.4f1では以下のようにしないとエラーになっていたので書き換えました。

        UNITY_INSTANCING_BUFFER_START(Props)
            UNITY_DEFINE_INSTANCED_PROP(float4, _Color)
        UNITY_INSTANCING_BUFFER_END(Props)

// ... 中略 ...

col *= UNITY_ACCESS_INSTANCED_PROP(Props, _Color);

マクロ展開後のコード例

最後に、上記のマクロを展開したらどうなるかをコメントしたコード全体を載せておきます。
なお、以下のコードの展開例はあくまで一例です。グラフィクスAPIやその他の設定に応じていくつかの分岐が存在するため、詳細について知りたい方はUnityInstancing.cgincHLSLSupport.cgincを適宜参照してください。

Shader "Unlit/Sample"
{
    Properties
    {
        _Color ("Color", Color) = (1, 1, 1, 1)
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_instancing

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;

                // uint instanceID : SV_InstanceID;が追加される
                UNITY_VERTEX_INPUT_INSTANCE_ID // 追加
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;

                // uint instanceID : SV_InstanceID;が追加される
                UNITY_VERTEX_INPUT_INSTANCE_ID // 追加

                // 以下のマクロを経由して「stereoTargetEyeIndexSV, stereoTargetEyeIndex」が追加される。
                // #define UNITY_VERTEX_OUTPUT_STEREO DEFAULT_UNITY_VERTEX_OUTPUT_STEREO
                // #define DEFAULT_UNITY_VERTEX_OUTPUT_STEREO uint stereoTargetEyeIndexSV : SV_RenderTargetArrayIndex; uint stereoTargetEyeIndex : BLENDINDICES0;
                UNITY_VERTEX_OUTPUT_STEREO     // 追加
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            // #define UNITY_INSTANCING_BUFFER_START(buf) CBUFFER_START(buf)
            // #define CBUFFER_START(name) cbuffer name { 
            UNITY_INSTANCING_BUFFER_START(Props)

                // #define UNITY_DEFINE_INSTANCED_PROP(type, var)  type var;
                UNITY_DEFINE_INSTANCED_PROP(float4, _Color)

            // #define CBUFFER_END };
            UNITY_INSTANCING_BUFFER_END(Props)

           // これを展開して整理すると以下のような形になります。
           // cbuffer UnityInstancing_Props { struct {
           //    fixed4 _Color;
           // } PropsArray[2]; }

            v2f vert (appdata v)
            {
                v2f o;

                // DEFAULT_UNITY_SETUP_INSTANCE_IDはいくつか定義が分散しているので詳細は「UnityInstancing.cginc」を参照。
                // #define UNITY_SETUP_INSTANCE_ID(input) DEFAULT_UNITY_SETUP_INSTANCE_ID(input)
                // #define DEFAULT_UNITY_SETUP_INSTANCE_ID(input) { UnitySetupInstanceID(UNITY_GET_INSTANCE_ID(input)); UnitySetupCompoundMatrices(); }
                UNITY_SETUP_INSTANCE_ID(v);

                UNITY_INITIALIZE_OUTPUT(v2f, o);

                // DEFAULT_UNITY_INITIALIZE_VERTEX_OUTPUT_STEREOもいくつか定義があるので「UnityInstancing.cginc」を参照。
                // #define UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output) DEFAULT_UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output)
                // #define DEFAULT_UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output) output.stereoTargetEyeIndexSV = unity_StereoEyeIndex; output.stereoTargetEyeIndex = unity_StereoEyeIndex;
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);

                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                UNITY_SETUP_INSTANCE_ID(i);

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

                // #define UNITY_ACCESS_INSTANCED_PROP(arr, var) arr##Array[unity_InstanceID].var
                col *= UNITY_ACCESS_INSTANCED_PROP(Props, _Color);

                return col;
            }
            ENDCG
        }
    }
}

開発環境についてのメモ

Magic Leap Remote for Unityを使って実機でプレビューする

まずはドキュメント。

creator.magicleap.com

Magic Leap Remoteを起動する

Remoteでの確認をするのに、専用のアプリを利用します。
ドキュメントは以下。

Developer Portal | Magic Leap

適切にセットアップが終わっていれば、Magic Leap Package Managerがインストールされているはずなので、その中のLumin SDKをリストから選択、さらにその後Use ML Remoteボタンを押してアプリを起動します。

f:id:edo_m18:20190528161618j:plain

また、Unity Editorから実機に転送できるようセットアップを行います。

Player Settingsから、Windows向け設定のAuto Graphics API for Windowsのチェックをはずし、OpenGL Coreをリストに追加します。

f:id:edo_m18:20190528162003g:plain

続いて、必要なパッケージをインポートします。
Magic Leap向けのプラグインをインポートしている場合は、メニューにMagic Leap用のものが追加されているので、そこから必要なパッケージをインポートすることができます。

Magic Leap > ML Remote > Import Support Librariesに該当のライブラリがあります。

f:id:edo_m18:20190528162232p:plain

そして同じメニュー内にMagic Leap > ML Remote > Launch MLRemoteと、MLRemoteを起動する項目があるので起動します。

起動すると以下のようなウィンドウが表示されるので、Start Deviceボタンを押下して実機に接続します。

f:id:edo_m18:20190528163228j:plain

あとはUnity Editorのプレイモードに入れば、自動的に描画結果が実機に転送されプレビューできるようになります。

コマンドラインを扱う

Magic Leap OneのSDKにはコマンドラインツールも含まれています。

利用するのはmldb.exeです。
コマンド自体は以下のようなパスに保存されています。(デフォルト設定の場合)

C:\Users\{USER_NAME}\MagicLeap\mlsdk\{VERSION}\tools\mldb\

接続されているデバイスのリストを表示する

$ mldb devices

コマンドラインからmpkファイルをインストールする

mpkファイルをコマンドラインからインストールするには以下のようにします。

$ mldb install /path/to/any.mpk

また、すでにインストール済のものを上書きインストールする場合は-uオプションを使用します。

$ mldb install -u /path/to/any.mpk

コマンドでできることをまとめてくれているサイトがあったので紹介しておきます。

littlewing.hatenablog.com

UnityのProfilerにつなぐ

こちらのフォーラムの質問にありました。

引用させてもらうと、以下のようにすることでUnityのプロファイラに接続することが出来ます。

Here’s my steps to profile:

  1. In a console: mldb forward tcp:55000 tcp:55000
  2. Build Settings: Check Development Build and Autoconnect Profiler
  3. Build app, deploy and run on device
  4. Open Profiler (Window > Analysis > Profiler) Editor > 127.0.0.1

その他Tips

Lumin OSを選択している場合のPlatform Dependent Compilation

PLATFORM_LUMINを利用する。

#if PLATFORM_LUMIN
// for lumin
#endif

Magic Leap Oneの映像をモバイルのコンパニオンアプリにミラーリングする

まだbeta版のようですが、コンパニオンアプリを使うことでミラーリングすることができます。
(ただし、コンパニオンアプリはアメリカのStoreでしか落とせないのでちょっとごにょごにょしないと手に入りません。無料です)

www.magicleap.care

ハマった点

ARKit関連でLinker error

ARKitを使っているARプロジェクトなどをMagic Leapに移植しようとして、ARKit関連のSDKが残ったままだと以下のようなエラーが出てしまいます。

In function `UnityARVideoFormat_EnumerateVideoFormats_m1076262586' : undefined reference to `EnumerateVideoFormats'

利用している箇所で#if UNITY_EDITORのみとなっている箇所が、Luming OSプラットフォームだとDLLを参照しにいこうとしてコケるやつです。
なので、分岐を追加することで回避できます。

こちらの記事にも似たようなことが書かれています。
(ただ、バージョン違いなのか自分の環境ではARVideoFormat.csというファイル名でした)

bitbucket.org

PlacenoteなどのライブラリをSymboliclinkで参照を作る

上記と似たような問題ですが、今回の開発では元々ARKit向けに作っていたものを改修する形で対応しました。
なので元々ARKit用のプラグインなどが入っていて、いくつかのライブラリに関してはそのままでも大丈夫だったのですが、場合によってはビルドがまったくできなくなってしまいます。

そこで、各プラットフォームごとに必要なSDKなどをSymboliclinkにして読み込ませる、という方法を取りました。

もっとスマートなやり方がある気もしますが、ビルド時に対象フォルダを外す、などはあまり簡単にできそうではなかったのでこの方法を選択しました。
(もし他の方法を知っている人いたら教えてください)

$ new-item -itemtype symboliclink -path D:\MyDesktop\UnityProjects\078_ar_city\Assets -name Placen
ote -value D:\MyDesktop\UnityProjects\078_ar_city\Placenote

satococoa.hatenablog.com

ネットワークで通信ができない

最初にビルドしたときはできていた気がしたんですが、途中からなぜかネットワークに接続できない現象が。

調べてみたら、以下の記事がヒット。
ただ、最終的にはマニフェストファイルで解決したんですが、ランタイムで個別に権限の確認などが行えるスクリプトが最初から用意されているらしく、メモとして残しておきます。

medium.com

簡単に説明しておくと、Privilege RequesterというコンポーネントをつけてLocal Area Networkを追加するだけでいいようです。

OSやSDKのupdateに伴う変更

Magic LeapにはPackageManagerがあるので、それでSDKなどのバージョン管理などを行います。
なのでアップデートがあったときはそこからインスールし、さらにバージョン管理も(フォルダ分けも)自動的に行ってくれるので非常に楽です。

が、Unity側で指定しているSDKのパスは、エディタ上で設定を変更しないとなりません。
設定自体は特にむずかしいことはないのですが、しばらく開発をしていて久々にアップデートした際に忘れがちになるので、メモとして残しておきます。

設定自体はAndroid SDKなどと同様に、Editor > Preferencesから開く設定画面で、以下の箇所に設定項目があります。

f:id:edo_m18:20190603140823p:plain

イベントを登録しているとuGUIへのイベントが発火しない?

ちょっとまだしっかりと調査していないのですが、ちょっとハマったのでメモ。
MLInputの各種イベントを購読して処理をしていたら、その処理を追加したあとなぜかuGUIへのイベントの発火が止まってしまい、uGUIを操作できなくなった。

処理終了後にそれらのイベントを購読解除したら正常に動いた。

最後に

ARKitを用いたモバイルARと、いわゆるARグラスを用いたAR体験はだいぶ色々なものが異なるので、プロジェクト的に一緒に開発していくのはややきびしいかもなーというのが正直な感想でした。

一番感じた点としては、ARKitなどのモバイルAR開発であったとしても、ARグラス向けを意識して開発しておくと後々幸せになれるかもしれません。(Hololens2も控えているし)

Swiftでネイティブプラグインを作る

概要

以前、Objective-Cを用いたプラグイン生成については記事を書きました。

edom18.hateblo.jp

今回はSwiftを用いたプラグイン作成方法です。
Unity側で準備する内容は基本的には大きく違いませんが、Xcode側でやることあったり、SwiftとObjective-Cとの連携周りについての調整が多く、そのあたりを中心にまとめておきたいと思います。

ちなみに、大本はこちらの記事を参考にさせていただきました。

qiita.com

SwiftからObjCを呼び出すためのBridge用ファイルを生成・設定する

通常、Xcodeプロジェクトから生成したものであればブリッジが必要になったタイミングでXcodeが聞いて自動的に生成してくれるようですが、Unityからだとそれが行われないので自分で追加、設定する必要があります。

ファイル名は任意で、必要な項目にそのファイルを設定するだけです。

設定箇所は「Swift Compiler - General > Objective-C Bridging Header」です。

$(SRCROOT)とするとプロジェクトフォルダまでのパスを設定してくれるので、あとは作成したファイル名を続けて入力すると無事に設定されます。

$(SRCROOT)/Bridging-Header.h

f:id:edo_m18:20190606170511p:plain

ヘッダファイルの中身は、Swiftから呼ばれる可能性のあるものをインポートしておきます。

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "UnityInterface.h"

Objective-CからSwiftのメソッドを呼び出す

Swiftのバージョンなどによって挙動が違うようですが、Swift4から(?)明示的に書かないとObjCに対して公開されないようになったようです。
そのため、ObjCに対して公開する場合は@objcをつけて宣言する必要があるようです。

class ExampleClass : NSObject {
    @objc static func swiftMethod(_ message: String) {
        print("\(#function) is called with message: \(message)")
    }
}

ObjCから呼び出している箇所はこんな感じ↓

extern "C" {
    void _ex_callSwiftMethod(const char* message) {
        [ExampleClass swiftMethod:[NSString stringWithUTF8String:message]];
    }
}

ちなみに、この@objcが必要な理由はこちらの記事で解説されているので、興味がある人は読んでみるといいかもです。

qiita.com

Objective-CからSwiftのクラスをイニシャライザを使って生成する

例えば以下のようなSwiftクラスを定義したとします。

public class SwiftClass : NSObject {
    var name: String?
    var age: Int?
    @objc public init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

これを、Objective-Cで利用するには以下のようにします。

SwiftClass *aClass = [[SwiftClass alloc] initWithName:@"hoge" age:@20]

Tips

< Product_Module_Name>-Swift.hCommand + Left Clickすると定義に飛べるので、自動生成したヘッダファイルを見て、Swiftのメソッドがどうエクスポートされているかを確認するとエラー解消が早いかもしれません。
Product_Module_NameはBuild Settingsで確認することができます)

ちなみに、以下のようなクラス・メソッドの場合のエクスポート例です。

Swiftの定義は以下。

import Foundation

public class Callback : NSObject {
    var objectName: String?
    var methodName: String?
    
    @objc public init(objectName: String, methodName: String) {
        self.objectName = objectName
        self.methodName = methodName
    }
    
    @objc public func Picked(date: String) {
        UnitySendMessage(objectName, methodName, date)
    }
}

ヘッダには以下のように書き出される。

@interface Callback : NSObject
- (nonnull instancetype)initWithObjectName:(NSString * _Nonnull)objectName methodName:(NSString * _Nonnull)methodName OBJC_DESIGNATED_INITIALIZER;
- (void)PickedWithDate:(NSString * _Nonnull)date;
- (nonnull instancetype)init SWIFT_UNAVAILABLE;
+ (nonnull instancetype)new SWIFT_DEPRECATED_MSG("-init is unavailable");
@end

Objective-Cからはこんな感じで呼び出します。

Callback *callback = [[Callback alloc] initWithObjectName:objStr methodName:methodStr];
[callback PickedWithDate:@"2019/06/07"];

その他気になったところ

Swift Versionの設定

上記設定をしてビルドをしようとすると、Swiftのバージョンについての問題を指摘されてエラーとなりビルドが失敗します。

設定項目があってそこを設定するだけなので、該当エラーが出たら以下の箇所を適切に設定してください。

f:id:edo_m18:20190606172936p:plain

stackoverflow.com

uGUIの背景をぼかしてオシャレに見せる

概要

最近、Apex Legendsにハマって毎日のように時間が吸われていってます。(まずい)

さて、今回はこのApex LegendsのUIで使われているような「背景がボケているUI」を作る方法を書いていきたいと思います。

↓こんな感じで、背景が透過+ボケている「すりガラス」風のUIですね。
f:id:edo_m18:20190402152514p:plain

↓実際に実装した動画はこんな感じ

背景がぼけているだけでなんか途端にオシャレ感でますよねw

今回のサンプルはGithubにアップしてあります。

github.com



フロー

今回の実装のフローは大まかに以下のような感じです。

  1. CommandBufferを使って画面をキャプチャする
  2. キャプチャした画像をブラー用のシェーダで加工する(※1)
  3. ブラーをかけた画像をuGUI用シェーダで合成する(※2)

という流れになります。

※1 ... ブラー画像を生成する方法については以前の記事で書いたのでそちらを参照ください。

edom18.hateblo.jp

CommandBufferを使って画面をキャプチャしブラーをかける

まず最初に行うのはCommandBufferによる画面キャプチャです。

CommandBuffer自体については以下のドキュメント凹みさんの記事にさらに詳しく書いてあるのでそちらも参考にしてみてください。

docs.unity3d.com

tips.hecomi.com

CommandBufferとは

CommandBufferとは、ドキュメントから引用すると以下のように記述されています。

これは、いわゆる “command buffers” で、Unity の Unity のレンダリングパイプライン を拡張することができます。 コマンドバッファがレンダリングコマンド(“set render target, draw mesh, …”)のリストを保持し、 カメラがレンダリング中にさまざまなポイントで実行するように設定することができます。

GPUは「コマンドバッファ」と呼ばれる、CPUからGPUに対して命令を送るためのバッファの仕組みがあります。いわゆると書いているのは、まさにこのことと同じことを指しているのだと思います。

なお、このあたりのGPUのコマンドバッファに関しては以下の記事が分かりやすいでしょう。興味がある人は読んでみてください。

blogs.msdn.microsoft.com

コマンドバッファとは、ざっくりと言うとCPUからGPUへ「こういう処理をしてから描画をこういうデータでやってね」という「コマンドを積み重ねたバッファ」、ということができます。


余談

余談ですが、なぜコマンドバッファという仕組みがあるのでしょうか。
CPUとGPUではその処理速度や実行するタスクなどが違うため協調して動くことができません。

そのため、CPU側で描画に必要なデータを準備し必要なタスクの順番などを定義したあと、それらをGPUに「依頼」する必要があります。

実際の仕事に置き換えて考えてみると、専門的な仕事に関してはその専門家に依頼することが自然な流れでしょう。
そして依頼したあとはその仕事が終わるまで待つことはせず、自分の仕事に戻るのが普通です。

これと似たようなことがCPUとGPUとの間で行われているわけです。
つまり、コマンドバッファにはGPUにやってほしいことを列挙し、それをGPUに渡して「レンダリング」という専門タスクを依頼するわけです。

そうして仕事の例と同じように、GPUにタスクを依頼したあとはCPUは自分のタスクに戻ります。
これが、「コマンドバッファ」と呼ばれるタスクのバッファを橋渡しとして利用する理由です。

このあたりのハード面からの理屈やGPUの仕組みについては以下の書籍がとても参考になりました。
興味がある方は読んでみると、GPU周りについて詳しくなるのでオススメです。


閑話休題

Unityの場合はCommandBufferはカメラに対して設定するようになっています。
まずはコードを見てみましょう。

CommandBufferをセットアップしているC#コード

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

namespace Example.uGUI
{
    /// <summary>
    /// Screen capture using CommandBuffer.
    /// </summary>
    public class SceneBlurEffect : MonoBehaviour
    {
        [SerializeField]
        private Shader _shader;

        [SerializeField, Range(1f, 100f)]
        private float _offset = 1f;

        [SerializeField, Range(10f, 1000f)]
        private float _blur = 100f;

        [SerializeField, Range(0f, 1f)]
        private float _intencity = 0;

        [SerializeField]
        private CameraEvent _cameraEvent = CameraEvent.AfterImageEffects;

        private Material _material;

        private Dictionary<Camera, CommandBuffer> _cameras = new Dictionary<Camera, CommandBuffer>();

        private float[] _weights = new float[10];
        private bool _enabledBlur = false;
        private bool _isInitialized = false;

        public float Intencity
        {
            get { return _intencity; }
            set { _intencity = value; }
        }

        private int _copyTexID = 0;
        private int _blurredID1 = 0;
        private int _blurredID2 = 0;
        private int _weightsID = 0;
        private int _intencityID = 0;
        private int _offsetsID = 0;
        private int _grabBlurTextureID = 0;

        private void Awake()
        {
            // OnWillRenderObjectが呼ばれるように、MeshRendererとMeshFilterを追加する
            MeshFilter filter = gameObject.AddComponent<MeshFilter>();
            filter.hideFlags = HideFlags.DontSave;
            MeshRenderer renderer = gameObject.AddComponent<MeshRenderer>();
            renderer.hideFlags = HideFlags.DontSave;

            _copyTexID = Shader.PropertyToID("_ScreenCopyTexture");
            _blurredID1 = Shader.PropertyToID("_Temp1");
            _blurredID2 = Shader.PropertyToID("_Temp2");
            _weightsID = Shader.PropertyToID("_Weights");
            _intencityID = Shader.PropertyToID("_Intencity");
            _offsetsID = Shader.PropertyToID("_Offsets");
            _grabBlurTextureID = Shader.PropertyToID("_GrabBlurTexture");

            Transform parent = Camera.main.transform;
            transform.SetParent(parent);
            transform.localPosition = Vector3.forward;

            UpdateWeights();
        }

        private void Update()
        {
            foreach (var kv in _cameras)
            {
                kv.Value.Clear();
                BuildCommandBuffer(kv.Value);
            }
        }

        private void OnEnable()
        {
            Cleanup();
        }

        private void OnDisable()
        {
            Cleanup();
        }

        public void OnWillRenderObject()
        {
            if (!gameObject.activeInHierarchy || !enabled)
            {
                Cleanup();
                return;
            }

            if (_material == null)
            {
                _material = new Material(_shader);
                _material.hideFlags = HideFlags.HideAndDontSave;
            }

            Camera cam = Camera.current;
            if (cam == null)
            {
                return;
            }

#if UNITY_EDITOR
            if (cam == UnityEditor.SceneView.lastActiveSceneView.camera)
            {
                return;
            }
#endif

            if (_cameras.ContainsKey(cam))
            {
                return;
            }

            // コマンドバッファ構築
            CommandBuffer buf = new CommandBuffer();
            buf.name = "Blur AR Screen";
            _cameras[cam] = buf;

            BuildCommandBuffer(buf);

            cam.AddCommandBuffer(_cameraEvent, buf);
        }

        private void BuildCommandBuffer(CommandBuffer buf)
        {
            buf.GetTemporaryRT(_copyTexID, -1, -1, 0, FilterMode.Bilinear);
            buf.Blit(BuiltinRenderTextureType.CurrentActive, _copyTexID);

            // 半分の解像度で2枚のRender Textureを生成
            buf.GetTemporaryRT(_blurredID1, -2, -2, 0, FilterMode.Bilinear);
            buf.GetTemporaryRT(_blurredID2, -2, -2, 0, FilterMode.Bilinear);

            // 半分にスケールダウンしてコピー
            buf.Blit(_copyTexID, _blurredID1);

            // コピー後はいらないので破棄
            buf.ReleaseTemporaryRT(_copyTexID);

            float x = _offset / Screen.width;
            float y = _offset / Screen.height;

            buf.SetGlobalFloatArray(_weightsID, _weights);
            buf.SetGlobalFloat(_intencityID, Intencity);

            // 横方向のブラー
            buf.SetGlobalVector(_offsetsID, new Vector4(x, 0, 0, 0));
            buf.Blit(_blurredID1, _blurredID2, _material);

            // 縦方向のブラー
            buf.SetGlobalVector(_offsetsID, new Vector4(0, y, 0, 0));
            buf.Blit(_blurredID2, _blurredID1, _material);

            buf.SetGlobalTexture(_grabBlurTextureID, _blurredID1);
        }

        private void Cleanup()
        {
            foreach (var cam in _cameras)
            {
                if (cam.Key != null)
                {
                    cam.Key.RemoveCommandBuffer(_cameraEvent, cam.Value);
                }
            }

            _cameras.Clear();
            Object.DestroyImmediate(_material);
        }

        private void OnValidate()
        {
            if (!Application.isPlaying)
            {
                return;
            }

            UpdateWeights();
        }

        public void EnableBlur(bool enabled)
        {
            _enabledBlur = enabled;
        }

        private void UpdateWeights()
        {
            float total = 0;
            float d = _blur * _blur * 0.001f;

            for (int i = 0; i < _weights.Length; i++)
            {
                float r = 1.0f + 2.0f * i;
                float w = Mathf.Exp(-0.5f * (r * r) / d);
                _weights[i] = w;
                if (i > 0)
                {
                    w *= 2.0f;
                }
                total += w;
            }

            for (int i = 0; i < _weights.Length; i++)
            {
                _weights[i] /= total;
            }
        }
    }
}

ちょっと長いので面食らってしまった方もいるかもしれませんが、大事な点は以下です。

  • CommandBufferを生成し、必要な処理(コマンド)をバッファに設定する
  • セットアップしたCommandBufferをカメラに設定する

という2点のみです。

画面のキャプチャとブラー処理

これを行っているところを抜粋すると以下の部分になります。

public void OnWillRenderObject()
{
    // レンダリング対象オブジェクトが非表示の場合はコマンドバッファをクリアして終了
    if (!gameObject.activeInHierarchy || !enabled)
    {
        Cleanup();
        return;
    }

    // コマンドバッファのレンダリングで使用するマテリアルをシェーダファイルから生成する
    if (_material == null)
    {
        _material = new Material(_shader);
        _material.hideFlags = HideFlags.HideAndDontSave;
    }

    // 現在、このコンポーネントを持っているオブジェクトをレンダリングしようとしているカメラへの参照
    // (メインカメラひとつなら通常は一回だけ呼ばれる)
    Camera cam = Camera.current;
    if (cam == null)
    {
        return;
    }

    // 対象カメラに対してすでにコマンドバッファが生成済みなら終了
    if (_cameras.ContainsKey(cam))
    {
        return;
    }

    // コマンドバッファ構築
    CommandBuffer buf = new CommandBuffer();

    // あとでデバッグするときに分かりやすいように名前をつけておく
    buf.name = "Blur AR Screen";

    // 生成したコマンドバッファをキャッシュする
    _cameras[cam] = buf;

    // コマンドバッファに対して必要な設定を行う
    BuildCommandBuffer(buf);

    // 対象カメラに対してコマンドバッファを登録する
    cam.AddCommandBuffer(_cameraEvent, buf);
}

// 実際のコマンドバッファの構築処理
private void BuildCommandBuffer(CommandBuffer buf)
{
    // テンポラリのスクリーンサイズと同じサイズのRenderTextureを取得する(-1の指定がスクリーンサイズと同じサイズを意味する)
    buf.GetTemporaryRT(_copyTexID, -1, -1, 0, FilterMode.Bilinear);

    // 現在のアクティブなRenderTextureから、取得したテンポラリのRenderTextureへ単純にコピーする
    buf.Blit(BuiltinRenderTextureType.CurrentActive, _copyTexID);

    // 半分の解像度で2枚のRender Textureを生成(-2が、スクリーンサイズの半分(1/2)を意味する)
    // ふたつ取得しているのは、縦方向と横方向の2回、ブラー処理を分けて行うため
    buf.GetTemporaryRT(_blurredID1, -2, -2, 0, FilterMode.Bilinear);
    buf.GetTemporaryRT(_blurredID2, -2, -2, 0, FilterMode.Bilinear);

    // スクリーンサイズと同サイズのRTから半分のサイズのRTにコピーを行うことで、自動的に半分のサイズにしてくれる
    buf.Blit(_copyTexID, _blurredID1);

    // コピー後はいらないので破棄
    buf.ReleaseTemporaryRT(_copyTexID);

    // ブラーのためにテクセルをフェッチするオフセットを、スクリーンサイズで正規化する
    float x = _offset / Screen.width;
    float y = _offset / Screen.height;

    // ガウシアンブラー用の「重み」パラメータを設定する
    buf.SetGlobalFloatArray(_weightsID, _weights);

    // ブラーの全体的な強さのパラメータを設定する
    buf.SetGlobalFloat(_intencityID, Intencity);

    // 横方向のブラー
    buf.SetGlobalVector(_offsetsID, new Vector4(x, 0, 0, 0));
    buf.Blit(_blurredID1, _blurredID2, _material);

    // 縦方向のブラー
    buf.SetGlobalVector(_offsetsID, new Vector4(0, y, 0, 0));
    buf.Blit(_blurredID2, _blurredID1, _material);

    // ブラーをかけた最終結果のテクスチャをグローバルテクスチャとして設定する
    buf.SetGlobalTexture(_grabBlurTextureID, _blurredID1);
}

抜粋したコードにはなにを行っているかをコメントしてあります。
処理の流れにの詳細についてはそちらをご覧ください。

大雑把に流れを説明すると、コマンドバッファを生成してブラー処理のためのコマンドを構築し、それをカメラに登録する、という流れになります。

コマンドバッファの構築についての大雑把な流れは、

  1. 現在レンダリング済みの内容をテンポラリなRenderTextureに等倍でコピーする
  2. (1)でコピーしたものをさらに半分のサイズにコピーする
  3. (2)の半分サイズのテクスチャに対し、横方向のブラーをかける
  4. (3)の横方向ブラーの画像に対し、さらに縦方向のブラーをかける
  5. (4)の最終結果を、グローバルなテクスチャとして登録する

という手順で最終的なブラーがかかった画像を生成しています。

実際に、Frame Debuggerを使うとこの過程を見ることができます。
f:id:edo_m18:20190403111126g:plain

BuildCommandBufferメソッドでこのあたりの処理を行っています。

このセットアップ部分を理解する上で重要な点が「この構築を行っている時点では実際に描画処理は行われていない」という点です。

前述のように、CPUからGPUへは「コマンドバッファ」と呼ばれるバッファに命令を積み込んで送る、と説明しました。
つまりここではその「命令群」をバッファに積み込んでいるだけなので、これが実際にGPUに届いて処理されるのは設定したイベントのタイミングとなります。

ちなみに「設定したイベントのタイミング」とは、カメラに設定するときに指定したイベントの種類のことです。
以下のところですね。

cam.AddCommandBuffer(_cameraEvent, buf);

_cameraEventは(今回のサンプルでは)デフォルトでCameraEvent.AfterImageEffectsが設定されています。
読んで分かる通り、イベントタイプはイメージエフェクトのあと、となります。

イベントの種類は、ドキュメントから画像を引用させていただくと以下のようなタイミングに処理を差し込むことができるようになっています。
https://docs.unity3d.com/ja/current/uploads/SL/CameraRenderFlowCmdBuffers.svg

緑の丸い点のところが差し込める位置ですね。
今回はこのうち、イメージエフェクトのあと、というタイミングで処理を行っているわけです。

ブラー処理用シェーダ

ちなみに、コマンドバッファで使用しているブラーのためのシェーダも載せておきます。
こちらのシェーダについては以前の記事(Unityでガウシアンブラーを実装する)を参照ください。

Shader "UI/BlurEffect"
{
    Properties
    {
        _MainTex("Texture", 2D) = "white" {}
    }

    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            ZTest Off
            ZWrite Off
            Cull Back

            CGPROGRAM
            #pragma fragmentoption ARB_precision_hint_fastest
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                half2 uv : TEXCOORD0;
            };

            sampler2D _MainTex;

            half4 _Offsets;
            float _Intencity;

            static const int samplingCount = 10;
            half _Weights[samplingCount];
            
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;

                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                half4 col = 0;

                [unroll]
                for (int j = samplingCount - 1; j > 0; j--)
                {
                    col += tex2D(_MainTex, i.uv - (_Offsets.xy * j * _Intencity)) * _Weights[j];
                }

                [unroll]
                for (int j = 0; j < samplingCount; j++)
                {
                    col += tex2D(_MainTex, i.uv + (_Offsets.xy * j * _Intencity)) * _Weights[j];
                }

                return col;
            }
            ENDCG
        }
    }
}

uGUIをカスタムする

最後に、上記までで得られたブラー画像を利用してuGUIの背景を作る過程を書いていきます。
この「ブラーをかけた画像を利用する」ため、少しだけシェーダを書かないとなりません。

また通常のモデルとは異なり、uGUIのシェーダは専用の処理もあるため普通に生成したシェーダでそのまま書いてもうまく動きません。

このあたりはテラシュールブログさんの以下の記事を参考にさせていただきました。

tsubakit1.hateblo.jp

要点だけ書いておくと、Unityの公式サイトからビルトインシェーダをDLしてきて、その中でUI用のシェーダをベースにカスタムしていく、という感じです。

カスタム後のシェーダを見てみましょう。

カスタム後のシェーダコード

Shader "UI/BlurScreen"
{
    Properties
    {
        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
        _Color ("Tint", Color) = (1,1,1,1)

        _StencilComp ("Stencil Comparison", Float) = 8
        _Stencil ("Stencil ID", Float) = 0
        _StencilOp ("Stencil Operation", Float) = 0
        _StencilWriteMask ("Stencil Write Mask", Float) = 255
        _StencilReadMask ("Stencil Read Mask", Float) = 255

        _ColorMask ("Color Mask", Float) = 15

        [Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
    }

    SubShader
    {
        Tags
        {
            "Queue"="Transparent"
            "IgnoreProjector"="True"
            "RenderType"="Transparent"
            "PreviewType"="Plane"
            "CanUseSpriteAtlas"="True"
        }

        Stencil
        {
            Ref [_Stencil]
            Comp [_StencilComp]
            Pass [_StencilOp]
            ReadMask [_StencilReadMask]
            WriteMask [_StencilWriteMask]
        }

        Cull Off
        Lighting Off
        ZWrite Off
        ZTest [unity_GUIZTestMode]
        Blend SrcAlpha OneMinusSrcAlpha
        ColorMask [_ColorMask]

        Pass
        {
            Name "Default"
        CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 2.0

            #include "UnityCG.cginc"
            #include "UnityUI.cginc"

            #pragma multi_compile __ UNITY_UI_CLIP_RECT
            #pragma multi_compile __ UNITY_UI_ALPHACLIP

            struct appdata_t
            {
                float4 vertex   : POSITION;
                float4 color    : COLOR;
                float2 texcoord : TEXCOORD0;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct v2f
            {
                float4 vertex   : SV_POSITION;
                fixed4 color    : COLOR;
                float2 texcoord  : TEXCOORD0;
                float4 worldPosition : TEXCOORD1;
                float4 pos : TEXCOORD2;
                UNITY_VERTEX_OUTPUT_STEREO
            };

            sampler2D _MainTex;
            fixed4 _Color;
            fixed4 _TextureSampleAdd;
            float4 _ClipRect;
            float4 _MainTex_ST;
            sampler2D _GrabBlurTexture;

            v2f vert(appdata_t v)
            {
                v2f OUT;
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
                OUT.worldPosition = v.vertex;
                OUT.vertex = UnityObjectToClipPos(OUT.worldPosition);

                OUT.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);

                OUT.pos = ComputeScreenPos(OUT.vertex);

                OUT.color = v.color * _Color;
                return OUT;
            }

            fixed4 frag(v2f IN) : SV_Target
            {
                float2 uv = IN.pos.xy / IN.pos.w;
                uv.y = 1.0 - uv.y;

                half4 color = (tex2D(_GrabBlurTexture, uv) + _TextureSampleAdd) * IN.color;

                #ifdef UNITY_UI_CLIP_RECT
                color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);
                #endif

                #ifdef UNITY_UI_ALPHACLIP
                clip (color.a - 0.001);
                #endif

                half4 mask = tex2D(_MainTex, IN.texcoord);
                color.a *= mask.a;

                return color;
            }
        ENDCG
        }
    }
}

色々記載がありますが、修正した箇所はそれほど多くありません。(ほぼ、UnityのビルトインシェーダをDLしてきたコードそのままです)

編集した部分だけを抜粋すると以下の箇所になります。

struct v2f
{
    /* ... 略 ... */
    float4 pos : TEXCOORD2; // キャプチャした画像のテクセルをフェッチするための変数
    /* ... 略 ... */
};

/* ... 略 ... */

// CommandBufferでキャプチャした画像が渡されてくる
sampler2D _GrabBlurTexture;

v2f vert(appdata_t v)
{
    /* ... 略 ... */

    // uGUIのメッシュの位置をスクリーン位置に変換する
    OUT.worldPosition = v.vertex;
    OUT.vertex = UnityObjectToClipPos(OUT.worldPosition);
    OUT.pos = ComputeScreenPos(OUT.vertex);

    /* ... 略 ... */
}

fixed4 frag(v2f IN) : SV_Target
{
    // デバイス正規化座標系とするため`w`で除算する
    // が、ComputeScreenPosの段階で正常な値が入っているっぽいが、
    // シーンビューでちらつくのでこうしておく
    float2 uv = IN.pos.xy / IN.pos.w;

    // キャプチャ時に反転しているのでUVを反転してフェッチするようにする
    uv.y = 1.0 - uv.y;
    half4 color = (tex2D(_GrabBlurTexture, uv) + _TextureSampleAdd) * IN.color;

    /* ... 略 ... */

    // uGUIのImageに設定されたテクスチャをマスク画像として利用する
    // 今回の例ではアルファ値でマスクしているが、白黒画像やその他の画像で独自にマスク位置を変更したい場合はここをいじる
    half4 mask = tex2D(_MainTex, IN.texcoord);
    color.a *= mask.a;

    return color;
}

追記した部分だけを抜き出しました。
見てもらうと分かる通り、それほど多くのコードは追加していないのが分かると思います。

どういう処理なのかはコメントとして追記したのでそちらをご覧ください。

大まかに説明だけすると、CommandBufferによって得られたブラー画像がuGUIの位置から見てどの位置なのか、の計算を行い、そのUV値を元にブラー画像からテクセルをフェッチし、それを利用しています。

またImageに設定されたテクスチャはマスク画像として利用するようにしているので特定の形にくり抜くことができます。

ここで重要な点は、実際に「半透明になっているわけではなく」、あくまでキャプチャした画像の適切な位置を利用することで、あたかも透過しているように見える、というわけです。

最後に

今回のサンプルは自分のiPhoneXでも問題なく動きました。処理負荷的にもそこまで大きくはないかなと思います。

iOSの表現でもブラー処理はよく見ますね。すりガラス風の表現はオシャレに見えるので、ワンポイントのアクセントなどに利用すると質感があがっていい感じです。

今回はuGUIの背景として利用しましたが、すりガラス風の表現はこれ以外にも活用する部分はあると思います。色々試してみるといいかもしれません。

Scriptable Render Pipeline(SRP)についてちょっと調べてみた

概要

いつもお世話になっている凹みさんの記事を参考にさせてもらってます。
基本的には凹みさんの記事を見ながら、自分の理解やメモを書いているだけの記事となります。

tips.hecomi.com

なお、SRPについてはUnityのGithubアカウントから提供されているものをクローンして利用します。

github.com

インストールする

まず、上記リポジトリからSRPのプラグインをクローンします。

利用方法についてはちょっとだけ複雑で、GithubのReadmeには以下のように書かれています。

How to use the latest version

Note: The Master branch is our current development branch and may not work on the latest publicly available version of Unity. To determine which version of SRP you should use with your version of Unity, go to Package Manager (Window > Package Manager > Show Preview Packages) to see what versions of SRP are available for your version of Unity Editor. Then you can search the Tags tab of the Branch dropdown in the SRP GitHub for that tag number. To use the latest version of the SRP, follow the instructions below:


This repository consists of a folder that should be cloned outside the Assets\ folder of your Unity project. We recommend creating a new project to test SRP. Do not clone this repo into an existing project unless you want to break it, or unless you are updating to a newer version of the SRP repo.


After cloning you will need to edit your project's packages.json file (in either UnityPackageManager/ or Packages/) to point to the SRP submodules you wish to use. See: https://github.com/Unity-Technologies/ScriptableRenderPipeline/blob/master/TestProjects/HDRP_Tests/Packages/manifest.json


This will link your project to the specific version of SRP you have cloned.


You can use the GitHub desktop app to clone the latest version of the SRP repo or you can use GitHub console commands.

ざっくりまとめると、

  1. パッケージマネージャを開き、プレビュー版も表示する(Window > Package Managerを開き、表示されたWindow上部にあるAdvancedをクリックしてShow Preview Packagesを選択)
  2. そこで表示されるSRPの利用可能バージョンを見る(これはUnityのバージョンによって異なるみたい)(※1)
  3. 利用可能バージョンと同じバージョンが記載されているタグ(Gitのタグ)をチェックアウト
  4. チェックアウトしたものを、Assetsフォルダと同階層(かそれより上)に配置する(Assets内には入れない)
  5. Package ManagerのJSONファイルに依存を追記する(※2)

※1 ... 開くとこんな感じのWindowが開き、確認することが出来ます。 f:id:edo_m18:20190307123332p:plain

※2 ... 依存関係についてはサンプルのJSONが公開されているので参照してください。 ちなみに、サンプルでは"com.unity.render-pipelines.core": "file:../../../com.unity.render-pipelines.core",みたいに書かれていますが、これは適切に、自分で配置したフォルダへの参照となるよう修正が必要です。

Lightweight Pipelineをインストールする

今回は、VR/AR向けに調べてたこともあってLightweight Pipelineのみの説明ですが、インストール自体はLightweight Pipelineの話ですが、High Definition Render Pipelineも基本的には同じです。

SRP自体をインストールしてあれば、同リポジトリに、LWRP用のモジュールも含まれているので、Package ManagerのJSONに依存関係を記載しておけば自動的にインポートされます。

LWRPを利用する

以上でインストールが完了しました。
次に行うのは、実際のレンダリングが、指定のLWRPで実行されるようにセットアップすることです。

Readmeから引用すると以下のように記述されています。

To create a Render Pipeline Asset:


In the Project window, navigate to a directory outside of the Scriptable Render Pipeline Folder, then right click in the Project window and select Create > Render Pipeline > High Definition or Lightweight > Render Pipeline/Pipeline Asset. Navigate to Edit > Project Settings > Graphics and add the Render Pipeline Asset you created to the Render Pipeline Settings field to use it in your project.

SRPが正常にインストールされていると、Project WindowのCreateメニューにSRP用のメニューが追加されています。

f:id:edo_m18:20190307124330p:plain

上記のように、メニューから「Lightweight Render Pipeline Asset」を生成します。
そしてEdit > Project Settings > Graphicsから開くグラフィクスセッティングに、上で生成したAssetを設定します。

f:id:edo_m18:20190307143812p:plain

これでLWRPを利用したレンダリングが行われるようになります。
ただこれを設定するとStandardシェーダが使えなくなるので、LWRP専用のシェーダを利用する必要があります。

LWRP専用シェーダについては別の記事で詳細を書きたいと思います。(というか、まだ現在調査中)

独自のSRPを実装する

LWRPだけ見ていても仕組みは理解できないので、独自のSRPを作って仕組みを把握してみたいと思います。

と言っても、今回は「なにもしない/簡単な処理だけ」のSRPを実装することでSRPがどういうことをやってくれるのかを把握するにとどめます。(というか、まだなにができるかを正確に把握していないので)

「なにもしない」SRP

まず見てみるのは、「なにもしない」SRPの実装です。
やることはただひとつ、グリーンで塗りつぶすだけです。

コードを見てみましょう。

なお、コードはこちらのリポジトリを参考にさせていただいています。
(こちらは公式ブログのデモのようです)

github.com

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Experimental.Rendering;

public class BasicAssetPipe : RenderPipelineAsset
{
    public Color ClearColor = Color.green;

#if UNITY_EDITOR
    [UnityEditor.MenuItem("SRP-Demo/01 - Create Basic Asset Pipeline")]
    static void CraeteBasicAssetPipeline()
    {
        var instance = ScriptableObject.CreateInstance<BasicAssetPipe>();
        UnityEditor.AssetDatabase.CreateAsset(instance, "Assets/SRP-Demo/1-BasicAssetPipe.asset");
    }
#endif

    protected override IRenderPipeline InternalCreatePipeline()
    {
        return new BasicPipeInstance(ClearColor);
    }
}

public class BasicPipeInstance : RenderPipeline
{
    private Color _clearColor = Color.black;

    public BasicPipeInstance(Color clearColor)
    {
        _clearColor = clearColor;
    }

    public override void Render(ScriptableRenderContext context, Camera[] cameras)
    {
        // does not so much yet.
        base.Render(context, cameras);

        // Clear buffer to the configured color.
        var cmd = new CommandBuffer();
        cmd.ClearRenderTarget(true, true, _clearColor);
        context.ExecuteCommandBuffer(cmd);
        cmd.Release();
        context.Submit();
    }
}

前述のように、SRPを利用するには、そのパイプラインを記述したAssetを生成してそれをセットすることで実現します。
なので、冒頭ではアセットを生成するEditor向けのクラスが定義されていますね。(↓これ)

public class BasicAssetPipe : RenderPipelineAsset
{
    public Color ClearColor = Color.green;

#if UNITY_EDITOR
    [UnityEditor.MenuItem("SRP-Demo/01 - Create Basic Asset Pipeline")]
    static void CraeteBasicAssetPipeline()
    {
        var instance = ScriptableObject.CreateInstance<BasicAssetPipe>();
        UnityEditor.AssetDatabase.CreateAsset(instance, "Assets/SRP-Demo/1-BasicAssetPipe.asset");
    }
#endif

    protected override IRenderPipeline InternalCreatePipeline()
    {
        return new BasicPipeInstance(ClearColor);
    }
}

RenderPipelineAssetクラスを継承したアセット生成を行う

RenderPipelineAssetクラスを継承した、独自パイプラインのためのクラスを定義します。
RenderPipelineAssetScriptableObjectを継承しているので、ScriptableObject化してアセットとして保存できるようになっています。

また、RenderPipelineAssetクラスはabstractクラスになっていて、以下のメソッドのみ、オーバーライドする必要があります。

//
// Summary:
//     ///
//     Create a IRenderPipeline specific to this asset.
//     ///
//
// Returns:
//     ///
//     Created pipeline.
//     ///
protected abstract IRenderPipeline InternalCreatePipeline();

中身は、IRenderPipelineを実装したクラスを作れ、ということのようです。

インターフェースはシンプルで、以下のふたつのメソッドを実装するのみとなっています。

//
// Summary:
//     ///
//     When the IRenderPipeline is invalid or destroyed this returns true.
//     ///
bool disposed { get; }

//
// Summary:
//     ///
//     Defines custom rendering for this RenderPipeline.
//     ///
//
// Parameters:
//   renderContext:
//     Structure that holds the rendering commands for this loop.
//
//   cameras:
//     Cameras to render.
void Render(ScriptableRenderContext renderContext, Camera[] cameras);

重要なのはRenderメソッドでしょう。ここで、実際のレンダリングの処理を行うわけです。

参考に上げた実装を見てみると以下のように実装されています。

protected override IRenderPipeline InternalCreatePipeline()
{
    return new BasicPipeInstance(ClearColor);
}

BasicPipeInstanceを、ClearColorを引数に生成したものを返しているだけですね。

IRenderPipelineインターフェースを実装したクラスを作る

ではBasicPipeInstanceのほうの実装も見てみましょう。

public class BasicPipeInstance : RenderPipeline
{
    private Color _clearColor = Color.black;

    public BasicPipeInstance(Color clearColor)
    {
        _clearColor = clearColor;
    }

    public override void Render(ScriptableRenderContext context, Camera[] cameras)
    {
        // does not so much yet.
        base.Render(context, cameras);

        // Clear buffer to the configured color.
        var cmd = new CommandBuffer();
        cmd.ClearRenderTarget(true, true, _clearColor);
        context.ExecuteCommandBuffer(cmd);
        cmd.Release();
        context.Submit();
    }
}

ベースクラスとしてRenderPipelineを継承していますが、RenderPipelineクラスはabstractクラスになっていて、インターフェースに必要な機能の定義と、レンダリング時のイベントをフィールドとして持つようになっているだけのクラスとなっています。

さて、BasicPipeInstanceで重要なのはRenderメソッドですね。
ここを見ればSRPで最低限なにをしないとならないかが分かるはずです。

そこだけを抜き出して見てみましょう。

base.Render(context, cameras);

// Clear buffer to the configured color.
var cmd = new CommandBuffer();
cmd.ClearRenderTarget(true, true, _clearColor);
context.ExecuteCommandBuffer(cmd);
cmd.Release();
context.Submit();

とてもシンプルですね。
コマンドバッファを作り、レンダーターゲットを指定した色(今回の例だとグリーン)でクリアし、それを実行しているだけ、と。

これで画面がグリーンのベタ塗りで表示される、というわけですね。
つまり、コマンドバッファを作ってそこで必要な処理をする、ということのようです。

ちなみに、サンプルではレンダリングごとにCommandBufferを生成していますが、コンストラクタで生成して使いまわしても大丈夫なようです。

不透明オブジェクトだけをレンダリングするSRP

さて、前述のSRPでは背景をベタ塗りするだけのものでした。
そのため、シーン内にオブジェクトがあっても表示されていませんでした。

次は不透明オブジェクトだけをレンダリングするSRPの実装を見てみましょう。
これを見ることで、なんとなくパイプライン全体の流れが把握できるかと思います。

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Experimental.Rendering;

public class OpaqueAssetPipe : RenderPipelineAsset
{
#if UNITY_EDITOR
    [UnityEditor.MenuItem("SRP-Demo/02 - Create Opaque Asset Pipeline")]
    static void CreateOpaqueAssetPipeline()
    {
        var instance = ScriptableObject.CreateInstance<OpaqueAssetPipe>();
        UnityEditor.AssetDatabase.CreateAsset(instance, "Assets/SRP-Demo/2-OpaqueAssetPipe.Asset");
    }
#endif

    protected override IRenderPipeline InternalCreatePipeline()
    {
        return new OpaquePipeInstance();
    }
}

public class OpaquePipeInstance : RenderPipeline
{
    public override void Render(ScriptableRenderContext context, Camera[] cameras)
    {
        base.Render(context, cameras);

        foreach (var camera in cameras)
        {
            // Culling
            ScriptableCullingParameters cullingParams;
            if (!CullResults.GetCullingParameters(camera, out cullingParams))
            {
                continue;
            }

            CullResults cull = CullResults.Cull(ref cullingParams, context);

            // Setup camera for rendering (sets render target, view/projection matrices and other
            // per-camera built-in shader variables).
            context.SetupCameraProperties(camera);

            // clear depth buffer
            var cmd = new CommandBuffer();
            cmd.ClearRenderTarget(true, false, Color.black);
            context.ExecuteCommandBuffer(cmd);
            cmd.Release();

            // Draw opaque objects using BasicPass shader pass
            DrawRendererSettings settings = new DrawRendererSettings(camera, new ShaderPassName("BasicPass"));
            settings.sorting.flags = SortFlags.CommonOpaque;

            FilterRenderersSettings filterSettings = new FilterRenderersSettings(true)
            {
                renderQueueRange = RenderQueueRange.opaque
            };

            context.DrawRenderers(cull.visibleRenderers, ref settings, filterSettings);

            // Draw skybox
            context.DrawSkybox(camera);

            context.Submit();
        }
    }
}

さて、最初のやつよりも少しコードが長くなりましたね。
しかしやっていることは比較的シンプルです。

冒頭ではカリングに関する処理を行っています。

// Culling
ScriptableCullingParameters cullingParams;
if (!CullResults.GetCullingParameters(camera, out cullingParams))
{
    continue;
}

CullResults cull = CullResults.Cull(ref cullingParams, context);

CullResutls.GetCullingParametersによってカメラの状態を評価します。
ドキュメントを見ると以下のように書かれています。

Get culling parameters for a camera.


Returns false if camera is invalid to render (empty viewport rectangle, invalid clip plane setup etc.).

つまり、viwportがemptyだったり、など不正な状態の場合は処理しないようになっています。

そして続くCullResults.Cull(...);によってカリングなどを評価し、実際に表示されるオブジェクトなどの結果を得ます。

ドキュメントによると以下のような結果を得ます。

Culling results (visible objects, lights, reflection probes).

さて、次に行うのはカメラに対するプロパティの設定です。

// Setup camera for rendering (sets render target, view/projection matrices and other
// per-camera built-in shader variables).
context.SetupCameraProperties(camera);

コメントにもあるように、様々な値を設定するようです。
ドキュメントにも以下のように書かれています。

Setup camera specific global shader variables.


This function sets up view, projection and clipping planes global shader variables.


Additionally, if stereoSetup is set to true, and single-pass stereo is enabled, stereo-specific shader variables and state are configured.

プロジェクション行列やクリッピングプレーンの情報など、カメラに必要な設定を行うようですね。

実際にレンダリングを行うコマンドを構築する

以上で設定などの処理が終了しました。
最後に行うのは実際にレンダリングを行うためのコマンドバッファの構築です。

やや長めですが、ざーっと見てみましょう。

// clear depth buffer
var cmd = new CommandBuffer();
cmd.ClearRenderTarget(true, false, Color.black);
context.ExecuteCommandBuffer(cmd);
cmd.Release();

// Draw opaque objects using BasicPass shader pass
DrawRendererSettings settings = new DrawRendererSettings(camera, new ShaderPassName("BasicPass"));
settings.sorting.flags = SortFlags.CommonOpaque;

FilterRenderersSettings filterSettings = new FilterRenderersSettings(true)
{
    renderQueueRange = RenderQueueRange.opaque
};

context.DrawRenderers(cull.visibleRenderers, ref settings, filterSettings);

// Draw skybox
context.DrawSkybox(camera);

context.Submit();

まず冒頭で行っているのは、レンダーターゲットのクリアですね。
このあたりは「グリーンベタ塗り」のときとあまり違いはありません。

その後に行っているのは、レンダリングする対象の絞り込みとその設定です。

レンダリング対象を指定する

DrawRendererSettingsによってどのキューのオブジェクトをレンダリング対象とするかを決め、またどのパス名のシェーダを利用するかも指定しています。

// Draw opaque objects using BasicPass shader pass
DrawRendererSettings settings = new DrawRendererSettings(camera, new ShaderPassName("BasicPass"));
settings.sorting.flags = SortFlags.CommonOpaque;

new ShaderPassName("BasicPass")によってレンダリングするパス名を指定しているのが分かります。今回の例ではBasicPassと名前がついたパスをレンダリング対象としています。

ちょっとシェーダ側の記述を見てみましょう。

Shader "Sample/BasicPass"
{
    Properties { ... }
    SubShader
    {
        Tags { "RenderType" = "Opaque" }

        Pass
        {
            Tags { "LightMode" = "BasicPass" }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            struct appdata { ... };

            struct v2f { ... };

            v2f vert (appdata v) { ... }

            fixed4 frag (v2f i) : SV_Target { ... }
            ENDCG
        }
    }
}

細かい処理を除いたサンプルです。
基本的な処理は違いはありません。

違いがあるのは、LightModeBasicPassと指定されている点にあります。
これは、前述のパイプラインのところで書かれていたnew ShaderPassName("BasicPass")と同じ名前ですね。

つまり、前述のパイプラインではこのパスのみをレンダリングしていた、というわけなんですね。

続く指定処理を見ていきましょう。

FilterRenderersSettings filterSettings = new FilterRenderersSettings(true)
{
    renderQueueRange = RenderQueueRange.opaque
};

context.DrawRenderers(cull.visibleRenderers, ref settings, filterSettings);

// Draw skybox
context.DrawSkybox(camera);

context.Submit();

最後はざーっと見てしまいましょう。

次に行っているのはフィルタリング(FilterRenderersSettings)ですね。

引数は初期化に関するパラメータのようです。ドキュメントには以下のように書かれています。

If initializeValues is true all values are initialized such that no filtering will occur. Otherwise the values are default initialized.

正直ここの変更でなにが変わるのかはまだちょっとよく分かっていません。

が、falseにしたら画面にオブジェクトが表示されなくなったので、基本はtrueでいいのかな、と思います。

さぁ、これで準備が整いました。
あとは実際にレンダリングの処理を記述すれば完成です。

レンダリングにはcontext.DrawRenderers(cull.visibleRenderers, ref settings, filterSettings);を呼び出します。

引数を見ればなんとなく推測できますが、CullResultsによって得られたレンダリング対象と、フィルタ設定を引数にコンテキストのDrawRenderersを呼び出します。

これが、対象オブジェクトのレンダリングを行っている箇所ですね。

そして最後に、context.DrawSkybox(camera);によってスカイボックスが描画されます。
スカイボックスは大抵最後に行われるのでこの位置なのでしょう。

あとはこれをSubmitすれば構築が完了します。

最後に

ベタ塗りだけするSRPと、不透明オブジェクトだけをレンダリングするSRPを見てきました。
どちらも非常にシンプルですが、SRPがどういうことをしてくれる機能なのか、そしてどんなことができるのかはこれでなんとなく見えてきたかと思います。

実際、使用に耐えうるものを構築するにはもっと色々な知識(SRPだけの話ではなく、レンダリングパイプライン全体の話)が必要になるので、イチから構築するというのはあまり現実的ではないかもしれません。

しかし、LWRPを使う際や、既存の仕組みをカスタムする際にはこうした知識は役に立つと思います。

次はLWRPを触ってみて、どんなことができるのか書いてみたいと思います。

UnityのARKit Pluginのカメラ映像を利用してなにかする

概要

UnityのARKit Pluginを使えばARコンテンツを手軽に作ることができます。
しかし、当然ですがARは外界をカメラで撮影し、それを元に姿勢を判断しています。

つまり、QRコードリーダーやOpenCVなどを利用した画像認識など「カメラを利用した処理」をするには、ARKit Pluginで利用しているカメラ映像を利用しないとなりません。

個別にカメラを起動して、その映像を使う、ということができないからです。

今回はARKit Pluginのカメラの映像を利用して画像処理をするためのTipsをまとめておこうと思います。

大まかな流れ

主な処理はUnityARVideoのコードを参考にしました。

  1. ARKitのセッションからARTextureHandlesを取得する
  2. Texture2D.CreateExternalTextureメソッドを利用して、ネイティブテクスチャのポインタからテクスチャを生成する
  3. UpdateExternalTextureメソッドを利用してテクスチャの内容をアップデートする
  4. UnityのARKit Pluginが提供してくれているマテリアルを利用して、ふたつのテクスチャを合成する(カメラの映像として見れる形に復元する(YCbCrフォーマットでふたつのテクスチャとして取得するため))
  5. RenderTextureの内容をTexture2Dにコピーする

という流れになります。

以下、細かく見ていきましょう。

ARTextureHandlesを取得する

UnityARSessionNativeInterfaceGetARVideoTextureHandlesというメソッドを利用してARTextureHandlesという、テクスチャのハンドルを取得することができます。

このハンドルを利用して、ARKit側で生成したネイティブのテクスチャへ(ポインタを経由して)アクセスすることができます。

ネイティブで生成されたテクスチャからテクスチャの内容を取得するには以下のようにします。

ARTextureHandles handles = UnityARSessionNativeInterface.GetARSessionNativeInterface().GetARVideoTextureHandles();
if (handles.IsNull())
{
    return;
}

Resolution currentResolution = Screen.currentResolution;

// _textureYはTexture2D
if (_textureY == null)
{
    _textureY = Texture2D.CreateExternalTexture(currentResolution.width, currentResolution.height, TextureFormat.R8, false, false, handles.TextureY);
    _textureY.filterMode = FilterMode.Bilinear;
    _textureY.wrapMode = TextureWrapMode.Repeat;
    _yuvMat.SetTexture("_textureY", _textureY);
}

handles.TextureYはネイティブテクスチャへのポインタとなっていてSystem.IntPtr型です。
そしてTexture2Dにはこうしたネイティブテクスチャからテクスチャを生成することができるようになっています。

それが以下の部分です。

_textureY = Texture2D.CreateExternalTexture(currentResolution.width, currentResolution.height, TextureFormat.R8, false, false, handles.TextureY);

CreateExternalTextureでネイティブテクスチャを元にテクスチャを生成することができます。
そしてテクスチャの内容を実際に取得して更新するにはUpdateExternalTextureメソッドを使います。

_textureY.UpdateExternalTexture(handles.TextureY);

とすることで、生成したTexture2Dの内容をアップデートすることができます。

ふたつのテクスチャを合成する

無事、ネイティブテクスチャからふたつのテクスチャを得ることができました。
ただ、前述のように、これらのテクスチャはカメラの映像がそのまま、というわけではありません。

YCbCrというフォーマットになっていて、適切に合成しないと元のカメラの映像になりません。
Wikipediaから引用させてもらうと以下の意味のようです。

YUVやYCbCrやYPbPrとは、輝度信号Yと、2つの色差信号を使って表現される色空間。

Wikipediaから画像を引用させてもらうと、以下のような感じのテクスチャが得られます。

YCbCr画像イメージ

一番上が元画像、その下が輝度画像、そしてその下がそれぞれ2つの色差信号によって表現されたものです。
これを合成して元の形に復元するために、ARKitPluginが提供してくれている「YUVMaterial」を利用します。

前段でネイティブテクスチャの情報は取得しているので、あとはこれを合成するマテリアルを通してRenderTextureに描き出してやればOKです。

// ネイティブテクスチャからテクスチャを生成し、マテリアルにセットしているところ
_yuvMat.SetTexture("_textureY", _textureY);

private void OnPostRender()
{
    // ... 中略 ...

    // RenderTextureへ、マテリアルの内容を書き込み
    Graphics.Blit(null, _arTexture, _yuvMat);

    // RenderTextureの内容をTexture2Dにコピーするため、AsyncGPUReadbackを利用して読み出し
    _request = AsyncGPUReadback.Request(_arTexture);
}

Graphics.Blitを利用してRenderTextureにマテリアルの内容を書き出します。
最終的に利用する形がRenderTextureなのであれば以上で終了です。
が、大体の場合はTexture2Dにするなり、テクセルの配列を利用して処理するなりの「情報として扱える形」に変換する必要が出てくるでしょう。

RenderTextureの内容をTexture2Dにコピーする

最後の工程は、テクスチャの合成を施した結果であるRenderTextureの内容をTexture2Dにコピーすることです。

コピーは以下のようにします。

RenderTexture back = RenderTexture.active;
RenderTexture.active = _arTexture;
_arTexture2D.ReadPixels(new Rect(0, 0, _arTexture.width, _arTexture.height), 0, 0);
_arTexture2D.Apply();
RenderTexture.active = back;

しかしこれ、実はだいぶ重い処理になります。 おそらくCPUを利用して全テクセルを読み出していると思うので、時間がかかります。
特に問題なのはメインスレッドで実行されるため、UIを停止、あるいはFPSの低下を招きます。

なので数フレームに1度、などの最適化を行わないとならないかもしれません。
幸いにして、今回やりたかったのはARKitのカメラの映像を利用してQRコードの読み取りをする、というものです。

なのでQRコードの読み取りが必要なタイミングでだけ有効にすることで今回は回避しています。

余談:AsyncGPUReadbackを使ってRenderTexutreの内容を読み出す

上で説明したように、ReadPIxelsはとても重い処理です。
そこでAsyncGPUReadbackというメソッドが追加されました。

これはGPURenderTextureの読み取りを実行し、結果を非同期で返してくれるメソッドです。

使い方などについては以下の記事が詳しく解説してくれているので参考にしてみてください。

qiita.com

なお、ここで紹介されている_tex.LoadRawTextureData(buffer);という形で読み込むと正常にデータが読み込めず、おかしな表示になってしまっていました。

keijiroさんのこちらのサンプルを見ると_tex.SetPixels32(buffer.ToArray());という形で読み込んでいて、こちらを試したところ正常に表示されました。

github.com

さて、最後に実際に実装したコードを掲載しておきます。

コード全文

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.UI;
using UnityEngine.Rendering;
using UnityEngine.XR.iOS;

namespace ARKitTextureSample
{
    public delegate void OnReadQRCode(string url);

    public class ARQRReader : MonoBehaviour
    {
        public event OnReadQRCode OnReadQRCode;

        [SerializeField]
        private RawImage _preview = null;

        [SerializeField]
        private Text _text = null;

        [SerializeField]
        private Material _yuvMat = null;

        private string _result = null;

        private Matrix4x4 _displayTransform;
        private Texture2D _textureY = null;
        private Texture2D _textureCbCr = null;
        private RenderTexture _arTexture = null;
        private Texture2D _arTexture2D = null;
        private bool _isActive = false;

#if !UNITY_EDITOR && UNITY_IOS
        private void Start()
        {
            _arTexture = new RenderTexture(Screen.width, Screen.height, 0);
            _arTexture2D = new Texture2D(_arTexture.width, _arTexture.height, TextureFormat.ARGB32, false);
            _yuvMat = Instantiate(_yuvMat);
        }

        public void Active(bool active)
        {
            if (_isActive == active)
            {
                return;
            }

            if (active)
            {
                UnityARSessionNativeInterface.ARFrameUpdatedEvent += UpdateFrame;
            }
            else
            {
                UnityARSessionNativeInterface.ARFrameUpdatedEvent -= UpdateFrame;
            }

            _isActive = active;
        }

        private void UpdateFrame(UnityARCamera cam)
        {
            _displayTransform = new Matrix4x4();
            _displayTransform.SetColumn(0, cam.displayTransform.column0);
            _displayTransform.SetColumn(1, cam.displayTransform.column1);
            _displayTransform.SetColumn(2, cam.displayTransform.column2);
            _displayTransform.SetColumn(3, cam.displayTransform.column3);
        }

        public void OnPreRender()
        {
            if (!_isActive)
            {
                return;
            }

            ARTextureHandles handles = UnityARSessionNativeInterface.GetARSessionNativeInterface().GetARVideoTextureHandles();
            if (handles.IsNull())
            {
                return;
            }

            Resolution currentResolution = Screen.currentResolution;

            if (_textureY == null)
            {
                _textureY = Texture2D.CreateExternalTexture(currentResolution.width, currentResolution.height, TextureFormat.R8, false, false, handles.TextureY);
                _textureY.filterMode = FilterMode.Bilinear;
                _textureY.wrapMode = TextureWrapMode.Repeat;
                _yuvMat.SetTexture("_textureY", _textureY);
            }

            if (_textureCbCr == null)
            {
                _textureCbCr = Texture2D.CreateExternalTexture(currentResolution.width, currentResolution.height, TextureFormat.R8, false, false, handles.TextureY);
                _textureCbCr.filterMode = FilterMode.Bilinear;
                _textureCbCr.wrapMode = TextureWrapMode.Repeat;
                _yuvMat.SetTexture("_textureCbCr", _textureCbCr);
            }

            _textureY.UpdateExternalTexture(handles.TextureY);
            _textureCbCr.UpdateExternalTexture(handles.TextureCbCr);

            _yuvMat.SetMatrix("_DisplayTransform", _displayTransform);
        }

        private void OnPostRender()
        {
            if (!_isActive)
            {
                return;
            }

            if (_textureY == null || _textureCbCr == null)
            {
                return;
            }

            Graphics.Blit(null, _arTexture, _yuvMat);

            RenderTexture back = RenderTexture.active;
            RenderTexture.active = _arTexture;
            _arTexture2D.ReadPixels(new Rect(0, 0, _arTexture.width, _arTexture.height), 0, 0);
            _arTexture2D.Apply();
            RenderTexture.active = back;

            _preview.texture = _arTexture2D;

            _result = QRCodeHelper.Read(_arTexture2D);

            if (_result != "error")
            {
                _text.text = _result;
                OnReadQRCode?.Invoke(_result);
            }
        }
#else
        public void Active(bool active)
        {
            _isActive = active;
        }
#endif
    }
}

まとめ

今回はARKitのカメラからの映像を利用して色々してみるという趣旨でしたが、ネイティブテクスチャからのテクスチャ生成および更新など、知らない機能についても知れたのでよかったです。

またそれ以外にも、(それなりに)高速にRenderTextureからTexture2Dへ内容をコピーするAsyncGPUReadback活用の幅が広そうです。

今後、ディープラーニングなど「画像解析」を経てなにかを行うことは増えていきそうなので、このあたりの処理は覚えておくとよさそうです。