ユーセンブログ

ゲーム開発に関することをたまに書きます

モバイルのGPUアーキテクチャ(mobile tile based rendering)について調べてみた

はじめに

モバイルでシェーダーを取り扱う際に、カットアウト(ピクセルの破棄)は使うなという話をよく聞きます。 なぜダメなのかという理由について、調べてみました。

結論としてはモバイル系で使われるGPUではカットアウトを使ってしまうとTBDRの利点が全く受けれなくなってしまい、結果としてかなりのオーバーヘッドが出てしまうためでした。

TBDR

TBDR( tile based rendering)というのは画面のピクセルを一定の大きさに切り分け、その単位ごとにライティングなどの処理を行うレンダリング方式のことです。 処理の順番としてはざっくり

  1. タイルに分割
  2. ジオメトリの前後関係から描画するポリゴンの決定(ジオメトリカリング)
  3. レンダリング

となります。

カットアウトがダメな理由

じゃあなんで、モバイルでカットアウトがだめかというと、理由はこれ。

Alpha testing, clip() and discard() are fragment shader operations. All of the speed advantages of TBDRs come from culling hidden surfaces at the geometry level. Once you introduce the ability to discard individual fragments, geometry culling doesn't work. (アルファテスト、clip()、およびdiscard()はフラグメントシェーダー操作です。 TBDRの速度の利点はすべて、ジオメトリレベルで非表示のサーフェスを選択することから得られます。個々のフラグメントを破棄する機能を導入すると、ジオメトリカリングは機能しません。)

ポリゴンの前後関係をジオメトリレベルで決定できるのがTBDRの利点なのにカットアウトではフラグメントで前後関係を決定してしまうのでTBDRの利点を生かせないどころか、

ジオメトリカリングをする→カットアウトがあるのでジオメトリカリングの結果を破棄してフラグメントでのZテスト(重い)となる

結果としてPCのアーキテクチャと異なりカットアウトが非常に重いということになるようです。

参考文献

game.watch.impress.co.jp

tech.drecom.co.jp

https://docs.unity3d.com/ja/current/Manual/SL-ShaderPerformance.html

light11.hatenadiary.com

http://research.tri-ace.com/Data/AndroidABC2014_Final.pptx

【Unity】async/awaitをコルーチン的に(簡単に)使いたい

はじめに

async/await使えば処理を早くしやすくなるし、コルーチンっぽく使えれば便利なのではということでサクッと非同期的な処理をするときどうするか調べてみました。 webからデータを読み込んで~みたいなのはたくさんあったのでとにかく重たい関数、処理を非同期にぶん投げるにはどうするかを中心に書いて行きます。

なぜ、コルーチンではなくasync/awaitなのか

理由としては

  • コルーチンだと戻り値がないので欲しい時面倒くさい。
  • 別スレッドに投げているわけではないので厳密にいう並列処理にはできない。
  • いちいちStartCoroutineで呼ぶのがダルい。関数呼び出し一発でいいじゃん。
  • UnityがC#7になったし、そろそろ使って行きたい。

から重要なところだけでなく、細かくasync/awaitを使っていきたいと思ってます。

使っていく

asyncな関数

基本的な関数の宣言方法としては、asyncをつけた関数の中で何らかのTaskをawaitで待たせることで単一の関数の中で非同期に処理を終わるのを待たせることができます。簡単な例をいくつか紹介します。

  • 並列処理の例
  • 数秒間待ってから処理
  • 定期的に処理を行う
並列処理

Taskの中身が別のスレッドで実行されている。

 using System.Threading.Tasks;
    private async Task  AsyncFunc()
    {
        //awaitをTask.Runにつけると処理を待ってくれる
        await Task.Run(() =>
        {
            hogehoge();
        });
        fugafuga();
    }
一定時間処理を待ってから実行

Task.Delayで一定時間待ってから処理をする。別スレッドから帰ってくるのを待っているため、別の処理は待っている間動き続ける。

 using System.Threading.Tasks;
    private async Task AsyncFunc()
    {
        await Task.Delay(1000);//1000ミリ秒(一秒)待って
        hogehoge();//処理を実行
    }
定期的に処理を行う

ループを自分で作って一定時間を待って定期的に処理を回す。

 using System.Threading.Tasks;
    async Task AsyncFunc()
    {
     //だいたい1フレに一回ぐらいのペースで実行される
        while(true)
        {
            await Task.Delay(16);
            hogehoge();
            //何かしらの条件で抜ける
            if ("ループ脱出条件") break;
        }
    }

