げぇむぷろぐらみんぐ

日々の生活で得た知識、経験を書きます

Unity URPでの画面キャプチャ用パスを用いたCameraCaptureBridgeについて

久しぶりのブログです。
UnityのURPで、カメラに映っている画面をキャプチャして取り扱えるようにしてくれるパスであるCapturePassを用いた、CameraCaptureBridgeについて紹介します。

検証環境は下記になります。

  • Unity2021.3.14f1
  • Universal RP 12.1.8

できること

下記のスクショのように、描画結果をRenderTextureに書き出したりできます。 RenderTextureに書き出す以外にも、特定のRenderTargetに出力することもできます。

使い方

まずはソースコード全文になります。

using UnityEngine;
using UnityEngine.Rendering;

/// <summary>
/// 渡されたRenderTextureにキャプチャ結果を描画する
/// </summary>
public class ScreenCapture : MonoBehaviour
{
    [SerializeField]
    private RenderTexture _targetRenderTexture;

    private Camera _targetCamera;

    private void Start()
    {
        CaptureStart(Camera.main);
    }

    /// <summary>
    /// 指定したカメラが移しているもののキャプチャの開始
    /// </summary>
    /// <param name="camera">キャプチャしたい描画対象を写しているカメラ</param>
    public void CaptureStart(Camera camera)
    {
        _targetCamera = camera;
        CameraCaptureBridge.AddCaptureAction(_targetCamera, Capture);
    }

    /// <summary>
    /// キャプチャ終了
    /// </summary>
    public void CaptureEnd()
    {
        CameraCaptureBridge.RemoveCaptureAction(_targetCamera, Capture);
    }

    /// <summary>
    /// キャプチャ処理
    /// </summary>
    private void Capture(RenderTargetIdentifier renderTargetIdentifier, CommandBuffer commandBuffer)
    {
        commandBuffer.Blit(renderTargetIdentifier, _targetRenderTexture);
    }
}

上記のコードの中で重要なのは、キャプチャ開始部分の

CameraCaptureBridge.AddCaptureAction(_targetCamera, Capture);

キャプチャ終了部分の

CameraCaptureBridge.RemoveCaptureAction(_targetCamera, Capture);

になります。

キャプチャをしたい対象のカメラと、そのキャプチャによって得られたRenderTargetIdentifier をどう取り扱うかのActionをコールバックで渡します。
また、Removeするまではキャプチャをし続けられるので、不要になったタイミングでRemoveします。

基本的な使い方はこれだけです。
以降では、どのようにしてCaptureされているかをURPのソースコードを覗きつつ、注意したほうが良さそうなことについても紹介します。

コードを追ってみる

CameraCaptureBridgeのAddCaptureActionをまず見てみます。 すると、下記のような内容になっています。(コード)

public static void AddCaptureAction(Camera camera, Action<RenderTargetIdentifier, CommandBuffer> action)
{
   actionDict.TryGetValue(camera, out var actions);
   if (actions == null)
   {
         actions = new HashSet<Action<RenderTargetIdentifier, CommandBuffer>>();
         actionDict.Add(camera, actions);
   }

   actions.Add(action);
}

これを見ると、登録したActionはactionDictというDictionaryに追加されているようです。

なので、次はactionDictがどう使われているかを追ってみると、下記のようにDictionaryの内容をIEnumeratorとして返しているのがわかります。 (コード)

public static IEnumerator<Action<RenderTargetIdentifier, CommandBuffer>> GetCaptureActions(Camera camera)
{
   if (!actionDict.TryGetValue(camera, out var actions) || actions.Count == 0)
         return null;

   return actions.GetEnumerator();
}

さらに、上記のメソッドを呼んでいる箇所を探してみると、下記のようでした。 ここでは、CameraDataのcaptureActionsというものに取得した結果を渡しています。(コード)

static void InitializeStackedCameraData(Camera baseCamera, UniversalAdditionalCameraData baseAdditionalCameraData, ref CameraData cameraData)
{
...
cameraData.captureActions = CameraCaptureBridge.GetCaptureActions(baseCamera);
}

次に、cameraData.captureActionsを使っているところを探してみると、CapturePassに辿り着きました。 (コード)

