したかみ ぶろぐ

Unity成分多め

Graphics.DrawMeshInstancedIndirectまとめ

始めに

去年ちょっとだけGraphics.DrawMeshInstancedIndirectを触りました。

shitakami.hatenablog.com


今回はその復習も兼ねてこの機能についてまとめていこうと思います。



Graphics.DrawMeshInstancedIndirectについて

このメソッドを使用することによって、同じメッシュをGPUInstancingすることができます。

簡単に言いますと滅茶苦茶たくさんのメッシュを描画することが出来ます。


この関数の引数について簡単に以下にまとめます。 また、その後に追加の解説をいくつか載せます。

引数 解説
mesh 描画するメッシュデータ
submeshIndex 描画するサブメッシュのインデックス(基本0が代入される)
material 使用するマテリアル
bounds 描画する範囲
bufferWithArgs 描画する個数などが入ったデータ
argsOffset bufferWithArgsが何バイト目から始まるか(デフォルトは0)
properties MaterialPropertyBlockを入れる。未調査です。(デフォルトはnull)
castShadows 描画したメッシュの影を描画するか(デフォルトはOn)
receiveShadows 描画したメッシュに他のオブジェクトの影を描画するか (デフォルトはtrue)
layer 使用するレイヤー(デフォルトは0)
camera 描画させるカメラ(デフォルトは0)
lightProbeUsage LightProbeの指定(デフォルトはBlendProbe)
lightProbeProxyVolume わからない. . .公式にも載ってない(デフォルトはnull)

特に細かく設定しないのであれば第五引数(bufferWithArgs)までを与えれば良いです。



bounds

boundsについては「描画する範囲」と解説していますが、これは「カメラがboundsで指定した範囲内を描画すれば、メッシュを描画する」となります。

例えば、中心座標を(x, y, z) = (0, 0, 0)、大きさを(x, y, z) = (0, 0, 0)にした場合はカメラが座標(0, 0, 0)を映していないと描画されたメッシュが消えてしまいます。 以下gifがその例です。


メッシュが描画される座標すべてを含める範囲を指定した方がいいかもしれません。

追記

boundsのCenterの座標はShader側の座標変換に影響を与えるので注意が必要です。

例えば、中心座標を(100, 100, 100)に設定した場合にShader側で(0, 0, 0)の座標に表示しようとすると、(100, 100, 100)にオブジェクトが表示されます。

なので、Centerを変更する場合はShaderにもCenterを適用して、Center分だけ座標をずらす必要があります。



bufferWithArgs

BufferWithArgsは5つのuint型の値を持つComputeBufferになります。

  • サブメッシュのインデックスの数
  • 描画する個数
  • メッシュバッファの開始インデックス
  • 基底頂点位置(base vertex location)
  • スタートインスタンス位置(start instance location)

1番目についてはMesh.GetIndexCount(0)で求めることが出来ます。

2番目についてはここで描画させたメッシュの個数をしていすれば、その分だけのオブジェクトを描画することが出来ます。

それ以外についてですが色々調べてもあまり情報が出ず、基本的に0で初期化すれば問題ないようです。



castShadows、receiveShadows

この項目では、「影を出すか」「影を受けるか」を指定することが出来ます。 デフォルトでは両方有効になっているため、軽量化したい場合はこの項目を変更すると良いかもしれません。



layer、camera

この項目を変更することで、描画するカメラを指定したりCullingMaskなどで描画するかを判定することが出来ます。

この内容については以下のサイトが参考になるかもしれません。

unity-guide.moon-bear.com



簡単なサンプル

調べた内容を元にして実装したサンプルになります。

TestDrawMesh.cs

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

public class TestDrawMesh : MonoBehaviour
{

    [Header("DrawMeshInstancedIndirectのパラメータ")]
    [SerializeField]
    private Mesh m_mesh;

    [SerializeField]
    private Material m_instanceMaterial;

    [SerializeField]
    private Bounds m_bounds;

    [SerializeField]
    private ShadowCastingMode m_shadowCastingMode;

    [SerializeField]
    private bool m_receiveShadows;

    [SerializeField]
    private string m_layerName;

    [SerializeField]
    private Camera m_camera;


    [Space(20)]
    [SerializeField]
    private int m_instanceCount;

    private ComputeBuffer m_argsBuffer;

    private ComputeBuffer m_positionBuffer;

    private ComputeBuffer m_eulerAngleBuffer;

    private int m_layer;

    // Start is called before the first frame update
    void Start()
    {
        InitializeArgsBuffer();
        InitializePositionBuffer();
        InitializeEulerAngleBuffer();

        m_instanceMaterial.SetVector("_CenterOffset", m_bounds.Center);

        m_layer = LayerMask.NameToLayer(m_layerName);
    }

