Help us understand the problem. What is going on with this article?

UNITY DrawCall調査 GPU Instancing ~ UNITY2018.3.6f1 ~

More than 1 year has passed since last update.

UNITYのパフォーマンス向上には、DrawCall削減という話はよく聞くと思うし、いろいろな手法が書いてあるが
実際に手で動かさなければ納得できないので、実際に調査です
手抜きで ビルトインのStandardShaderで行います

DrawCall、SetpassCall、TotalBatchesって何?

SetPass

マテリアルの変更のためにGPUへグラフィックコンテキストを送る回数です
グラフィックコンテキストの変更はとてもコストが大きいため
この数を減らすことが最重要
基本的にはマテリアル数を減らす事で下げます

DrawCall

これはGPUに対して頂点の描画命令を出した回数です
同じマテリアルでも異なるオブジェクトの場合はDrawCallが呼ばれる事もあるので
DrawCall≧SetPass になるでしょう

TotalBatches

なんだろうね

素のScene

UNITYを新規で作り実行すると下記の画面になります
0.PNG

これをプロファイリングしましょう
Windows->Analytics->Profiler でプロファイル表示

Renderingをみます

SetPass Calls: 2    Draw Calls: 2       Total Batches: 2    Tris: 1.0k  Verts: 4.1k
(Dynamic Batching)  Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Static Batching)       Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Instancing)        Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
Used Textures: 2 - 1.0 KB
RenderTextures: 9 - 23.4 MB

何もしてないのに SetPass、DrawCallが2 ??
SkyBoxですね!

ってことで

SkyBox消して本当に何もない世界

SnapCrab_NoName_2019-2-27_18-44-11_No-00.png

カメラのSkyBox削除したりRendering設定のSkybox消したり
で、本当の無ができました

SetPass Calls: 0    Draw Calls: 0       Total Batches: 0    Tris: 0     Verts: 0
(Dynamic Batching)  Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Static Batching)       Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Instancing)        Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
Used Textures: 0 - 0 B
RenderTextures: 7 - 16.3 MB

本当の無ができました

Cube1個表示

画面にCube1個置きます

SnapCrab_NoName_2019-2-27_19-15-0_No-00.png

SetPass Calls: 5    Draw Calls: 5       Total Batches: 5 

おや?5個?
影の設定を消します

SetPass Calls: 2    Draw Calls: 2       Total Batches: 2 

2?
よくわかりませんが、2個(Standardはマルチパスなのかな?)

同じマテリアルのCube4個

当然全部影をOFFにしておきます

SnapCrab_NoName_2019-2-27_19-27-17_No-00.png

SetPass Calls: 2    Draw Calls: 2       Total Batches: 2 

同じマテリアルはUNITYでは可能な限りまとめて送ってくれるので(CPUバッチング)
2のままですね

スフィア1個

SetPass Calls: 2    Draw Calls: 2       Total Batches: 2 

Cubeと変わらず2回

スフィア4個

SetPass Calls: 2    Draw Calls: 8       Total Batches: 8 

スフィアは頂点数が多いので、まとめてくれません
ので DrawCallは8です(2x4)
が、マテリアルは1個なので SetPassは2ですんでます

色違いのマテリアルをエディタ上で作成しアタッチ

4個のうち2個を色違いのマテリアルを作り設定

4SnapCrab_NoName_2019-2-27_19-39-17_No-00.png

SetPass Calls: 4    Draw Calls: 4       Total Batches:  

4になった

同じマテリアルを使いスクリプトでシェーダーカラーを変更

4つのCubeに下記のスクリプトを設定し、そのうち2個をRedにする

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

public class materialize : MonoBehaviour
{
    public Color color = Color.white;
    private Material mat = null;

    void Start()
    {
        Renderer renderer = GetComponent<Renderer>();
        mat = renderer.material;
    }

    void Update()
    {
        mat.SetColor("_Color", this.color);
    }
}
SetPass Calls: 8    Draw Calls: 8       Total Batches: 8 