internal class CapturePass : ScriptableRenderPass
{
   public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
   {
      CommandBuffer cmdBuf = CommandBufferPool.Get();
      using (new ProfilingScope(cmdBuf, m_ProfilingSampler))
      {
            var colorAttachmentIdentifier = m_CameraColorHandle.Identifier();
            var captureActions = renderingData.cameraData.captureActions;
            for (captureActions.Reset(); captureActions.MoveNext();)
               captureActions.Current(colorAttachmentIdentifier, cmdBuf);
      }

      context.ExecuteCommandBuffer(cmdBuf);
      CommandBufferPool.Release(cmdBuf);
   }
}

上のコードを見ると、cameraDataに渡したcaptureActionsをforで順次回して実行していることがわかります。
上記の挙動から、注意するべきこととして、渡すAction内ではRemoveCaptureAction を呼んではいけないことがわかります。
もし呼んでしまうと、ループ中にEnumeratorに変更が加わってしまい、例外が発生してしまいます。 なので、もし1Frameだけキャプチャしたい、という場合にも渡すAction内でRemoveするのではなく、上記の処理が終わったタイミングで別途Removeをしてあげる必要があります。

まとめ

今回は、URPで簡単にキャプチャ処理が実装できるCameraCaptureBridgeについて紹介しました。 こちらは、とても簡潔にキャプチャ処理ができる反面、キャプチャのタイミングをRenderPassEvent.AfterRendering以外にしたい場合などカスタマイズしたい場合には使用することができないので、使用機会は少ないかもしれませんが、求めている状況と合えばとても便利だと思います。

Firebase Cloud FirestoreとRealTimeDatabaseの違いを調べた

何かを調べたときにまとめながらのほうが勉強になるし、忘れても取り出せるようにしておきたいので、これからは気が向いたら調べたことをまとめます。 完全に自分用メモのため雑です。

Firebase Cloud Firestore

今まで使ってみてたのがFirebaseのRealTimeDatabaseでした。 Cloud FirestoreはRealTimeDatabaseよりも新しく、なんか強いらしいので、どんな違いがあるのか調べてみました。

データの持ち方

RealTimeDatabaseは、データ全体をJSONのツリーとして保持する。 すべてのデータはキーと対となる値を持ち、それが階層構造になって保持される。

具体的に例を出すと、以下のようになる。

{
    "users": {
        "hoge": {
            "firstname": "hoge",
            "lastname": "maru",
            "age": 14
        },
        "taro": {
            "firstname": "taro",
            "lastname": "jiro",
            "age": 21
        }
    }
}

上記のように、全てがキーと対となる値を持っており、その値の型はノードごとに異なっている。

参考: データベースの構造化  |  Firebase Realtime Database  |  Firebase

対して、Cloud Firestoreは、ドキュメント指向データベースであり、データはコレクションとドキュメントとフィールドからなる。 コレクションは、ドキュメントを保持し、ドキュメントはフィールドを保持するような階層型のデータ構造となっている。 コレクションがフィールドを持つことはできないが、ドキュメントがコレクションをサブコレクションとして持つことはできる。

具体的に例を出すと以下のようになる。

chatrooms(Col)
    roomA(Doc)
        name: "hoge"
        messages(Col)
            message1(Doc)
                "title": "fuga"

サブコレクションを使用すると、ドキュメント内のデータをまとめられるので、データが取りやすくなるらしい。

参考:

firebase.google.com

スケーラビリティ

RealTimeDatabaseは、一つのDBで10万件までで、それ以上はシャーディングが必要となる。 対して、Cloud Firestoreは自動でスケーリングされる。

Unityで使えるかどうか

RealTimeDatabaseはUnity用のSDKが公開されているため、利用可能となっている。

firebase.google.com

対して、Cloud FirestoreはSDKが公開されておらず、そのままでは利用できない。 そのため、Cloud Functions経由で操作することになるらしい。(対応してほしい…)

まとめ

RealTimeDatabaseに対して、Firestoreのほうがデータを綺麗に保持できそうな作りだと思いました。 とりあえず、普通に今後使う分にはFirestoreで良いのかな。 早くUnityに対応したSDKを出してほしいなあ。 Unity対応のSDKが出たらまた使ってみた記事を書こうと思います。

参考:

firebase.google.com

