0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Unity】Custom Render Texture の Wrap Update Zones が Double Buffered に与える影響について

Posted at

操作によっては Unity がクラッシュします(解説).注意してください.

検証環境:Unity 2022.3.22f

背景

Custom Render Texture を使ってシミュレーションを行う際,自分自身のテクスチャ( _SelfTexture* )を参照するために,Double Buffered の機能を使うことが多いかと思います.また,シミュレーションによっては,1フレームの間に複数回の更新が必要となり,Update Zones を追加することがあります.その際に問題となるのが,Update Zones に追加した最後の Update Zone が描画結果に反映されないことです.
この対策として,Wrap Update Zones を有効にする対策が知られています.筆者も今まであまり考えずにこの対策を使っていましたが,これによって Draw Call が 9 倍に増えていたことが分かったため,詳細を調査することにしました.

Wrap Update Zones とはそもそも何なのか

今まで意味も分からずに使っていた Wrap Update Zones ですが,Unity Manual では下記のように説明があります.

部分的な更新ゾーンがテクスチャの境界線の周りを包むようにできます。

よくわかりません.

たぶんループの設定なので,実際に Update Zone をはみ出させてみると,確かに端でループするようになりました.(思ってたループとは違いましたが...)

image.png

ループするだけならシェーダーでやればいい気もしますが,Rotation と組み合わせるならシェーダー内で座標変換するよりこっちの方がシンプルです.

image.png

Custom Render Texture の設定と最終的な描画結果

「最後の Update Zone が描画されない」現象についてもう少し詳しく調べます.

Custom Render Texture を作成して Update Zone を3つ設定し,それぞれに別の Pass を設定して,どの Pass が描画されているかがわかるようにしておきます.その状態で,

  • Double Buffered
  • Wrap Update Zones
  • 各 Update Zone の Swap (Double Buffer)

の有効 / 無効を切り替えたときに,どの Pass が最終の描画結果に反映されているかを調べました.

使用したシェーダー
Shader "Test/WrapUpdateZone/Count"
{
    Properties {}

    SubShader
    {
        Lighting Off
        Blend One Zero
        
        CGINCLUDE
            #pragma vertex CustomRenderTextureVertexShader
            #pragma fragment frag
            #pragma target 3.0
            
            #include "UnityCustomRenderTexture.cginc"
        ENDCG

        Pass
        {
            Name "0"
            CGPROGRAM
            float4 frag(v2f_customrendertexture IN) : SV_Target
            {
                return float4(0, 0, 0, 1);
            }
            ENDCG
        }
        
        Pass
        {
            Name "1"
            CGPROGRAM
            float4 frag(v2f_customrendertexture IN) : SV_Target
            {
                return float4(0.1, 0, 0, 1);
            }
            ENDCG
        }
        
        Pass
        {
            Name "2"
            CGPROGRAM
            float4 frag(v2f_customrendertexture IN) : SV_Target
            {
                return float4(0.2, 0, 0, 1);
            }
            ENDCG
        }
    }
}
Custom Render Texture の設定

image.png

結果は下の表のようになりました.Result は最終的に描画結果に反映された Update Zone の番号です.

Update Mode Update Zones Result
Double
Buffered
Wrap
Update
Zones
Zone 0
Swap
Zone 1
Swap
Zone 2
Swap
2
2
2
2
0
1
0
1
1
1
2
0
1
2
1
2
2
2

Wrap Update Zones が有効な時は Swap が有効になっている最後の Update Zone が最終的な描画結果に反映されています.
Wrap Update Zones が無効な時は,Swap が有効になっている最後の Update Zone のひとつ手前の Update Zone が最終的な描画結果に反映されています.Update Zone 0 だけ Swap を有効にした場合は,すべての Update Zone が描画されているように見えましたが,描画の時刻を調べたところ,ひとつ前のフレームで描画された結果が見えている状態でした.おそらく,Double Buffer している相方の Buffer へ最後に描画された Update Zone 2 が見えているものと思われます.

また,Frame Debugger で確認すると,最終的な描画結果が"2"にならないケースにおいても,描画自体は Update Zone 2 まで行われていますが,Main Camera への描画時にその結果が反映されていないようです.

Update Zone の Swap (Double Buffer) について,Unity Manual では下記のように説明されています.

(ダブルバッファされたテクスチャのみ) このプロパティを有効にすると、Unity はこの更新ゾーンを処理する前にバッファを交換します。

また,Scripting API では下記のように説明されています.

If true, and if the texture is double buffered, a request is made to swap the buffers before the next update. Otherwise, the buffers will not be swapped.

Swap が有効になっている Update Zone をアップデートする直前に Buffer が Swap されるそうです.Swap が有効になっている Update Zone が存在しない場合は,Update Zone 0 の描画の直前に Swap を行うものと思われます.