戻り値にTaskがあるのにretrun文がないぞ?となると思うのですが、asyncな関数は戻り値のTaskをvoidとして扱うことができます。なので実は戻り値はvoidでも動きます。しかし、関数を呼び出す側がタスクの状態を見る術がなくなるのでTaskで返してあげると親切です。

また、注意事項として、並列処理の例で上げたTask.Runの中では、Unityのオブジェクト類の操作(transformの操作など)はできません。基本的にはファイル操作やデータのエンコード、デコードなどに使用します。

呼び出し方

呼び出し方は戻り値がvoidなものであればシンプルにasyncな関数を普通に呼び出せば実行できます。 そのまま呼び出すと、呼び出す際に終わるの待たないで勝手に処理するぞ的な警告が出ますが、無視してもちゃんと非同期で動きます。気になる方は帰って来るTaskを変数で受け取っておけば警告は消えます。

f:id:k_tachiban:20190411194024p:plain
呼び出し時にはこんな感じの警告が出ますが無視してもOK

 using System.Threading.Tasks;
    public void main()
    {
         //これだけでもOK
         AsyncFunc();

        //お行儀良く書くなら呼び出し方は基本的にこっち
        //Taskを受けとっておけばあとでコケたときとかのエラー処理とかできる。警告も消える。
        //戻り値がほしいときは引数のTaskを後で使う
        Task task = AsyncFunc();
    }

戻り値つきのasyncな関数

戻り値付きのasyncな関数を使う場合は戻り値をTaskの形で宣言し、return Tの形で返します。 関数は基本的にはこれだけですが、呼び出し側に少し注意が必要になります。

    async Task<string> ReturnAsyncFunc()
    {
        await Task.Delay(1000);
        return "hogehoge";
    }

戻り値付きのasync関数を使う場合は、呼び出し側の関数もasyncにする必要があります。 理由としては、 この場合asyncをつけないと、Task.ResultがReturnAsyncFunc()の完了を待つため、main関数の中で処理が止まり、結果としてReturnAsyncFunc()のTask.Delay(1000);の影響で1000ミリ秒がメインスレッドの処理が止まってしまいます(全体の処理が止まる)。 そのため、呼び出し側の関数にasyncをつけてawait task;で非同期に完了を待ち、結果を受け取る必要があります。

呼び出し方

    public async Task main()
    {
        //戻り値が必要な場合は一度Taskの変数を持って
        Task<string> task =  ReturnAsyncFunc();
        //awaitでタスクの終了を非同期で待ち
        await task;
        //resultで受け取る。
        string hoge =   task.Result;

        //短縮したい場合はこうでも行ける
        hoge = await ReturnAsyncFunc();
    }
//この書き方だと処理が止まる(main関数にasync/awaitなし)
    public Task main()
    {
        Task<string> task =  ReturnAsyncFunc();
        string hoge =   task.Result;
    }

感想

  • コルーチンでよく使う処理の一部はこれで結構簡略化できそう
  • 呼び出しタイミングをきちんと制御した。スレッドをしばき倒したい。などの場合はUniTaskがあると良さそう
  • コルーチンのほうがいい場合もおそらくあるのでそこはケースバイケース

    参考文献

    qiita.com

neue.cc

【Unity】ViveのHandTracking SDKのサンプルをVive Proで試してみる

はじめに

Viveのカメラを使ったHandTackingのSDKが公開されました。 細かい説明は3/19のGDCで説明があると思うのですが、とりあえず試してみたいということでサンプルを落として動かしてみたのでまとめます。

community.viveport.com

動作環境

  • Unity 2018.3.8f1
  • GeForce Game Ready Driver 419.35
  • Vive Pro

手順

SDKの準備

まずはHandTrackingのSDKをこちらのページからダウンロードします。ViveProかつUnityの場合以外は上のResorcesタブから該当のSDKを落としてきてください。 developer.vive.com

Unityの設定

ダウンロードしたファイルの中に”Hand Tracking SDK Unity Sample.unitypackage”と"Hand Tracking SDK Unity.unitypackage"が入っているのでこれをUnityにインポートします。 その後、Package ManagerからOpenVR Pluginをインポートして、XR SettingsのVirtual Reality Supportedをオンにしてください。 これでUnity側の準備は完了です。しかし、このままVive ProをつなげてもVive Proのカメラが有効になっていないためハンドトラッキングをすることはできません。

Vive Proのカメラを有効にする

カメラを有効にするためには、Steam VRの設定>カメラから"カメラを有効にする"を選択して有効化してください。 カメラを有効化することができたら一度Steamを再起動してカメラのウィンドウからカメラレートのテストでカメラが動いているかを確認してください。 無事にカメラが動いているようでしたら準備は完了になります。 f:id:k_tachiban:20190318170445p:plain