developers-jp.googleblog.com

【Unity】Blenderからモデルを書き出してBlendShapeをやってみた

  • はじめに
  • 動かした結果
  • BlendShapeとは
  • BlenderでBlendShape作成
  • UnityでBlendShapeアニメーションする
  • まとめ

はじめに

最近社内勉強会などでBlendShapeという単語をよく聞くのですが、概念を知っているだけで動かしてみたことがなかったのでやってみました。 何事も試してみるのが大事。

環境は、

Blenderはほぼ触ったことないのでかなり古いかもしれないですが、動いたので一旦良しとします。

続きを読む

【Unity】任意のコンポーネントがアタッチされたPrefab一覧を探す

はじめに

今回は、Unityで任意のコンポーネントがアタッチされたPrefab一覧を取得する方法についてまとめます。

Unity標準の機能だと、シーン上で任意のコンポーネントがアタッチされたオブジェクト一覧を探すことはできますが、Project全体から探すことはできないようでした。

そのため今回は、シェルスクリプトを用いて探す方法と、Unity上でエディタ拡張をすることによって探す方法を説明します。

今回の環境は、

です。

シェルスクリプトによる方法

探し方としては、以下のようになります。

  1. アタッチされていることを調べたいスクリプトのGUIDを調べる
  2. プロジェクト内のPrefab一覧を調べる
  3. Prefab一覧のmetaファイルで、1で調べたGUIDを持っているものがないか調べる

Unityで管理されているものにはmetaファイルが存在し、その中でGUIDというユニークなIDを持っているため、それを利用して探したいものを特定することができます。

具体的なスクリプトは以下です。

#!/bin/sh

GUID=`find ./Assets -type f -name $1.meta | xargs grep guid | sed -e 's/guid: //g'`
find ./Assets -type f -name "*.prefab" -print0 | xargs -0 grep -l $GUID

引数に渡されたファイル名のmetaファイルを探して、その中からguidが書かれている行を選択し、余計な文字列を除去してGUIDのみを取得して変数に格納します。 その後、格納したGUIDを持つPrefabを検索し、その名前を表示しています。 findとxargsにオプションがついているのは、以下の記事を参考に区切り文字をnull文字にしているからです。

qiita.com

先程のシェルスクリプトをreference.shという名前で保存して、実際に実行してみた結果が以下です。

% sh reference.sh Test.cs
./Assets/AttachedPrefab.prefab
./Assets/SubAttachedPrefab.prefab

Test.csがアタッチされているPrefabを探して、その結果としてAttachedPrefab.prefabとSubAttachedPrefab.prefabが返されました。

エディタ拡張による方法

次に、Unityエディタ上で任意のコンポーネントがアタッチされたPrefabを探す方法について説明します。

以下が指定されたコンポーネントがアタッチされているPrefab一覧を探すためのエディタ拡張のコードです。

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

namespace Sample.Editor
{
    public class ReferenceFinder : EditorWindow
    {
        private MonoScript _targetScript;
        private List<GameObject> _findAssets = new List<GameObject>();
        private Vector2 _currentScrollPosition;

        [MenuItem("Sample/ReferenceFinder")]
        private static void Open()
        {
            GetWindow<ReferenceFinder>("Reference Finder");
        }

        private void OnGUI()
        {
            EditorGUILayout.BeginHorizontal();
            _targetScript =
                EditorGUILayout.ObjectField("TargetScript", _targetScript, typeof(MonoScript), false) as MonoScript;
            EditorGUILayout.EndHorizontal();

            if (_targetScript == null || !_targetScript.GetClass().IsSubclassOf(typeof(MonoBehaviour)))
            {
                EditorGUILayout.LabelField("MonoBehaviourを継承したクラスを設定してください");
                _targetScript = null;
                return;
            }

            if (_findAssets.Count > 0)
            {
                _currentScrollPosition = EditorGUILayout.BeginScrollView(_currentScrollPosition);
                foreach (var asset in _findAssets)
                {
                    EditorGUILayout.ObjectField(asset.name, asset, typeof(GameObject), false);
                }

                EditorGUILayout.EndScrollView();
            }

            if (GUILayout.Button("Search"))
            {
                _findAssets.Clear();

                var guids = AssetDatabase.FindAssets("t:GameObject", null);


                foreach (var guid in guids)
                {
                    string path = AssetDatabase.GUIDToAssetPath(guid);
                    var loadAsset = AssetDatabase.LoadAssetAtPath<GameObject>(path);

                    if (loadAsset.GetComponent(_targetScript.GetClass()) != null)
                    {
                        _findAssets.Add(loadAsset);
                    }
                }
            }
        }
    }
}

