UNITYのパフォーマンス向上には、DrawCall削減という話はよく聞くと思うし、いろいろな手法が書いてあるが
実際に手で動かさなければ納得できないので、実際に調査です
手抜きで ビルトインのStandardShaderで行います
#DrawCall、SetpassCall、TotalBatchesって何?
##SetPass
マテリアルの変更のためにGPUへグラフィックコンテキストを送る回数です
グラフィックコンテキストの変更はとてもコストが大きいため
この数を減らすことが最重要
基本的にはマテリアル数を減らす事で下げます
##DrawCall
これはGPUに対して頂点の描画命令を出した回数です
同じマテリアルでも異なるオブジェクトの場合はDrawCallが呼ばれる事もあるので
DrawCall≧SetPass になるでしょう
##TotalBatches
なんだろうね
#素のScene
これをプロファイリングしましょう
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消して本当に何もない世界
カメラの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個置きます
SetPass Calls: 5 Draw Calls: 5 Total Batches: 5
おや?5個?
影の設定を消します
SetPass Calls: 2 Draw Calls: 2 Total Batches: 2
2?
よくわかりませんが、2個(Standardはマルチパスなのかな?)
#同じマテリアルのCube4個
当然全部影をOFFにしておきます
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個を色違いのマテリアルを作り設定
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);
}
}
##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のインスタンシングである
それをUNITYは上記のマクロを使い非常に巧妙にマルチプラットフォームに対応させている
コード見れば見るほど素晴らしさがわかる!
コードの説明を行おうとおもったが、凹みさんが丁寧に解説してるので紹介にとどめる
とりあえず上記のコードで、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
}
最初の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 等は本当に不要なんだろうか?
とりあえず、ShadowCasterパスも Instancing有効になった