    // Update is called once per frame
    void Update()
    {

        Graphics.DrawMeshInstancedIndirect(
            m_mesh,
            0,
            m_instanceMaterial,
            m_bounds,
            m_argsBuffer,
            0,
            null,
            m_shadowCastingMode,
            m_receiveShadows,
            m_layer,
            m_camera,
            LightProbeUsage.BlendProbes,
            null
        );

    }

    private void InitializeArgsBuffer() {

        uint[] args = new uint[5] { 0, 0, 0, 0, 0 };

        uint numIndices = (m_mesh != null) ? (uint) m_mesh.GetIndexCount(0) : 0;

        args[0] = numIndices;
        args[1] = (uint)m_instanceCount;
        
        m_argsBuffer = new ComputeBuffer(1, args.Length * sizeof(uint), ComputeBufferType.IndirectArguments);
        m_argsBuffer.SetData(args);

    }

    private void InitializePositionBuffer() {

        // xyz:座標   w:スケール
        Vector4[] positions = new Vector4[m_instanceCount];

        for (int i = 0; i < m_instanceCount; ++i)
        {

            positions[i].x = Random.Range(-100.0f, 100.0f);
            positions[i].y = Random.Range(-100.0f, 100.0f);
            positions[i].z = Random.Range(-100.0f, 100.0f);

            positions[i].w = Random.Range(0.1f, 1f);

        }

        m_positionBuffer = new ComputeBuffer(m_instanceCount, 4 * 4);
        m_positionBuffer.SetData(positions);

        m_instanceMaterial.SetBuffer("positionBuffer", m_positionBuffer);

    }

    private void InitializeEulerAngleBuffer() {

        Vector3[] angles = new Vector3[m_instanceCount];

        for(int i = 0; i < m_instanceCount; ++i) {
            angles[i].x = Random.Range(-180.0f, 180.0f);
            angles[i].y = Random.Range(-180.0f, 180.0f);
            angles[i].z = Random.Range(-180.0f, 180.0f);
        }

        m_eulerAngleBuffer = new ComputeBuffer(m_instanceCount, 4 * 3);
        m_eulerAngleBuffer.SetData(angles);

        m_instanceMaterial.SetBuffer("eulerAngleBuffer", m_eulerAngleBuffer);

    }

    // 領域の解放
    private void OnDisable() {

        if(m_positionBuffer != null)
            m_positionBuffer.Release();
        m_positionBuffer = null;

        if(m_eulerAngleBuffer != null)
            m_eulerAngleBuffer.Release();
        m_eulerAngleBuffer = null;

        if(m_argsBuffer != null)
            m_argsBuffer.Release();
        m_argsBuffer = null;

    }

}


TestInstancedIndirectSurf.shader

Shader "Custom/TestInstancedIndirectSurf"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        // Physically based Standard lighting model, and enable shadows on all light types
        #pragma surface surf Standard addshadow fullforwardshadows // 影を描画するためにはaddshadowが必要
        #pragma multi_compile_instancing    // GPU Instancingを可能にする
        #pragma instancing_options procedural:setup // setup関数を呼び出す

        sampler2D _MainTex;
        float3 _CenterOffset;

        struct Input
        {
            float2 uv_MainTex;
        };

#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
    StructuredBuffer<float4> positionBuffer;
    StructuredBuffer<float3> eulerAngleBuffer;
