e.blog

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

Magic Leap One開発入門

概要

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

※ なお、本記事はUnity2019の、Lumin OS向けの開発がデフォルトで採用されたあとの話となります。

まずはCreator Portalにログイン

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

creator.magicleap.com

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

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

Unity2019以降をインストール

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

Settingsを調整

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

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

XR Legacy Input Helperをインストール

まず、PackageManagerからXR Legacy Input Helpersをインストールします。

f:id:edo_m18:20190710133843p:plain

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)

            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での確認をするのに、専用のアプリを利用します。
ドキュメントは以下。

Learn | 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

その他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

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

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

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

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周りについて詳しくなるのでオススメです。

GPUを支える技術 ――超並列ハードウェアの快進撃[技術基礎] (WEB+DB PRESS plus)

GPUを支える技術 ――超並列ハードウェアの快進撃[技術基礎] (WEB+DB PRESS plus)


閑話休題

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活用の幅が広そうです。

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

特定のUIを除いて画面のスクリーンショットを撮影する

概要

ゲーム画面のスクリーンショットを撮影したい、というのはよくある要望でしょう。
そしてさらに「特定のUIだけを除いて」撮影したい、というのもわりとある要望だと思います。
例えば操作用のUIは非表示だけども、ステータス表示などは表示しておきたい、などですね。

今回はそれを実現する方法について書きたいと思います。

なお、今回の記事の動作サンプルはGithubにアップしてあります。

github.com

実際に動かした動画↓

大まかなフロー

まず詳細を説明する前に、必要な項目について簡単に説明しておきます。

  1. GUI用のカメラを用意する
  2. 無視したいUIを持っているCanvasのレイヤーを適切に設定する
  3. CommandBufferを用いて、UI以外の要素がレンダリングされたあとのタイミングでバッファをコピーする
  4. GUI描画時に、カメラのレンダリングするレイヤー(cullingMask)を変更する
  5. GUI用カメラを、(3)で取得したテクスチャに追加で描画する

という流れになります。

詳細については順に説明していきます。

GUI用カメラを用意する

Canvas要素はデフォルトではRender ModeScreen Space - Overlayになっています。
これをまずScreen Space - Cameraに変更します。

すると以下のように、GUI要素をレンダリングするためのカメラを設定する項目が表示されるのでGUI用に用意したカメラを設定します。

f:id:edo_m18:20190208200618p:plain

なお、GUI用に用意したカメラではClear FlagsDepth Onlyに変更しておきます。

f:id:edo_m18:20190208200730p:plain

これでカメラ側の準備はOKです。

Canvasにレイヤーを設定する

続いて、Layerを設定していきます。
Cameraにはレイヤーごとにレンダリングするかしないか、というマスクが設定できるようになっており、これを利用してキャプチャ時に描画対象とするか、を切り替えます。

なので、まずは無視するUIのレイヤーを設定できるように新しいレイヤーを追加します。
(今回のサンプルではMenuUIという名前にしました)

そして新しいレイヤーが追加できたら、キャプチャ時に無視したいUI Canvasにそのレイヤーを設定します。

f:id:edo_m18:20190208211428p:plain

なお、レイヤーマスクの設定で無視されるかどうかはCanvasのレイヤー設定のみが反映されるようです。
なので、UI要素に対して個別にレイヤーを設定してもレンダリングされてしまいます。

もし細かく制御したい場合は、Canvas入れ子にして、そのCanvasにレイヤーを設定し、さらにそのCanvasの小要素としてキャプチャされたくないUI要素を入れることで対応することができます。

CommandBufferを用いてシーンの状況をキャプチャする

今回はシーンのキャプチャにCommandBufferを用いることにしました。
(もちろん、それ以外の方法でもキャプチャできます)

なお、CommandBuffer自体の詳細については凹みさんの記事がとても分かりやすく書かれているのでオススメです。

tips.hecomi.com

CommandBufferをカメラに登録する

CommandBufferは、カメラのレンダリングのパイプラインの中で、特定のイベントに紐づけて呼び出される「コマンド」を追加できる仕組みです。

