DrawCall Batching

DrawCall Batching

Unityは描画効率のために、複数のRendererの描画を一つに束ねることがあります。 これをバッチングといい、 どのRendererの描画をまとめるかを事前に指定する方式をstatic batching Materialの状況をみてUnityがよしなに描画を束ねるかどうかを自動で判断するのをdynamic batchingといいます。 バッチングのON/OFFはPlayer Settingsで指定でき、デフォルトはONです。

f:id:enrike3:20180228223429p:plain

dyanmic batchingの条件

dynamic batchingの効く条件は、以下2点を満たすことです

  • 同一のマテリアルを使用していること
  • Material Property Blockの値が一致していること

あ、もちろん同一のCameraで映してくださいね。

実験

どシンプルなSprite風シェーダで実験です。 このシェーダは画像と乗算カラーのみを指定できます。

Shader "Hoge/OreOreSprite"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Color("Tint", Color) = (1,1,1,1)
    }
    SubShader
    {
        Cull Off ZWrite Off ZTest Always
        Blend One OneMinusSrcAlpha

        Tags
        {
            "Queue" = "Transparent"
            "PreviewType" = "Plane"
        }

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

            float4 _Color;

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

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

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                o.color = v.color * _Color;
                return o;
            }
            
            sampler2D _MainTex;

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

作ったShaderにunity-chanの画像でもぶっ挿して乗算カラーも指定しておきます。 f:id:enrike3:20180228225127p:plain

このMaterialを使用して適当にQuadメッシュを二つ描画してみます。 FrameDebuggerでみると、ちゃんとバッチが効いて1Drawで2つの描画が束ねられています。 f:id:enrike3:20180228225400p:plain

ここで、2体目のUnity-ChanのMaterialを差し替えてみましょう。 元のMaterialをコピーして、同じシェーダ、同じテクスチャ、同じ乗算カラーのMaterialを作成します。 中身は一緒だけどガワが違うとでもいいましょうか。

f:id:enrike3:20180228230008p:plain

描画は分かれてしまいました。 中身がどんなに等しくても、Materialが別だとバッチングは効かないことがわかります。

Material Property Block

Unityには、同一のMaterialを使用しつつも、Renderer単位でパラメータを変える仕組みとして Material Property Blockが存在します。 すんごい雑にStartでMaterialPropertyBlockをつっこむコンポーネントを書いてみます。

using UnityEngine;

[RequireComponent(typeof(MeshRenderer))]
public class OreOreSprite : MonoBehaviour
{
    [SerializeField]
    private Texture _texture = null;

    [SerializeField]
    private Color _color;

    private MeshRenderer _renderer = null;
    public MeshRenderer Renderer
    {
        get { return _renderer != null ? _renderer : (_renderer = GetComponent<MeshRenderer>()); }
    }

    private MaterialPropertyBlock _block = null;

    void Start ()
    {
        _block = new MaterialPropertyBlock();
        _block.SetTexture("_MainTex", _texture);
        _block.SetColor("_Color", _color);
        this.Renderer.SetPropertyBlock(_block);
    }
}

インスペクタでテクスチャとカラーを指定可能 f:id:enrike3:20180228231945p:plain

これで、同一のMaterialを使いながらもパラメータを分けることができるようになりました。 同一のマテリアルを使用していてもパラメータが異なることによりバッチングは効いていません。 f:id:enrike3:20180228232244p:plain

この状態で、Inspectorから同じ画像、同じ色を指定してみましょう。(Materialは同一のものを使用するとします)

f:id:enrike3:20180228233246p:plain

無事描画が一つにまとまりました。

まとめ

dynamic batchingの効く条件は、以下2点を満たすことです

  • 同一のマテリアルを使用していること
  • Material Property Blockの値が一致していること

今回はMeshRendererを使用しましたが、SpriteRendererやCanvasあたりも基本的には同じです。 (Canvasはdyanmic batchingというよりはCanvas独自バッチングになるのでむしろdynamic batchingからははずれますが、Materialが分かれたりパラメータがばらけるとバッチされないという点では一緒) MaterialPropertyBlockによって、同一Materialを使いまわしながらも、パラメータを変更できるようにし、 MaterialPropertyBlockのパラメータが(たまたまor狙って)一致した場合は描画が束ねられる、という形をとっています。 なおuGUIの場合はCanvasRendererがMaterialPropertyBlockに近い役割を担っています。

テクスチャなんて一致するのかよ、という点については、 複数の画像を一つにパッキングしておけば束ねられます。

f:id:enrike3:20180228234909p:plain

こうやってパックされたテクスチャを使って、Spriteによる範囲指定で抜けば、画像内のキャラを何体書いても同一Materialを使う限り1DrawCallで済みます。 キャラクターは画像が大きいことが多いので例として良くないですが、 小さい画像の多いUIではパッキングは効果があります。

余談:PerRendererDataについて

MaterialPropertyBlockやCanvasRendererによって、後から画像を挿す場合Materialに挿してある画像は無駄です。 消し忘れると、MaterialをLoadしただけで使う予定のない画像までついてくる。許せん、となります。 そのような想定の画像はshader側で [PerRendererData] 指定をしておくと良いです。

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

これを書くと、Materialのインスペクタから _MainTexを指す箇所が消えるので事故防止になります。

© UTJ/UCL