やっていることは、

  1. 探したいコンポーネントをMonoScriptとして受け取る
  2. Searchボタンが押されたらプロジェクト内のGameObjectのGUID一覧を取得
  3. GUIDからオブジェクトをロード
  4. オブジェクトに対して任意のScriptをGetComponentして取得できるかをチェック
  5. 取得できたらリストに追加

です。

今回は、Prefabにアタッチできるスクリプトのみを指定できるようにしたかったため、MonoBehaviourを継承していないスクリプトは選択しても検索できないようにしました。

上記の拡張の結果以下のようなウィンドウで検索することができるようになりました。

f:id:siguma_sig:20181014155641p:plain

まとめ

今回は、プロジェクト内で任意のスクリプトがアタッチされたPrefabの一覧を取得する方法について説明しました。

今回の例では、Prefabの一覧のみを取得しましたが、任意のスクリプトがアタッチされたオブジェクトをシーン上に含むシーンの一覧なども今回のスクリプトを少し改造すれば作れるはずです。

なにか間違いや、もっと良い書き方があればぜひ教えてください!

Unityによる開発のときに使えるシェルスクリプト入門

はじめに

Unityで、あるスクリプトがアタッチされているPrefabをプロジェクト内からすべて見つけ出したいということがあったときに、Unityエディタの機能だと無さそうだったので先輩に相談しました。 すると、サクッとシェルスクリプトを書いて探し方を教えてくれて、かっけーとなったので僕も勉強してまとめてみました。

長くなってしまったので、今回は便利そうなコマンドのまとめだけになってしまいました。

シェルスクリプト初心者なので、もっとこう書くと簡潔だよとかあればどしどしご指摘お願いします。

使えそうなコマンド一覧

使えそうなコマンド一覧に関してどんなコマンドか紹介と、具体例を記述していきます。 コマンドの引数やオプションに取りうる詳細な情報は割愛します。

find

指定したディレクトリ以下に存在するファイルやディレクトリを再帰的に検索してくれるコマンドです。

例えば、Unityでプロジェクトのルートディレクトリにいるとして、Resourcesの中身一覧を見たくなった場合には以下のようなコマンドでできます。

% find Assets/Resources
  • ファイルだけ表示したくなった場合
% find Assets/Resources -type f
  • .prefabのみ表示したくなった場合
% find Assets/Resources -name "*.prefab"

xargs

標準入力から扱いたいものを受け取って、それを指定されたコマンドの引数に渡すためのコマンドです。 かなりわかりづらいですが、コマンドとコマンド間の仲介役のような感じです。

例えば、先程のfindと組み合わせて、Resources以下のどのディレクトリがサイズが大きいかを調べたくなったとすると以下のようなコマンドでできます。

% find  Assets/Resources -type d | xargs du

上記に関して、-typeの引数にdを指定するとディレクトリの一覧を得ることができます。それによって、得たディレクトリの一覧をディレクトリのサイズを調べるためのコマンドであるduの引数に渡しています。

この例をもとにもっと詳しく説明すると、まずはじめのfindコマンドの結果が以下のようになったとします。

% find  Assets/Resources -type d
Assets/Resources/Sprites
Assets/Resources/Animations
Assets/Resources/Font

これらのfindした結果に対して、ディレクトリのサイズを調べるためには愚直にやると以下のように一つづつ調べることになります。

% du Assets/Resources/Sprites
% du Assets/Resources/Animations
% du Assets/Resources/Font

これと同じように、xargsは標準入力から受け取った結果を直接duに渡してくれているため、一つのコマンドで完結します。

sed

与えられた文字列を指定した処理を加えて標準出力できるコマンドです。 主に読み込んだテキストであったり、入力したコマンドの結果の一部に手を加えたい場合に用います。