今回はすべてのオブジェクトがレンダリングされ終わったタイミングでキャプチャしたかったのでCameraEvent.BeforeImageEffectsというタイミングでキャプチャを行っています。

コマンドバッファの生成は以下のようにしています。

/// <summary>
/// バッファを生成する
/// </summary>
private void CreateBuffer()
{
    _buf = new RenderTexture(Screen.width, Screen.height, 0);

    _commandBuffer = new CommandBuffer();
    _commandBuffer.name = "CaptureScene";
    _commandBuffer.Blit(BuiltinRenderTextureType.CurrentActive, _buf);
}

行っていることは単純に、生成したRenderTextureに、現在の(CurrentActive)状態をコピーしているだけです。

あとは、スクリーンショットを撮影するタイミングでこれをカメラに追加してやれば、適切なタイミングで処理が実行されます。

実際に実行すると以下のような形になります。

private void Update()
{
    if (Input.GetKeyDown(KeyCode.S))
    {
        TakeScreenshot();
    }
}

/// <summary>
/// スクリーンショットを撮影する
/// </summary>
public void TakeScreenshot()
{
    Camera.main.AddCommandBuffer(_cameraEvent, _commandBuffer);

    StartCoroutine(WaitCapture());
}

/// <summary>
/// コマンドバッファの処理を待つ
/// </summary>
private IEnumerator WaitCapture()
{
    yield return new WaitForEndOfFrame();

    BlendGUI();

    Camera.main.RemoveCommandBuffer(_cameraEvent, _commandBuffer);
}

スクリーンショットを撮影したいタイミングでコマンドバッファをカメラに追加し、そのフレームの終わりまで待機した上でコマンドバッファを取り除いています。

ここで取り除いているのは、取り除かないと以後常にコマンドバッファが呼ばれてしまうため一度だけシーンをコピーしたら呼び出されないようにしています。

ここまでで、GUIを除く部分がRenderTextureに描かれている状態となります。
あとはGUI部分を適切に設定して描画してやれば目的の絵を得ることができます。

描画対象のGUIだけを上乗せする

最後は、必要なGUI部分を描くことができれば目的達成です。
GUI部分を描画するには以下のようにします。

/// <summary>
/// GUI要素をブレンドする
/// </summary>
private void BlendGUI()
{
    _guiCamera.targetTexture = _buf;

    int tmp = _guiCamera.cullingMask;
    _guiCamera.cullingMask = _captureTargetLayer;

    _guiCamera.Render();

    _guiCamera.cullingMask = tmp;

    _guiCamera.targetTexture = null;
}

上記でやっていることは、GUI用カメラのtargetTextureに、コマンドバッファによってシーンがコピーされたバッファを適用し、また取り除きたいレイヤーを設定した上でGUI用カメラのレンダリングを行っています。

cullingMaskが、レンダリング対象となるかどうかのマスク情報を設定するレイヤーマスクです。
ここに、今回はMenuUIレイヤーを持つ要素を外してレンダリング_guiCamera.Render())しています。

最後にtargetTexturecullingMaskを元に戻して終わりです。

これで、最終的に得たい、不要なGUI要素が除かれた状態でシーンをキャプチャすることができます。

コード全文

最後に、今回のサンプルのコード全文を掲載しておきます。
なお、撮影したスクショをファイルに保存する処理についてはGithubのプロジェクトを参照してください。
(今回の主題からははずれるので割愛します)

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

public class SelectGUICapture : MonoBehaviour
{
    [SerializeField, Tooltip("GUIをレンダリングしているカメラ")]
    private Camera _guiCamera = null;

    [SerializeField, Tooltip("キャプチャするタイミング")]
    private CameraEvent _cameraEvent = CameraEvent.BeforeImageEffects;

    [SerializeField, Tooltip("合成時に無視されるUIのレイヤー")]
    private LayerMask _captureTargetLayer = -1;

    private Camera _mainCamera = null;
    private RenderTexture _buf = null;
    private CommandBuffer _commandBuffer = null;