動かす

これで準備は一通り終わったので次はサンプルを動かしてみます。 Aristoフォルダ内にあるSampleシーンを起動します。

f:id:k_tachiban:20190318175015g:plain

動きました。

このシーンでできるアクションは

  • 右手を握って開くとレーザーが出せる
  • 両手を握ってエイムを箱に合わせて開くと掴む、手をもう一度握ると放す
  • 左手で箱を掴む
  • 右手で箱を押す
  • 右手の人差指を立てると出てくる光の玉を順になぞると箱を出せる

f:id:k_tachiban:20190318172131p:plain

といったアクションが可能になっています。

参考

developer.viveport.com

developer.viveport.com

おまけ

自分の環境ではデフォルトのまま実行するといくつかエラーが出てました。毎フレーム出てくるエラーがあるので対処法を書いておきます。 あと原因は不明なんですがハンドトラッキング周りの処理のCPU負荷がやばい。(i7-7700) f:id:k_tachiban:20190318174157p:plain

大量のMissingComponentException

両手を使ったAimを行うと前のUIパネルにレイがヒットしてしまい、アタッチされていないRigidbodyを参照しようとします。なのでヒエラルキーの中にあるInstructionといゲームオブジェクトのレイヤーをIgnore Raycastに変更してください。

【Unity】Mirage Soloでステンシルバッファが使えなかったときの対処

はじめに

MIrage Soloで開発を行った際、なぜかステンシルが効かずハマったのでメモ

原因としてはシンプルでDayDreamのDepthBufferの設定がデフォルトでステンシルバッファを含まないものになっていたからでした。

対処方法

PlayerSettings>XR Settings>DayDreamのDepthFormatを16-bit depthから24-bit depth | 8-bit stencilに変更すればOK

これでステンシルが使えるようになるはず。ちなみにこの問題はDayDream系全般ハマるはずなので注意。

f:id:k_tachiban:20181226161133p:plain

【Unity】選択したオブジェクトを強調表示するためのアウトラインシェーダーの作成

はじめに

ゲームを製作しているときに選んでいるオブジェクトをわかりやすくするために分かりやすいアウトラインを出して強調したいという場面は割とよくあるのですが、簡単に使えて綺麗にアウトラインを表示する方法がなかなか見つからなかったので作成して見ました。 単純にアウトライン部分のみを深度を無視して描画するシェーダーなので使い方は簡単でカメラにアウトライン表示用のスクリプト、オブジェクトに今回作成するシェーダをアタッチしたマテリアルを追加するだけでこんな感じに裏に回った部分もアウトラインだけ表示される形で強調表示ができるようになります。 f:id:k_tachiban:20180917151938p:plain

実装

今回のシェーダーの実装ですが方法としてはステンシルバッファにステンシルのみ書き込んで後からポストプロセスでアウトライン部分を描画しています。アウトラインの実装はよくある裏面を法線方向に拡大して描画する手法を使っています。

オブジェクト用のシェーダー

アウトラインをつけるオブジェクト用のシェーダーコードです。 今回は2Passを使って描画をしています。

1Pass目

1Pass目ではZWrite、ZTestをオフ、アルファを0にした状態でポリゴンの裏面を法線方向に伸ばした上で描画しています。この時あとでアウトラインとして描画するためのステンシルを書き込んでおきます。

2Pass目

2Pass目でもZWrite、ZTestをオフ、アルファを0にした状態に設定し、ステンシルの番号を変えて描画処理を行います。 これで法線方向が反転しているような特殊なモデル以外はアウトライン部分が1のStencilが書き込まれた状態になります。(深度テストを行わないため法線が反転していると表示がおかしくなりがち)

全文

ポストプロセス

ポストプロセス用スクリプト

続いては、アウトラインを実際に書き込むためにスクリプトを作成します。 本来ではOnRenderImage内でBlitを使ってポストプロセス処理を行うのですが、Blitを使ってポストプロセスを使ってステンシルを利用しようとするとかきこまれたステンシルを参照できないというバグ(?)がありました。 なので、この場合はCommandBuffer経由で描画してやる必要があるようです。 (今回はこちらのサイトのものをお借りしました)

[Unity]イメージエフェクトを特定のモデルだけにかける

using UnityEngine;
using UnityEngine.Rendering;

[ExecuteInEditMode]
[RequireComponent(typeof(Camera))]
public class ImageEffect : MonoBehaviour
{
    /// <summary>
    /// コマンドバッファ名
    /// </summary>
    private const string CommandBufferName = "StencilImageEffect";

