したかみ ぶろぐ

Unity成分多め

描画を効率的に行いたい

始めに

前回ComputeShaderについて勉強しました。

shitakami.hatenablog.com

shitakami.hatenablog.com


次に学習した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はスキップされるそうです。

f:id:vxd-naoshi-19961205-maro:20191110214310p:plain


DrawCallはCPUからGPUへ描画命令を送る処理です。ここで先程のSetPassで設定されたマテリアルの設定値を用いてポリゴンを描画します。同一のマテリアルから異なるオブジェクトを描画する際にもDrawCallが発生するみたいです。

f:id:vxd-naoshi-19961205-maro:20191110215308p:plain


順番としてSetPass Call -> DrawCallが行われます。



確認してみる

UnityのWindow->Analysis->Profilerを開き、Renderingを選択することでみることでSetPass Call、Draw Callが行われた回数を確認することが出来ます。

f:id:vxd-naoshi-19961205-maro:20191110215844p:plain f:id:vxd-naoshi-19961205-maro:20191110215854p:plain



様々な描画を試してみる

以下のサイトを参考にしながら様々なインスタンシングをしてSetpass Calls, Draw Callsを調べてみます。

qiita.com


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がスキップされているようです。

f:id:vxd-naoshi-19961205-maro:20191111022717p:plain



では次に、一つ一つ別の色をマテリアルに設定してみました。

.........
        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)));
.........

f:id:vxd-naoshi-19961205-maro:20191111022648p:plain

結果としてSetPass Callsが増えました。 f:id:vxd-naoshi-19961205-maro:20191111022710p:plain

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);

            }
        }
    }
}

f:id:vxd-naoshi-19961205-maro:20191111025613p:plain

ここで出てくるMaterialPropertyBlockについて少し解説します。

オブジェクトごとに色を変更した場合、新しくマテリアルを作成してそれぞれにそのマテリアルを設定します。それによってオブジェクトの数だけマテリアルが作成されることとなります。

それをMaterialPropertyBlockを使用すれば新しいMaterialを作成することなく、一部のプロパティの値を変更することが出来ます。



結果としてSetPass Callsは変わりませんでしたが、Draw Callsは 1/8 とかなり減りました。

f:id:vxd-naoshi-19961205-maro:20191111025811p:plain



ここで試しに、MaterialのEnable GPU Instancingをチェックして実行してみました。描画結果は変わらないです。

f:id:vxd-naoshi-19961205-maro:20191111034029p:plain


結果はSetPass CallsとDraw Callsは半分まで減りました。

f:id:vxd-naoshi-19961205-maro:20191111034146p:plain



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が少なくなっています。 f:id:vxd-naoshi-19961205-maro:20191112031645p:plain



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関数で座標と色を指定しています。この内容はこのサイトが分かり易いかもしれません。

docs.google.com



では結果です。

f:id:vxd-naoshi-19961205-maro:20191113141632p:plain f:id:vxd-naoshi-19961205-maro:20191113141919p:plain

SetPass Calls = 5、Draw Calls = 5となりました。出来るだけこの方法で描画したほうが良いかもしれません。

ただ、シェーダー内で座標や回転を指定しなくてはいけない分難易度も上がるため3D数学について復習したいと思います。



最後に

あまり描画周り詳しくなかったので今回調べて勉強することが多かったです。ただ、これだけだとまだ理解が足りないので、いくつかのサイトを参考にしてもう少し勉強します。

参考

このサイトも同じように様々な描画メソッドについてまとめています。大変勉強になりました。 gottaniprogramming.seesaa.net


こちらのサイトでも同じようにSetPass CallsやDraw Callsについて調べてまとめています。とても参考になりました。 qiita.com


ブログを見直している際、「MaterialPropertyBlockってなんだっけ?」となったのでこちらのサイトを参考に復習しました。

light11.hatenadiary.com