    #region ### MonoBehaviour ###
    private void Awake()
    {
        CreateBuffer();
    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.S))
        {
            TakeScreenshot();
        }
    }

    /// <summary>
    /// 動作確認用にGizmoでテクスチャを表示する
    /// </summary>
    private void OnGUI()
    {
        if (_buf == null) return;
        GUI.DrawTexture(new Rect(5f, 5f, Screen.width * 0.5f, Screen.height * 0.5f), _buf);
    }
    #endregion ### MonoBehaviour ###

    /// <summary>
    /// バッファを生成する
    /// </summary>
    private void CreateBuffer()
    {
        _buf = new RenderTexture(Screen.width, Screen.height, 0);

        _commandBuffer = new CommandBuffer();
        _commandBuffer.name = "CaptureScene";
        _commandBuffer.Blit(BuiltinRenderTextureType.CurrentActive, _buf);
    }

    /// <summary>
    /// スクリーンショットを撮影する
    /// </summary>
    public void TakeScreenshot()
    {
        AddCommandBuffer();

        StartCoroutine(WaitCapture());
    }

    /// <summary>
    /// コマンドバッファの処理を待つ
    /// </summary>
    private IEnumerator WaitCapture()
    {
        yield return new WaitForEndOfFrame();

        BlendGUI();

        RemoveCommandBuffer();
    }

    /// <summary>
    /// メインカメラにコマンドバッファを追加する
    /// </summary>
    private void AddCommandBuffer()
    {
        if (_mainCamera == null)
        {
            _mainCamera = Camera.main;
        }

        _mainCamera.AddCommandBuffer(_cameraEvent, _commandBuffer);
    }

    /// <summary>
    /// メインカメラからコマンドバッファを削除する
    /// </summary>
    private void RemoveCommandBuffer()
    {
        if (_mainCamera == null)
        {
            return;
        }

        _mainCamera.RemoveCommandBuffer(_cameraEvent, _commandBuffer);
    }

    /// <summary>
    /// GUI要素をブレンドする
    /// </summary>
    private void BlendGUI()
    {
        _guiCamera.targetTexture = _buf;

        int tmp = _guiCamera.cullingMask;
        _guiCamera.cullingMask = _captureTargetLayer;

        _guiCamera.Render();

        _guiCamera.cullingMask = tmp;

        _guiCamera.targetTexture = null;
    }
} 

揚力を計算して滑空する

概要

今モックで作成しているVRコンテンツに、滑空の要素を入れたかったので揚力の計算について調べてみたのでそのメモです。

ちなみに適当に飛行機っぽい形状を作って適用した動画はこんな感じです↓

また、実装にあたって参考にさせてもらったのは以下の記事です。

64章:二次元翼の揚力と抗力

www.cfijapan.com

blog.goo.ne.jp

blog.goo.ne.jp

揚力の計算

揚力(Lift)は以下の数式から求めることができるようです。

$$ L = C_L \frac{ρ}{2} q^2 A $$

$$ D = C_D \frac{ρ}{2} q^2 A $$

ここで、\(C_L\)は無次元の係数で揚力係数、\(C_D\)は無次元の係数で抗力係数といいます。
また、数式の記号の意味は以下となります。

\(ρ\)は空気密度、\(q\)は流速、\(A\)は翼の面積です。

それぞれの単位は以下のようになります。

  • \(ρ\) ... \(kg/m^3\)
  • \(q\) ... \(m/s\)
  • \(A\) ... \(m^2\)

流体の密度\(ρ\)ですが、場所によって異なるようで、海面高度の大気中は大体\(1.2250kg/m^3\)となるようです。
また、揚力係数、抗力係数は実験的に求められているようで、こちらの記事から画像を引用させていただくと以下のようなグラフになるようです。

力のかかる方向

揚力、抗力については上で書いた通りです。
この力のかかる方向を図示すると以下のようになります。

f:id:edo_m18:20190113152919j:plain

\(\theta\)が進行方向と流体(空気)との成す角度です。
そして\(L\)が揚力(Lift)を、\(D\)が抗力(Drag)を、\(mg\)が重力方向を表しています。

この図を見てもらうと分かりますが、揚力は進行方向に対して垂直、抗力は進行方向と平行(ただし逆向き)となります。

揚力のかかる方向を求める