    /// <summary>
    /// イメージエフェクトで使用するマテリアル
    /// </summary>
    [SerializeField] private Material material;

    /// <summary>
    /// イメージエフェクト用コマンドバッファ
    /// </summary>
    private CommandBuffer commandBuffer;


    private void OnEnable()
    {
        if (material == null) return;
        if (commandBuffer != null) return;

        var cam = GetComponent<Camera>();
        var cbs = cam.GetCommandBuffers(CameraEvent.BeforeImageEffects);
        foreach (var cb in cbs)
        {
            // 多重登録を回避するため、名前でチェック
            if (cb.name == CommandBufferName) return;
        }

        commandBuffer = new CommandBuffer();
        commandBuffer.name = CommandBufferName;

        // Blitでmaterialを適用してイメージエフェクトをかける
        commandBuffer.Blit(
            BuiltinRenderTextureType.CameraTarget,
            BuiltinRenderTextureType.CameraTarget,
            material);

        // カメラにコマンドバッファを登録
        cam.AddCommandBuffer(CameraEvent.BeforeImageEffects, commandBuffer);
    }

    private void OnDisable()
    {
        if (commandBuffer == null) return;

        var cam = GetComponent<Camera>();
        cam.RemoveCommandBuffer(CameraEvent.BeforeImageEffects, commandBuffer);
        commandBuffer = null;
    }
}

ポストプロセス用シェーダ

ポストプロセス用のシェーダーはシンプルにステンシルが書き込まれている場所に指定したカラーを描画するだけのシンプルなものになります。

StencilOutlinePostEffect.shader

この方法のメリット、デメリット

メリット

  • アウトラインの付け外しが楽
  • 半透明なものに対しても綺麗なアウトラインが描画できる
  • 既存のマテリアルに基本的に干渉しないため応用しやすい

デメリット

  • 通常の描画に加えて2Pass+ポストプロセス1Passなのでオーバヘッドが多い
  • アウトライン部分は深度チェックを行っていないため法線が反転しているものは相性が悪い
  • 鋭角なモデルに対してはポリゴンが浮いて見える

今回のスクリプト、シェーダはGitHubに上げておきました。 便利に使ってもらえると嬉しいです。 GitHub github.com

UnityPackerge Release StencilOutline.unitypackage · TachibanaKoki/StencilOutline · GitHub

参考サイト

ToonLitOutlineとステンシルによるOutlineの違い - AIプログラムとかUnityゲーム開発について

【Unity】【シェーダ】ステンシルバッファでアウトラインを描画する - LIGHT11

【Unity】ステンシルバッファをポストエフェクトで使う方法(と謎の挙動) - LIGHT11

[Unity]イメージエフェクトを特定のモデルだけにかける

Unity謎機能まとめ

なんとなくまとめたくなったのでUnityの存在しているがなんの為につかうのかよくわからない機能をまとめてみようと思います。

こう使うと便利だよ、とかこんな謎機能あるよだとか募集します。

Animator Parameter

使い方

  • Window>Animator Parameter

Animation Parametrsをみることが出来るwindow。普通のAnimatorWindowと同じ内容が表示される。 Animator見れば問題は無い。 用途は不明。

【Unity】Animator Parameterというウィンドウ - テラシュールブログ

Extract From Prefab

使い方

  • Project ビューでマテリアルが埋め込まれているモデルを右クリック>Extract From Prefab

モデルからマテリアルを抽出する機能。 マテリアル自体はインポート時に作成されるのでマテリアルを取得する機能として使うことはなさそう。データ容量が減らせたりする?

【Unity】Project ビューを右クリックした時に表示される「Extract From Prefab」とは - コガネブログ

まとめ

みんな気づいてないけど実は良い機能あったりしないかなーとか思ったり。 他にも謎機能があれば見つけ次第追加していきます。

【Lumberyard】Script Canvas入門

Lumberyard Advent Calendar 2017 の10日目です。 今回はLumberyard1.11.0から追加されたScriptCanvasの基本的な使い方と機能の紹介をします。

ScriptCanvasについて