超増えた!!
どうやら全部別々のマテリアルだと認識するようです
GPU Instancingしてもかわらない

MaterialPropertyBlockを使う

上記のスクリプトを変更

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

public class materialize : MonoBehaviour
{
    public Color color = Color.white;
    private Material mat = null;
    private MaterialPropertyBlock materialPropertyBlock = null;
    private Renderer renderer = null;

    void Start()
    {
        materialPropertyBlock = new MaterialPropertyBlock();
        renderer = GetComponent<Renderer>();
    }

    void Update()
    {
        materialPropertyBlock.SetColor(id, color);
        renderer.SetPropertyBlock(materialPropertyBlock);
    }
}
SetPass Calls: 4    Draw Calls: 4       Total Batches: 4 

4になった(別マテリアルと同じ)

GPU Instancingを4つのCubeに設定

SetPass Calls: 3    Draw Calls: 3       Total Batches: 3    Tris: 0     Verts: 0
(Dynamic Batching)  Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Static Batching)       Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Instancing)        Batched Draw Calls: 8   Batches: 3  Tris: 0     Verts: 0
Used Textures: 3 - 0 B
RenderTextures: 8 - 17.3 MB

GPU Instancingも効き 3になった
MaterialPropertyBlockでは、マテリアルを複製せず、同じマテリアルとして設定だけ変更してくれるようです

いっぱい表示してみよう

Updateで毎フレーム CubeをInstancingし
ランダムで色をつけてランダムな方向に飛ばすパーティクルもどきを作り
負荷を比較する

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

public class Shoot : MonoBehaviour
{
    public GameObject go;

    // Update is called once per frame
    void Update()
    {
        var obj = Instantiate(go);
        var rigidbody = obj.GetComponent<Rigidbody>();
        var script = obj.GetComponent<materialize>();
        var col = new Color( Random.Range(0.0f, 1.0f), Random.Range(0.0f, 1.0f), Random.Range(0.0f, 1.0f));
        script.color = col;

        rigidbody.AddForce( Random.Range(-20.0f, 20.0f), Random.Range(3.0f, 50.0f), Random.Range(-20.0f, 20.0f), ForceMode.Impulse);
    }
}

SnapCrab_NoName_2019-2-28_3-0-39_No-00.png

MaterialPropertyBlock不使用&GPU Instancing OFF

SetPass Calls: 370      Draw Calls: 370         Total Batches: 370  Tris: 4.1k  Verts: 8.2k
(Dynamic Batching)  Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Static Batching)       Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Instancing)        Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0 

400付近。Batchなし

MaterialPropertyBlock不使用&GPU Instancing ON

SetPass Calls: 358      Draw Calls: 358         Total Batches: 358  Tris: 4.1k  Verts: 8.2k
(Dynamic Batching)  Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Static Batching)       Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Instancing)        Batched Draw Calls: 358     Batches: 358    Tris: 4.1k  Verts: 8.2k

Batchにはなっているが、Drawcall減らず

MaterialPropertyBlock使用&GPU Instancing OFF

SetPass Calls: 348      Draw Calls: 348         Total Batches: 348  Tris: 4.1k  Verts: 8.2k
(Dynamic Batching)  Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Static Batching)       Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Instancing)        Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0

あまり変わらず

MaterialPropertyBlock使用&GPU Instancing ON

SetPass Calls: 197      Draw Calls: 197         Total Batches: 197  Tris: 4.1k  Verts: 9.2k
(Dynamic Batching)  Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Static Batching)       Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Instancing)        Batched Draw Calls: 392     Batches: 197    Tris: 4.1k  Verts: 9.2k

GPU Instancingが効いて、全てのDrawが約400に対して、SetPass、DrawCallは200と半分に!

・・・予想ではSetPassが20ぐらいまで下がるはずだったんだけど思ったほどさがらなかった
どこかにミスあるのか?

おまけ、GPU Instancing ONで同じ色だけ描画