#endif

        #define Deg2Rad 0.0174532924

        float4x4 eulerAnglesToRottationMatrix(float3 angles) {

            float cx = cos(angles.x * Deg2Rad); float sx = sin(angles.x * Deg2Rad);
            float cy = cos(angles.z * Deg2Rad); float sy = sin(angles.z * Deg2Rad);
            float cz = cos(angles.y * Deg2Rad); float sz = sin(angles.y * Deg2Rad);

            return float4x4(
                cz*cy + sz*sx*sy, -cz*sy + sz*sx*cy, sz*cx, 0,
                sy*cx, cy*cx, -sx, 0,
                -sz*cy + cz*sx*sy, sy*sz + cz*sx*cy, cz*cx, 0,
                0, 0, 0, 1);

        }


        void setup() {

        #ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
            float4 data = positionBuffer[unity_InstanceID];
            float3 angles = eulerAngleBuffer[unity_InstanceID];

            // スケーリング
            unity_ObjectToWorld._11_21_31_41 = float4(data.w, 0, 0, 0);
            unity_ObjectToWorld._12_22_32_42 = float4(0, data.w, 0, 0);
            unity_ObjectToWorld._13_23_33_43 = float4(0, 0, data.w, 0);

            // 回転
            unity_ObjectToWorld = mul(eulerAnglesToRottationMatrix(angles), unity_ObjectToWorld);

            // 座標
            unity_ObjectToWorld._14_24_34_44 = float4(data.xyz, 1) - _CenterOffset;  // Boundsの中心座標だけずらす
            // モデル行列を求める
            unity_WorldToObject = unity_ObjectToWorld;
            unity_WorldToObject._14_24_34 *= -1;
            unity_WorldToObject._11_12_13 = unity_ObjectToWorld._11_21_31;
            unity_WorldToObject._21_22_23 = unity_ObjectToWorld._12_22_32;
            unity_WorldToObject._31_32_33 = unity_ObjectToWorld._13_23_33;
            unity_WorldToObject._11_12_13 /= data.w * data.w;
            unity_WorldToObject._21_22_23 /= data.w * data.w;
            unity_WorldToObject._31_32_33 /= data.w * data.w;
        #endif

        }


        half _Glossiness;
        half _Metallic;
        fixed4 _Color;

        // 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
        UNITY_INSTANCING_BUFFER_START(Props)
            // put more per-instance properties here
        UNITY_INSTANCING_BUFFER_END(Props)

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            // Albedo comes from a texture tinted by color
            fixed4 cy = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = cy.rgb;
            // Metallic and smoothness come from slider variables
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = cy.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}



解説

bufferWithArgsの初期化

bufferWithArgsの初期化を行います。ここでは変数m_argsBufferに相当します。

始めにuint型の配列を作成し、そこに必要なデータを渡します。ここでは0番目にMesh.GetIndexCountの値を、1番目に描画したいメッシュの個数を入れます。

uint[] args = new uint[5] { 0, 0, 0, 0, 0 };

uint numIndices = (m_mesh != null) ? (uint) m_mesh.GetIndexCount(0) : 0;

args[0] = numIndices;
args[1] = (uint)m_instanceCount;


次に、m_argsBufferの領域を作成します。 ComputeBufferのコンストラクターでは、要素数1、データの個数5 * uintのバイト数、ComputeBufferType.IndirectArgumentsを指定します。

その次に、SetData関数を使って先ほどのargsの値をm_argsBufferに入れます。

m_argsBuffer = new ComputeBuffer(1, args.Length * sizeof(uint), ComputeBufferType.IndirectArguments);
m_argsBuffer.SetData(args);



座標、回転、大きさの初期化

すべてのメッシュが同じ座標、同じ回転であったらちょっと嫌なのでランダムに描画したいと思います。

描画する際に座標や回転などをDrawMeshInstancedIndirectで指定する方法は無いので、ComputeBufferに座標などの値を入れて描画で使用するMaterialに渡します。

注意点としてbufferWithArgsと異なり、ComputeBufferのコンストラクタ-に描画するメッシュの個数、型の大きさを入れます。

また、ComputeBufferのコンストラクタの第二引数で"4 * 4"、"4 * 3"と指定されていますが、これはfloat型のbyte数*要素数(Vector4なら4つ、Vector3なら3つ)を表します。

追記:System.Runtime.InteropServices.Marshal.SizeOfでVector4やVector3のサイズを取得することが出来ます。


    private void InitializePositionBuffer() {

        // xyz:座標   w:スケール
        Vector4[] positions = new Vector4[m_instanceCount];

        for (int i = 0; i < m_instanceCount; ++i)
        {

            positions[i].x = Random.Range(-100.0f, 100.0f);
            positions[i].y = Random.Range(-100.0f, 100.0f);
            positions[i].z = Random.Range(-100.0f, 100.0f);

            positions[i].w = Random.Range(0.1f, 1f);

        }

        m_positionBuffer = new ComputeBuffer(m_instanceCount, 4 * 4);
        m_positionBuffer.SetData(positions);

        m_instanceMaterial.SetBuffer("positionBuffer", m_positionBuffer);

    }

    private void InitializeEulerAngleBuffer() {

        Vector3[] angles = new Vector3[m_instanceCount];

        for(int i = 0; i < m_instanceCount; ++i) {
            angles[i].x = Random.Range(-180.0f, 180.0f);
            angles[i].y = Random.Range(-180.0f, 180.0f);
            angles[i].z = Random.Range(-180.0f, 180.0f);
        }

        m_eulerAngleBuffer = new ComputeBuffer(m_instanceCount, 4 * 3);
        m_eulerAngleBuffer.SetData(angles);

        m_instanceMaterial.SetBuffer("eulerAngleBuffer", m_eulerAngleBuffer);

    }