ScriptCanvasはLumberryard1.11から追加された目玉機能の一つで新しいビジュアルスクリプト((UE4のBPのようなスクリプト環境環境になります。 CryEngineからの流れでFlow Graphというビジュアルスクリプト環境はあったのですが今後はScriptCanvasに置き換えられていく流れになると思います。 使用感としては機能縮小版BPといった感じでした。 今回はScriptCanvasの紹介とキーの入力でオブジェクトを動かすところまでやりたいと思います。  

ScriptCanvasのエディタ基本機能

ScriptCanvasエディターの基本構成の説明です。 といっても基本構成はシンプルで、左にノードを一覧できるNode Palleteとノードをつなげたり、管理するCanvasがあるだけです。 この画面はtools>Script Canvsから開けます。

image.png

ノードの追加は左のビューからドラッグアンドドロップか右クリックから検索で行います。

追加のウィンドウ

基本構成以外の追加可能なウィンドウもあります。 追加可能なウィンドウはNode InspectorNode OutLinerの二つです。

image.png

右上のNode Inspectorはノードが設定している定数が見れるようです(今後増える?) 右下のNode OutLinerではキャンバスに配置されているノードを一覧することができる機能です。

コメント

ScriptCanvasではコメント機能もありました。 コメントの追加方法はノードと同じようにcommentを探して追加するか、右クリック時に出てくるAdd Commentで追加できます。 使用感としてはキャンバスに置けるメモといった感じでサイズの変更もできずUE4のようにグループ化するために使うといったことはできなさそうです。 f:id:k_tachiban:20171203201702p:plain

ScriptCanvasの作成

Script Canvasを作成するには

  • Tools>Script CanvasまたはEntiityのScriptCanvasからScript Canvasのウィンドウを開く
  • File>New Scriptで作成
  • Ctrl+SまたはFile>Saveで名前を付けて保存 成功すると.scariptcanvasファイルが作成されます。

ユーザーからの入力を取得する

それではいよいよScriptCanvasの記述方法についてです。 まずはユーザーからの入力を受け取るスクリプトから書いていきます。

Input Bindingの作成

Script Canvasで入力を行うためにはまず初めに.inputBindingsを作成する必要があります。 手順は以下の通りです。

  • Tools>Input EditorでInput Binding Editorを開く
  • File>Create New Asset>Input to Event Bindings Assetで.inputbindingsを作成
  • Input Event Groupsの横の+ボタンを押す
  • 作成されたUnspecified EventにEvent Nameを入力
  • 作成したイベントのEvent Generatorsの+をクリック
  • Input Device TypeとInput Nameをイベントに合わせて入力
  • 以降Input Eventに必要な数のイベントを作成する
  • 入力が終わったらSave&Closeで保存

WASDでのキー入力をこんな感じで登録しました。

image.png

これでScriptCanvasでキーの入力がイベント名で取れるようになります。

入力を取得する

それでは作成したInput Bindingを使ってキーの入力を取得します。 Inputの取得にはInput Handlerノードを使用します。

 image.png

Event Nameのところに先ほど作成したInput Bindingのイベント名を入力してその入力時にログを出します。 ログを出すにはUtilities>Debug下のLogノードとVariables>CreateVariablesのStringからStringのVariableノードを作成することで実現できます。

image.png

出力を確認する

出力を確認するためにEntityに作成したScriptCanvasをAddします。 今回は入力をするためScrpit Canvas ComponentのほかにInput Componentの追加します。

image.png

そのあとCtrl+Gでゲームを実行。 登録したDを押したら無事Consoleにログが出ました。

image.png

オブジェクトの移動操作

ユーザーからの入力が取れたところでオブジェクトを移動させてみます。 オブジェクトの移動はMoveEntityノードで行えます。

image.png

キーの入力に合わせてこんな感じで作成。

キャプチャ.PNG

f:id:k_tachiban:20171203195120g:plain

これで動くようになりましたがノードが少々汚いのでもう少し改善します。 具体的にはInputBindingを編集して、S(Down)、A(Left)を押したときに-1が帰るように変更し、ノードを減らします。

f:id:k_tachiban:20171203200044p:plain

イベントをmove_X、move_Yの二つに減らしてS、Aの入力時のEvent value multiplierの値を-1.0にしています。 こうすることでノードがすっきりしました。

f:id:k_tachiban:20171203200404p:plain

まとめ

現時点でのScript Canvasを簡単に使ってみた感触ですが機能も少なく、シンプルなビジュアルスクリプティングといえると思いました。 UE4のブループリントのようにがっつり作るのには向いていませんがシンプルな機能の実装や、短期間での高速な開発には向いているようにかんじました。 シンプルなものにはScript Canvas、複雑なものにはC++luaを使うといった住み分けができるので状況に合わせて何を使ってスクリプティングするのか選択して使うのがよさそうです。

参考

Getting Started with Script Canvas - Lumberyard

Script Canvas Tutorials - Lumberyard

script canvas - Game Dev Forum