SetPass Calls: 4    Draw Calls: 4       Total Batches: 4    Tris: 4.1k  Verts: 9.2k
(Dynamic Batching)  Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Static Batching)       Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Instancing)        Batched Draw Calls: 392     Batches: 4  Tris: 4.1k  Verts: 9.2k

400のDrawに対して SetPass4。完全に GPU Instancing効いてる!

まとめ

UNITYは同じマテリアル&設定のモデルは可能な限りまとめて表示してくれる(CPU バッチング)
スフィアのような頂点数が多いものはバッチング出来ない(がマテリアルが同じであればSetPassは増えない)
異なるマテリアル、パラメータの異なるマテリアルはバッチング出来ない
MaterialPropertyBlockを使えばパラメータの異なるマテリアルでも新たにマテリアルが作成されないようだ
GPU Instancingに対応したシェーダを使う事でSetPass CallやDrawCallを削減できる

で、納得いった??
いや、絶対納得しないよね!

という事でシェーダー作る事にした

シェーダー作成

Instancingというシェーダーを作った

Shader "Instancing"
{
    Properties
    {
        _Color("Color", Color) = (1, 1, 1, 1)
    }
        SubShader
    {
        Tags { "RenderType" = "Opaque" "DisableBatching" = "True"}

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #pragma multi_compile_instancing

            #include "UnityCG.cginc"
            #include "UnityStandardParticleInstancing.cginc"

        struct appdata
        {
            float4 vertex : POSITION;
            float4 color : COLOR;
            UNITY_VERTEX_INPUT_INSTANCE_ID
        };

        struct v2f
        {
            float4 color : TEXCOORD1;
            float4 vertex : SV_POSITION;
        };


        UNITY_INSTANCING_BUFFER_START(Props)
            UNITY_DEFINE_INSTANCED_PROP(float4, _Color)
        UNITY_INSTANCING_BUFFER_END(Props)

        v2f vert(appdata v)
        {
            UNITY_SETUP_INSTANCE_ID(v);

            v2f o;
            o.vertex = UnityObjectToClipPos(v.vertex);
            o.color = UNITY_ACCESS_INSTANCED_PROP(Props, _Color);

            return o;
        }

        fixed4 frag(v2f i) : SV_Target
        {
            return i.color;
        }

            ENDCG
        }
    }
}

_Colorというプロパティに対してInstancingを行う何の変哲もないシェーダーだ

pragma multi_compile_instancing
UNITY_VERTEX_INPUT_INSTANCE_ID
UNITY_INSTANCING_BUFFER_START(Props)
UNITY_DEFINE_INSTANCED_PROP(float4, Color)
UNITY_INSTANCING_BUFFER_END(Props)
UNITY_SETUP
INSTANCE_ID(v);
UNITY_ACCESS_INSTANCED_PROP(Props, _Color);

のあたりがキーとなる

そもそもGPU Instancingとは、GLSLやHLSLのインスタンシングである

http://maverickproj.web.fc2.com/d3d11_18.html

それをUNITYは上記のマクロを使い非常に巧妙にマルチプラットフォームに対応させている
コード見れば見るほど素晴らしさがわかる!

コードの説明を行おうとおもったが、凹みさんが丁寧に解説してるので紹介にとどめる

http://tips.hecomi.com/entry/2018/09/24/232125

とりあえず上記のコードで、GPU Instancingが可能になったので測定する

まず、GPU Instancingチェックなし

SetPass Calls: 191      Draw Calls: 191         Total Batches: 191  Tris: 2.0k  Verts: 4.1k
(Dynamic Batching)  Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Static Batching)       Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Instancing)        Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0

DrawCall 約200。今までの半分なのはやはり、Standardのシェーダーは2パス走っていたのだろう
(ライト1個、影なしだけど、何のパスだろうか?? ForwardAddかShadowCasterが怪しい)

そして、GPU Instancingあり