例えば、スクリプト内のTestという文字列をTest2に置き換えたくなった場合には以下のようなコマンドでできます。

% sed -i -e s/Test/Test2/g Hoge.cs

上記のコマンドでは、-iオプションによって直接Hoge.csの中身を書き換えています。 Macだと、上のコマンドを実行した場合は、-iのあとをバックアップの拡張子としてバックアップを保存してしまうので、Hoge.cs-eというファイルができてしまいます。 それを防ぐには、バックアップの拡張子を以下のように空にすれば良いです。

% sed -i '' -e s/Test/Test2/g Hoge.cs

また、-iでなく以下のようにすれば、書き換えた結果を他のファイルに書き込むこともできます。

% sed -e s/Test/Test2/g Test.cs

grep

与えられた文字列から正規表現に一致する行を出力できるコマンドです。 あるコマンドの結果を条件によって絞り込んで確認したい場合や、ファイル内に任意の文字列が含まれているかどうかの確認に使ったりできます。

例えば、a.txt内でTestという文字列が含まれる行を調べたい場合は以下のようなコマンドでできます。

% grep Test a.txt

また、今までのコマンド群を組み合わせて、Resourcesフォルダ内にあるPrefabのGUIDを調べるコマンドは以下のようになります。

find Assets/Resources -name "*.prefab" -type f | xargs grep guid

まとめ

よく使いそうなシェルコマンドについて調べてまとめてみました。 普段はgrepくらいしか使っていなかったのですが、これらが使いこなせるようになると簡単なファイルの一括操作などが非常に楽にできるようになりそうなので、ちょっとずつ使うコマンドを増やしていきたいです。

【Unity】FirebaseのRealTimeDatabaseを使ってみた

はじめに

直近でサーバーからのPushの実装が必要な案件があったのですが、サーバーサイドの知見はあまり持っておらずWebSocketを用いてある程度の同時接続に耐えうるように作るのは厳しそうだなと思ったので、 どうにかそこらへんから逃げつつ作れないかなと思い先輩に相談したところFirebaseのRealTimeDatabaseを勧められました。

実際にRealTimeDatabaseを使って実装してみたところ、想像以上に簡単にサーバーサイドの実装を終えることができたので書き残します。 クライアントサイドはUnityなので、今回はC#で記述しますがドキュメントを読んだ感じ他の言語でもほぼ同様なようでした。

RealTimeDatabaseとは

FirebaseのRealTimeDatabaseは公式サイトによると、

NoSQL クラウド データベースでデータの保管と同期を行うことができます。データはすべてのクライアントにわたってリアルタイムで同期され、アプリがオフラインになっても、利用可能な状態を保ちます。

とあるように、リアルタイムに同期可能なデータベースです。

クライアントから直接データベースへの書き込みや読み取りを行うような方法でやり取りを行うため、サーバー側は全く意識することなく実装を行うことができます。

また、お値段も非常に安く、同時接続100までは無料で使用することができます。

firebase.google.com

使い方

Unityプロジェクトに対するFirebaseの導入方法などは公式のサイトをご覧ください。

Unity の Firebase Realtime Database を使ってみる  |  Firebase

今回は、データの取得、更新と、リアルタイムに更新を受け取る方法について説明します。

データの取得

データの取得は、以下のようにGetValueAsyncというメソッドを用いて行うことができます。 今回は、コールバックによって結果を受け取っていて、taskが完了したタイミングで結果をLogとして表示しています。

FirebaseDatabase.DefaultInstance.GetReference("user")
    .GetValueAsync().ContinueWith(task =>
    {
        if (task.IsFaulted)
        {
            Debug.LogError("not found users");
        }
        else if (task.IsCompleted)
        {
            DataSnapshot snapShot = task.Result;
            Debug.Log(snapShot.Value);
        }
    });

上記のコードは、以下のようなデータ構成だった場合、user以下すべてのデータを読み取ります。

  • user
    • books
    • animals
    • age

すべてを読み取るのではなく、user以下のある特定のデータを読み取る場合は以下のようにします。

FirebaseDatabase.DefaultInstance.GetReference("user")
    .Child("age")
    .GetValueAsync().ContinueWith(task =>
    {
        if (task.IsFaulted)
        {
            Debug.LogError("not found users");
        }
        else if (task.IsCompleted)
        {
            DataSnapshot snapShot = task.Result;
            Debug.Log(Convert.ToInt32(snapShot.Value)); // user's age
        }
    });