そして,Main Camera で描画する際に渡される Buffer は,Swap して後続の Update Zone が描画された最新の Buffer ではなく,Swap して _SelfTexture* で参照できるように待機している方の Buffer のようです.また,この参照される _SelfTexture* は,現在のフレームで更新された分ではなく,前回のフレームで更新されたものが Main Camera で使われます.

ついでに,Swap という操作ですが,

パフォーマンスに関する注意: ダブルバッファリングは現在、各スワップでテクスチャのコピーを使用するため、実行される頻度とテクスチャの解像度に応じてパフォーマンスが低下する可能性があります。

だそうで,アドレスを Swap しているわけではなく,バッファ自体をコピーしているそうです.

ここまでを整理すると,すべての Update Zone で Swap を有効にしている場合,下図のような時系列になっていることがわかります.

image.png

すべての Update Zone について描画はしているものの,Swap の都合で古い方の Buffer が Main Camera での描画に使われてしまうため,Game Viewで見たときに,最後の Update Zone が描画されていないように見えているようです.

Wrap Update Zones を有効にすると解決する理由

Wrap Update Zones を有効にした状態で Frame Debugger を見てみると,ひとつの Update Zone あたり最大9回の Draw Call が発生していることがわかります.
CustomRenderTextureCenters などの値からメッシュの配置を読み取ると,Update Zone の場所に加えて,取り囲むように8つの四角形が配置され,これによって Wrap を実現していることがわかります.
描画の順は,中央,左,右,下,上,左下,左上,右下,右上,です.(ただし,V方向の上下はたぶんプラットフォーム依存です.)
Update Zone の位置やサイズによって,Custom Render Texture の描画範囲に影響しない四角形は省略されるようで,例えば,Center = (0.5, 0.5), Size = (0.5, 0.5) の場合は中央の四角形以外は省略されます.ちなみに,デフォルトの Center = (0.5, 0.5), Size = (1, 1) の場合は9個すべて描画されます.

image.png

この,Wrap Update Zones によって発生する9回の Draw Call についてですが,Swap (Double Buffer) を有効にしている場合は,毎回 Swap を行っているようです.(あまり正確な調査はできなかったので,厳密に"毎回"かはわからないです.)

つまり,Wrap Update Zones を有効にしている場合は,中央の Update Zone を描画した後,取り巻きを描画するために8回 Swap していたおかげで,見かけ上(見えている範囲は)最後の Update Zone も描画された状態になっていた,ということになります.

実際,上図の "9" が見えるように Update Zone を配置してみると,"9" に当たる部分が描画されない状態で表示されます.

image.png
※ "0" の Pass は全面を (0,0,0,1) で塗りつぶすパスです.

Wrap Update Zones の代替手段

Swap のためだけに Draw Call を9倍に増やすのはさすがに嫌なので,Wrap Update Zones に頼らない次善の策を考えます.
といっても単純で,何もしないパスを Update Zones の最後に追加します.

image.png

Shader "Test/WrapUpdateZone/Template"
{
    Properties {}

    SubShader
    {
        Lighting Off
        Blend One Zero

        CGINCLUDE
            #pragma vertex CustomRenderTextureVertexShader
            #pragma fragment frag
            #pragma target 3.0

            #include "UnityCustomRenderTexture.cginc"
        ENDCG

        Pass
        {
            Name "Main 1"
            CGPROGRAM
            float4 frag(v2f_customrendertexture IN) : SV_Target
            {
                float4 output = tex2Dlod(_SelfTexture2D, float4(IN.globalTexcoord.xy, 0, 0));

                /*
                
                メイン1の処理
                
                */

                return output;
            }
            ENDCG
        }
        
        Pass
        {
            Name "Main 2"
            CGPROGRAM
            float4 frag(v2f_customrendertexture IN) : SV_Target
            {
                float4 output = tex2Dlod(_SelfTexture2D, float4(IN.globalTexcoord.xy, 0, 0));

                /*
                
                メイン2の処理
                
                */

                return output;
            }
            ENDCG
        }
        
        Pass
        {
            Name "Copy"
            CGPROGRAM
            float4 frag(v2f_customrendertexture IN) : SV_Target
            {
                return tex2Dlod(_SelfTexture2D, float4(IN.globalTexcoord.xy, 0, 0));
            }
            ENDCG
        }
    }
}

意味のない Update Zone を最後につけることで Swap が発生し,直前までの処理がすべて済んだ状態の Buffer が Main Camera での描画で使われるようになります.
Draw Call が1回増えてしまいますが,9倍になるよりはましです.

雑記

Custom Render Texture の仕様をいろいろと勘違いしていたので,いろいろと驚愕しました.

Custom Render Texture なので,サイズが小さければ Draw Call が9倍になろうがたいして影響はないですが,サイズが大きかったり,3Dテクスチャを扱ったりする場合はそれなりに影響があります.筆者は 128x128x128 の3Dテクスチャに対して Update Zones を6個セットしたうえで Wrap Update Zones したところ,Draw Call が 128x6x9 = 13824 回になってだいばくはつしました.気を付けましょう.

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?