SetPass Calls: 1    Draw Calls: 1       Total Batches: 1    Tris: 2.0k  Verts: 4.1k
(Dynamic Batching)  Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Static Batching)       Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Instancing)        Batched Draw Calls: 207     Batches: 1  Tris: 2.0k  Verts: 4.1k

キマシタワーーーー!!

200 Drawですが、1マテリアルなのでSetPass Callは1回で、全部GPU Instancing化された!

念のためスフィアも

GPU Instancing無し

SetPass Calls: 207      Draw Calls: 207         Total Batches: 207  Tris: 158.7k    Verts: 106.5k
(Dynamic Batching)  Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Static Batching)       Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Instancing)        Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0

GPU Instancing無し

SetPass Calls: 1    Draw Calls: 1       Total Batches: 1    Tris: 157.7k    Verts: 105.5k
(Dynamic Batching)  Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Static Batching)       Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Instancing)        Batched Draw Calls: 206     Batches: 1  Tris: 157.7k    Verts: 105.5k

CPUバッチングでは、頂点数(正確には頂点要素数)が多いとバッチング出来ないので
スフィアは個別にDrawCallが走っていたが
GPU Instancingでは やはり DrawCallは1回で終わる!!
これが、CPUバッチングは多ポリゴンメッシュに弱いが、GPU Instancingはハイポリゴンに強いというやつだ

そうそう、これを期待してた!!
Standardシェーダーが、DrawCallが半分にしかならなかった理由は
おそらく、シェーダーが2パスあり、1つのパスはGPU Instancingが効いて激減したが
もう1つのパスがGPU Instancingに対応しておらず、半分という結果だったと思われる

やっと寝れる

追記

ShadowCaster

最初に謎のパスはやはりShadowCasterパスだった。
ShadowCasterパスを書かなければForwardBassのみの1パスだった

先ほどのシェーダにShadowCasterパスを追加

        Pass
        {
            Name "SHADOW_CASTER"
            Tags{ "LightMode" = "ShadowCaster" }

            Lighting Off
            ColorMask RGB
            Fog{ Mode Off }

            Offset 1, 1
            Cull Off

            ZWrite On ZTest LEqual

            CGPROGRAM
            #pragma target 3.0
            #pragma vertex vertShadowCaster
            #pragma fragment fragShadowCaster

            #include "UnityCG.cginc"

            struct v2f {
                V2F_SHADOW_CASTER;
            };

            v2f vertShadowCaster(appdata_base v) {
                v2f o = (v2f)0;
                TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
                return o;
            }

            float4 fragShadowCaster(v2f i) : SV_TARGET{
                SHADOW_CASTER_FRAGMENT(i)
            }
            ENDCG
        }

すると結果は
SnapCrab_NoName_2019-3-5_2-30-5_No-00.png

最初のStandardシェーダのように 177パス増えた
このうちわけは、177のGPU InstancingされたForwardBaseパスと、 Instancingされない177のShadowCasterパスが動いている

ShadowCasterもGPU Instancingしたいよね?

GPU Instancing

            #pragma multi_compile_instancing

を追加するだけで、ShadowCasterも GPU Instancing出来る
(UNITY_VERTEX_INPUT_INSTANCE_ID 等は本当に不要なんだろうか?

SnapCrab_NoName_2019-3-5_2-37-33_No-00.png

とりあえず、ShadowCasterパスも Instancing有効になった

YukiMiyatake
C++が喋れる ゲームプログラマ インフラ、サーバ、UNITY、ゲームエンジンが最近多いな・・ MONA: MPpuEnmqDYBCxSZyG5cBDt6UWtXczmRmkn BTC: 13JpgsF3n6K2WhjEeUuUUqS7V71gWdFx56 BCH: 18q6rfi9ynyTgynrB8tJ2eSDLPQM32RZk5
http://murasame-labo.hatenablog.com/
murasame
ゲーム、エンタメ、サーバインフラ等 少人数で技術力の高い仕事をする会社
http://murasame-lab.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away