始めに
前回ComputeShaderについて勉強しました。
次に学習したComputeShaderを使って何かしたいなってことで大量にオブジェクトを生成したいってなったので、GPUインスタンシング、そしてそれを勉強する過程で学んだことをまとめていこうと思います。
GPUインスタンシングについて
Unity公式マニュアルより、
GPU インスタンシングを使うと、少ない ドローコール で、同じメッシュの複数のコピーをいっぺんに描画 (またはレンダリング) できます。 これは、建物、樹木、草などのオブジェクトを描画したり、シーンに繰り返し登場するものを描画する場合に便利です。
GPU インスタンシングは、各ドローコールで同じメッシュをレンダリングするだけですが、各インスタンスは変化を加えるため、パラメーター (例えば、色やスケール) を変えて、繰り返しの回数を減らすことができます。
GPU インスタンシングを使うと、シーンごとに使用されるドローコールの数を減らすことができます。 これにより、プロジェクトのレンダリングパフォーマンスが大幅に向上します。
ということです。結論としてはレンダリングのパフォーマンスがよくなるらしいです。
しかし、よく出てくる「ドローコール」って何だろう?ってことで調べてみる。
SetPass Call、DrawCallとは
ドローコールについて調べていると同時に「SetPass Call」に出会いました。この2つについて以下のサイトを参考に調べてみました。
【Unity】Draw CallやSetPass Callって結局なんなのか? - LIGHT11
[Unity]最適化の要!「DrawCall」とは? | notargs.com
UNITY DrawCall調査 GPU Instancing ~ UNITY2018.3.6f1 ~ - Qiita
SetPass CallはCPUからGPUへマテリアルの設定値を伝える処理です。また、一回前にSetPassした時とマテリアルが同じであればSetPass Callはスキップされるそうです。
DrawCallはCPUからGPUへ描画命令を送る処理です。ここで先程のSetPassで設定されたマテリアルの設定値を用いてポリゴンを描画します。同一のマテリアルから異なるオブジェクトを描画する際にもDrawCallが発生するみたいです。
順番としてSetPass Call -> DrawCallが行われます。
確認してみる
UnityのWindow->Analysis->Profilerを開き、Renderingを選択することでみることでSetPass Call、Draw Callが行われた回数を確認することが出来ます。
様々な描画を試してみる
以下のサイトを参考にしながら様々なインスタンシングをしてSetpass Calls, Draw Callsを調べてみます。
Instantiate
普段オブジェクトを生成する際に使用するメソッドです。このInstantiateメソッドを使用して500個のオブジェクトを生成してみます。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class MakeInstansiate : MonoBehaviour { [SerializeField] private int m_width; [SerializeField] private int m_height; [SerializeField] private float m_space; [SerializeField] private GameObject m_cubePrefab; // Start is called before the first frame update void Start() { float cubeSize = m_cubePrefab.transform.localScale.x; for (int i = 0; i < m_width; ++i) { for (int j = 0; j < m_height; ++j) { Vector3 pos = new Vector3( (cubeSize + m_space) * j, (cubeSize + m_space) * i, 0); var newCube = Instantiate(m_cubePrefab, pos, Quaternion.identity); } } } }
結果が次のようになりました。
Draw Callsはかなり多く呼ばれていますが、SetPass Callsが5回しか呼ばれていません。同じマテリアルを使用しているためSetPass Callsがスキップされているようです。
では次に、一つ一つ別の色をマテリアルに設定してみました。
......... var mat = newCube.GetComponent<Renderer>().material; mat.SetColor("_Color", new Color( Random.Range(0.0f, 1.0f), Random.Range(0.0f, 1.0f), Random.Range(0.0f, 1.0f))); .........
結果としてSetPass Callsが増えました。
Graphics.DrawMesh
このメソッドではGameObjectを作成せずにメッシュをレンダリングできるようです。私も初めて触れる内容なので試してみようと思います。 このメソッドについてはUnity公式マニュアルを参照してください。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class MakeDrawMesh : MonoBehaviour { [SerializeField] private int m_width; [SerializeField] private int m_height; [SerializeField] private float m_space; [SerializeField] private Mesh m_mesh; [SerializeField] private Material m_material; private MaterialPropertyBlock m_materialPropertyBlock; // Start is called before the first frame update void Start() { m_materialPropertyBlock = new MaterialPropertyBlock(); } // Update is called once per frame void Update() { for (int i = 0; i < m_width; ++i) { for (int j = 0; j < m_height; ++j) { Vector3 pos = new Vector3( m_space * j, m_space * i, 0); m_materialPropertyBlock.Clear(); m_materialPropertyBlock.SetColor("_Color", new Color( ((float)i / m_width), ((float)j / m_height), 0)); Graphics.DrawMesh(m_mesh, pos, Quaternion.identity, m_material, 0, null, 0, m_materialPropertyBlock, false, false); } } } }
ここで出てくるMaterialPropertyBlockについて少し解説します。
オブジェクトごとに色を変更した場合、新しくマテリアルを作成してそれぞれにそのマテリアルを設定します。それによってオブジェクトの数だけマテリアルが作成されることとなります。
それをMaterialPropertyBlockを使用すれば新しいMaterialを作成することなく、一部のプロパティの値を変更することが出来ます。
結果としてSetPass Callsは変わりませんでしたが、Draw Callsは 1/8 とかなり減りました。
ここで試しに、MaterialのEnable GPU Instancingをチェックして実行してみました。描画結果は変わらないです。
結果はSetPass CallsとDraw Callsは半分まで減りました。
Graphics.DrawMeshInstanced
GPUインスタンシングを利用してメッシュをまとめてレンダリングするメソッドです。
このメソッドで描画するためにはマテリアルのEnable GPU Instancingをチェックしなければエラーが起こります。この方法ではCubeをひとまとめに描画しているため一つ一つの色を変えることが出来ませんでした。
こちらも初めて触るのでUnity公式マニュアルを参考にしています。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class MakeDrawMeshInstanced : MonoBehaviour { [SerializeField] private int m_width; [SerializeField] private int m_height; [SerializeField] private float m_space; [SerializeField] private Mesh m_mesh; [SerializeField] private Material m_material; private MaterialPropertyBlock m_materialPropertyBlock; private List<Matrix4x4> m_cubeTrs = new List<Matrix4x4>(); // Start is called before the first frame update void Start() { m_materialPropertyBlock = new MaterialPropertyBlock(); for (int i = 0; i < m_width; ++i) { for (int j = 0; j < m_height; ++j) { Vector3 pos = new Vector3( m_space * j, m_space * i, 0); Matrix4x4 matrix = new Matrix4x4(); matrix.SetTRS(pos, Quaternion.identity, Vector3.one); m_cubeTrs.Add(matrix); } } } // Update is called once per frame void Update() { Graphics.DrawMeshInstanced(m_mesh, 0, m_material, m_cubeTrs.ToArray()); } }
同じマテリアルを描画しているため、かなりSetPass Callsが少なくなっています。
Graphics.DrawMeshInstancedIndirect
このメソッドではComputeBufferを使用して描画しているようです。 一番調べてて情報が少なく様々なサイトを参考にしています。 Unity公式マニュアルも参考にしています。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class MakeDrawMeshInstancedIndirect : MonoBehaviour { [SerializeField] private int m_width; [SerializeField] private int m_height; [SerializeField] private float m_space; [SerializeField] private Mesh m_mesh; [SerializeField] private Material m_material; private ComputeBuffer m_argsBuffer; private Bounds m_bounds; private uint[] m_args = new uint[5]; // Start is called before the first frame update void Start() { m_bounds = new Bounds(Vector3.zero, new Vector3(m_width * m_space, m_height * m_space, 2)); m_argsBuffer = new ComputeBuffer(1, m_args.Length * sizeof(uint), ComputeBufferType.IndirectArguments); m_args[0] = m_mesh.GetIndexCount(0); m_args[1] = (uint)(m_width * m_height); m_args[2] = m_mesh.GetIndexStart(0); m_args[3] = m_mesh.GetBaseVertex(0); m_args[4] = 0; m_argsBuffer.SetData(m_args); m_material.SetInt("_Width", m_width); m_material.SetInt("_Height", m_height); m_material.SetFloat("_Spacing", m_space); } // Update is called once per frame void Update() { Graphics.DrawMeshInstancedIndirect(m_mesh, 0, m_material, m_bounds, m_argsBuffer); } private void OnDestroy() { m_argsBuffer?.Release(); } }
ここでこのメソッドについて簡単に解説します。
Graphics.DrawMeshInstancedIndirect(m_mesh, 0, m_material, m_bounds, m_argsBuffer);
このメソッドの引数はメッシュ、サブメッシュのインデックス、マテリアル、描画範囲、描画データを渡しています。それ以降の引数についてはオプション引数となっています。(参考:Unity公式マニュアル)
ここで、最後の描画データについてさらに解説します。このデータはComputeBuffer型であり、5つのuint型の データが格納されています。
private ComputeBuffer m_argsBuffer; private uint[] m_args = new uint[5]; // Start is called before the first frame update void Start() { m_argsBuffer = new ComputeBuffer(1, m_args.Length * sizeof(uint), ComputeBufferType.IndirectArguments); m_args[0] = m_mesh.GetIndexCount(0); m_args[1] = (uint)(m_width * m_height); m_args[2] = m_mesh.GetIndexStart(0); m_args[3] = m_mesh.GetBaseVertex(0); m_args[4] = 0; m_argsBuffer.SetData(m_args);
まず、このComputeBufferを作成する際は第三引数にComputeBufferType.IndirectArgumentを指定します。そうしないといけない決まりみたいです。
そしてこの中にメッシュの数、描画する個数、メッシュバッファの開始インデックス、頂点のインデックス、生成し始めるインスタンスのインデックスを渡しています(説明が曖昧ですが)。 いくつかの例ではメッシュの数と描画する個数しか指定せず、それ以外は0にしているものもありました。
あとはこのデータをメソッドに渡せば描画できます。しかし、座標や回転を指定するところがありません。ので、シェーダーで書きます。
Shader "Custom/GPUInstancingColor" { Properties { _MainTex ("Albedo (RGB)", 2D) = "white" {} _Glossiness ("Smoothness", Range(0,1)) = 0.5 _Metallic ("Metallic", Range(0,1)) = 0.0 _Width("Width", Range(10, 1000)) = 120 _Heihgt("Height", Range(10, 1000)) = 80 _Spacing("Spacing", Range(1, 10)) = 2 } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM // Physically based Standard lighting model, and enable shadows on all light types #pragma surface surf Standard fullforwardshadows #pragma multi_compile_instancing #pragma instancing_options procedural:setup // Use shader model 3.0 target, to get nicer looking lighting #pragma target 3.0 sampler2D _MainTex; struct Input { float2 uv_MainTex; }; #ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED StructuredBuffer<float> colorXBuffer; StructuredBuffer<float> colorYBuffer; #endif int _Width; int _Height; float _Spacing; half _Glossiness; half _Metallic; fixed4 _Color; void setup() { #ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED int x = (unity_InstanceID / _Width) * _Spacing; int y = (unity_InstanceID % _Width) * _Spacing; _Color = fixed4( (fixed)(unity_InstanceID / _Width) / _Height, (fixed)(unity_InstanceID % _Width) / _Width, 0, 1); // メッシュのワールド座標を設定 unity_ObjectToWorld._14_24_34_44 = float4(x, y, 0, 1); #endif } // Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader. // See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing. // #pragma instancing_options assumeuniformscaling // put more per-instance properties here void surf (Input IN, inout SurfaceOutputStandard o) { // Albedo comes from a texture tinted by color fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color; o.Albedo = c.rgb; // Metallic and smoothness come from slider variables o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = c.a; } ENDCG } FallBack "Diffuse" }
setup関数で座標と色を指定しています。この内容はこのサイトが分かり易いかもしれません。
では結果です。
SetPass Calls = 5、Draw Calls = 5となりました。出来るだけこの方法で描画したほうが良いかもしれません。
ただ、シェーダー内で座標や回転を指定しなくてはいけない分難易度も上がるため3D数学について復習したいと思います。
最後に
あまり描画周り詳しくなかったので今回調べて勉強することが多かったです。ただ、これだけだとまだ理解が足りないので、いくつかのサイトを参考にしてもう少し勉強します。
参考
このサイトも同じように様々な描画メソッドについてまとめています。大変勉強になりました。 gottaniprogramming.seesaa.net
こちらのサイトでも同じようにSetPass CallsやDraw Callsについて調べてまとめています。とても参考になりました。
qiita.com
ブログを見直している際、「MaterialPropertyBlockってなんだっけ?」となったのでこちらのサイトを参考に復習しました。