このように、読み取りたいところまでChildで指定して行ってGetValueAsyncを呼べば、読み取りたいデータのみを読み取ることができます。 また、上記のコードを見るとわかるように、結果はすべてobject型で返されるため適宜変換が必要となるので注意してください。

データの書き込み

データの書き込みに関しても、読み取りとほぼ同様に書き込みたいところまでChildで指定していき、SetValueAsyncによって書き込みを行います。 以下に例を示します。

FirebaseDatabase.DefaultInstance.GetReference("users")
    .Child("age")
    .SetValueAsync(21);

こちらは、基本的には特に型変換もすることなく値を設定することができます。 すでにデータが存在していた場合は上書きされ、存在していなかった場合は新しく子要素として作られます。

データの更新通知

今回一番欲しかった機能である更新検知を行う方法について説明します。 仕組みとしては、ある値が変わったときに呼ばれるメソッドを登録しておくことができます。 以下のように記述することで、メソッドを登録できます。

FirebaseDatabase.DefaultInstance.GetReference("users")
    .Child("age").ValueChanged += HandleAgeValueChanged;

private void HandleAgeValueChanged(object sender, ValueChangedEventArgs args)
{
    if (args.DatabaseError != null)
    {
        Debug.LogError(args.DatabaseError.Message);
        return;
    }


    Debug.Log(args.Snapshot.Value); // current age
}

登録されたメソッドの内部では、データ取得のときとほぼ同様に、SnapShotから現在の値を取得しています。 どこかでageに対して更新処理があった際には、自動的に上記のメソッドが呼ばれることになります。

まとめ

今回は、FirebaseのRealTimeDatabaseを用いてデータの取得、更新、更新通知を行う方法についてまとめました。 その他にもトランザクションとしてデータの書き込みを行うこともできたり、以下の記事のようにリレーションを実現することもできるようです。

qiita.com

まだ実運用していないので、同時接続などの性能面での不安が残っているのですが、ここらへんに関しても学びがあったらまたまとめたいと思います。

【Unity】ScreenSpace OverlayなCanvas上に3Dモデルを表示する

はじめに

通常、ScreenSpaceをOverlayに指定した場合は最前面にCanvas上のUIの描画が来るため、3Dモデルをさらにその上に表示することはできません。 今回は、Canvasの設定はOverlayにしておきたいが、3DモデルをUIの上に表示したい!という場合の対応方法の一つをやってみたのでまとめます。

完成形

以下のようにOverlayなCanvasのUI上に3D表示を行い、それを切り替えることができるようになります。

f:id:siguma_sig:20180908231417g:plain

やり方

やり方は単純で、3Dモデルの描画をRenderTextureに行いCanvas上ではRawImageでそのRenderTextureの表示を行えば完了です。

順を追って具体的に説明します。

RenderTextureの作成

Projectウィンドウで Create > RenderTextureで作成できます。 細かい設定が気になる方はこちらを見てください。

f:id:siguma_sig:20180908232123p:plain

3Dモデル描画用のカメラを作成

Hierarchy上でメニューから選択してCameraを新しく作ります。その際に、ClearFlagsをSolidColorにしてアルファ値を0にしておいてください。 また、先ほど作成したRenderTexutreをTargetTextureに設定してください。

f:id:siguma_sig:20180908232232p:plain

RawImageの配置

OverlayなCanvas上にRawImageを配置して、Textureに先程作成したRenderTextureを設定します。

f:id:siguma_sig:20180908232442p:plain

カメラの微調整

これで新しく作成したカメラに描画されているものがUIとして表示されるようになります。 写したい3Dモデルが映るような位置に微調整をしましょう。

まとめ

同様な方法で、パーティクルに関してもOverlayなCanvasの上に表示を行うことができます。 ただし、RenderTextureへの描画を行うためにCameraを一つ作成する必要があるため、パフォーマンスのことを考えるとUI上に3Dモデルをばんばん出したいという場合には向いていないかもしれません。

OverlayのままUI上に3Dモデルを表示できるいい方法が他にあれば是非教えてください。