座標、回転、大きさの計算

ここでは先ほどマテリアルに渡された座標、回転、大きさのデータの扱いについて解説します。

ComputeBufferを使用してMaterialに渡されたデータはStructuredBufferに保存されます。

次に、unity_InstanceIDを使用してデータを取り出し座標計算を行います。

このsetup関数はvert関数やsurf関数が実行される前に呼び出されます。

#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
    StructuredBuffer<float4> positionBuffer;
    StructuredBuffer<float3> eulerAngleBuffer;
#endif

        #define Deg2Rad 0.0174532924

        float4x4 eulerAnglesToRottationMatrix(float3 angles) {

            float cx = cos(angles.x * Deg2Rad); float sx = sin(angles.x * Deg2Rad);
            float cy = cos(angles.z * Deg2Rad); float sy = sin(angles.z * Deg2Rad);
            float cz = cos(angles.y * Deg2Rad); float sz = sin(angles.y * Deg2Rad);

            return float4x4(
                cz*cy + sz*sx*sy, -cz*sy + sz*sx*cy, sz*cx, 0,
                sy*cx, cy*cx, -sx, 0,
                -sz*cy + cz*sx*sy, sy*sz + cz*sx*cy, cz*cx, 0,
                0, 0, 0, 1);

        }


        void setup() {

        #ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
            float4 data = positionBuffer[unity_InstanceID];
            float3 angles = eulerAngleBuffer[unity_InstanceID];

            // スケーリング
            unity_ObjectToWorld._11_21_31_41 = float4(data.w, 0, 0, 0);
            unity_ObjectToWorld._12_22_32_42 = float4(0, data.w, 0, 0);
            unity_ObjectToWorld._13_23_33_43 = float4(0, 0, data.w, 0);

            // 回転
            unity_ObjectToWorld = mul(eulerAnglesToRottationMatrix(angles), unity_ObjectToWorld);

            // 座標
            unity_ObjectToWorld._14_24_34_44 = float4(data.xyz, 1) - _CenterOffset;  // Boundsの中心座標だけずらす

            // モデル行列を求める
            unity_WorldToObject = unity_ObjectToWorld;
            unity_WorldToObject._14_24_34 *= -1;
            unity_WorldToObject._11_12_13 = unity_ObjectToWorld._11_21_31;
            unity_WorldToObject._21_22_23 = unity_ObjectToWorld._12_22_32;
            unity_WorldToObject._31_32_33 = unity_ObjectToWorld._13_23_33;
            unity_WorldToObject._11_12_13 /= data.w * data.w;
            unity_WorldToObject._21_22_23 /= data.w * data.w;
            unity_WorldToObject._31_32_33 /= data.w * data.w;
        #endif

        }



描画

描画する際はUpdate関数で毎フレーム呼び出して描画します。

ここではすべての引数を指定していますが、詳細を設定しないのであればbufferWithArgsまでで大丈夫です。

    void Update()
    {

        Graphics.DrawMeshInstancedIndirect(
            m_mesh,
            0,
            m_instanceMaterial,
            m_bounds,
            m_argsBuffer,
            0,
            null,
            m_shadowCastingMode,
            m_receiveShadows,
            m_layer,
            m_camera,
            LightProbeUsage.BlendProbes,
            null
        );

    }



使用したComputeBufferの解放

ComputeBufferは使用した後に解放しないと警告が出るので忘れずにしましょう。

    private void OnDisable() {

        if(m_positionBuffer != null)
            m_positionBuffer.Release();
        m_positionBuffer = null;

        if(m_eulerAngleBuffer != null)
            m_eulerAngleBuffer.Release();
        m_eulerAngleBuffer = null;

        if(m_argsBuffer != null)
            m_argsBuffer.Release();
        m_argsBuffer = null;

    }



結果

サムネでもお見せしましたが、このように大量のCubeを出力することが出来ました。


また、100万個のCubeを出力しても60fps以上でました。


遠くから見ると流石に恐怖でした。



感想

前に触った際はあまり覚えきれていなかったので、今回はがっつりまとめてみました。

まだまだ、分からない点はありますが使おうと思えば使えるレベルにはまとめきれたかなと思います。

これからまた、分かったことがあれば追記していこうかなと思います。



参考

今回は多くのUnity公式マニュアルを参考にしました。

docs.unity3d.com

docs.unity3d.com


また、サンプルとして以下のリポジトリがとても参考になりました。

github.com


その他参考にしたサイトです。

edom18.hateblo.jp

docs.google.com