色々な記事を見ても、揚力のかかる方向が垂直であることは示されているものの、「じゃあ実際プログラムするときに垂直方向ってどっちさ?」となり、色々考えた結果、以下のようにして求めるようにしたところ、それっぽく動いているのでこれを採用しています。

実際は流体力学などから、流体、そして渦の生成、気圧など様々な条件から方向が定まり、実際に揚力のかかる方向を計算するのだと思いますが、今回はあくまで「それっぽく」動くことが目的だったので単純に垂直な方向を求めています。

考え方としては以下のような感じです。

  1. 進行方向との垂直方向、つまり外積の向く向きを採用する
  2. しかし「飛行機が逆さま」になっていることも考慮すると、もうひとつの軸の取り方で結果が変わってしまう
  3. 平行に近い角度で進行している場合は、翼の「右」方向と進行方向との外積が「上」として都合が良さそう(進行方向との外積なので必ず進行方向に対して垂直となる)
  4. 反対の考慮は?
  5. 翼の「上」ベクトル(leftWing.transform.up)と進行方向との外積方向をまず求める
  6. 通常飛行(翼の前ベクトルと平行的な方向)の場合においては、翼の右ベクトル(leftWing.transform.rightと(5)のベクトルは概ね同じ方向を向く
  7. 翼に対して後ろ方向に進行している場合は右ベクトルとは反対方向を向くようになる
  8. (5)のベクトルと右ベクトルとの「内積」を計算し、マイナスとなる場合は逆と判断
  9. -leftWing.transform.rightを計算のベクトルに用いる

というようなフローで解決するようにしてみました。

実際に、翼の上ベクトルと進行方向との外積で求めたベクトルの動きを動画にしてみると以下のような感じになります。


進行方向の判別(揚力の適用向き用)

黄色のバーが進行方向を表し、紫色のバーが外積によって求めたベクトルです。
動画を見てもらうと分かりますが、進行方向が翼に対して前方に向いている場合は、翼の右ベクトルとの角度は鋭角になっており、進行方向が逆転した場合に鈍角になることが分かります。

このことから、翼の右ベクトルと求めたベクトルとの内積を取り、結果がマイナス(=鈍角)なら進行方向が想定と逆、とみなすことができるわけです。

コードにすると以下のような感じです。

Vector3 dir = v.normalized;

// 進行方向に対しての「右」を識別する
Vector3 lcheckDir = Vector3.Cross(_leftWing.up, dir);

float lcheckd = Vector3.Dot(_leftWing.right, lcheckDir);

Vector3 lright = (lcheckd < 0) ? -_leftWing.right : _leftWing.right;

// ldirが揚力の働く方向(Lift Direction)
Vector3 ldir = Vector3.Cross(dir, lright);

コード全文

そんなに長くないのでコード全文を載せておこうと思います。

ちなみに「揚力係数」と「抗力係数」についてはアニメーションカーブを用いて、概ね以下のような感じで設定してそこから値を取得するようにしています。

揚力係数のアニメーションカーブ(グラフ)

f:id:edo_m18:20190113172042p:plain

抗力係数のアニメーションカーブ(グラフ)

f:id:edo_m18:20190113172125p:plain

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

public class LiftTest : MonoBehaviour
{
    [SerializeField]
    private Transform _body;
    
    [SerializeField]
    private Transform _leftWing;

    [SerializeField]
    private Transform _rightWing;

    [SerializeField]
    private AnimationCurve _liftCoeff;

    [SerializeField]
    private AnimationCurve _dragCoeff;

    [SerializeField]
    private ParticleSystem _lparticle;

    [SerializeField]
    private ParticleSystem _rparticle;

    [SerializeField]
    private float _pitchSpeed = 0.5f;

    [SerializeField]
    private float _rollSpeed = 0.5f;

    [SerializeField]
    private float _acc = 1000f;

    [SerializeField]
    private float _rho = 1.225f;

    [SerializeField]
    private float _area = 12f;

    [SerializeField]
    private float _initVelocity = 30f;

    [SerializeField]
    private bool _useLift = true;

    private Rigidbody _rigid;

    private void Start()
    {
        _rigid = GetComponent<Rigidbody>();
        _rigid.velocity = transform.forward * _initVelocity;

        _lparticle.Stop();
        _rparticle.Stop();
    }

    private void Update()
    {
        Control();
    }

    private void FixedUpdate()
    {
        if (_useLift)
        {
            CalcLift();
        }
    }

    private void Control()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            _lparticle.Play();
            _rparticle.Play();
        }

        if (Input.GetKeyUp(KeyCode.Space))
        {
            _lparticle.Stop();
            _rparticle.Stop();
        }

        if (Input.GetKey(KeyCode.Space))
        {
            _rigid.AddForce(_body.forward * _acc);
        }

        if (Input.GetKey(KeyCode.DownArrow))
        {
            _body.Rotate(_body.worldToLocalMatrix.MultiplyVector(_body.right), -_pitchSpeed);
        }

        if (Input.GetKey(KeyCode.UpArrow))
        {
            _body.Rotate(_body.worldToLocalMatrix.MultiplyVector(_body.right), _pitchSpeed);
        }

        if (Input.GetKey(KeyCode.LeftArrow))
        {
            _body.Rotate(_body.worldToLocalMatrix.MultiplyVector(_body.forward), _rollSpeed);
        }

        if (Input.GetKey(KeyCode.RightArrow))
        {
            _body.Rotate(_body.worldToLocalMatrix.MultiplyVector(_body.forward), -_rollSpeed);
        }
    }

    private void CalcLift()
    {
        Vector3 lpos = _leftWing.position;
        Vector3 rpos = _rightWing.position;

        Vector3 lup = _leftWing.up;
        Vector3 rup = _rightWing.up;

        Vector3 v = _rigid.velocity;

        float m = v.magnitude;
        float velocitySqr = m * m;

        Vector3 dir = v.normalized;

        // 揚力、抵抗ともに使う係数の計算(ρ/2 * q^2 * A)
        // ρ ... 密度
        // q ... 速度
        // A ... 面積
        float k = _rho / 2f * _area * velocitySqr;

        Debug.DrawLine(_body.position, _body.position + dir, Color.black);

        float ldot = Vector3.Dot(lup, dir);
        float lrad = Mathf.Acos(ldot);

        float rdot = Vector3.Dot(rup, dir);
        float rrad = Mathf.Acos(rdot);

        float langle = (lrad * Mathf.Rad2Deg) - 90f;
        float rangle = (rrad * Mathf.Rad2Deg) - 90f;

        float lcl = _liftCoeff.Evaluate(langle);
        float rcl = _liftCoeff.Evaluate(rangle);

        // 単位: N = kg・m/s^2
        float ll = lcl * k;
        float rl = rcl * k;

        // 進行方向に対しての「右」を識別する
        Vector3 lcheckDir = Vector3.Cross(_leftWing.up, dir);
        Vector3 rcheckDir = Vector3.Cross(_rightWing.up, dir);

        float lcheckd = Vector3.Dot(_leftWing.right, lcheckDir);
        float rcheckd = Vector3.Dot(_rightWing.right, rcheckDir);

        Vector3 lright = (lcheckd < 0) ? -_leftWing.right : _leftWing.right;
        Vector3 rright = (rcheckd < 0) ? -_rightWing.right : _rightWing.right;

        Vector3 ldir = Vector3.Cross(dir, lright);
        Vector3 rdir = Vector3.Cross(dir, rright);

        Vector3 lv = ldir * ll;
        Vector3 rv = rdir * rl;

        Debug.DrawLine(_leftWing.position, _leftWing.position + lv, Color.cyan);
        Debug.DrawLine(_rightWing.position, _rightWing.position + rv, Color.cyan);

        float lcd = _dragCoeff.Evaluate(langle);
        float rcd = _dragCoeff.Evaluate(rangle);

        float ldrag = lcd * k;
        float rdrag = rcd * k;

        Vector3 drag = -dir * (ldrag + rdrag);

        Debug.DrawLine(_body.position, _body.position + drag);

        Vector3 force = (lv + rv + drag) * Time.deltaTime;;

        _rigid.AddForce(force);
    }
}