Edited at

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

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有効になった