ShaderTips

シェーダーTips

主にUnityシェーダーについての記事を書いています。

【Unity】ComputeShader + Graphics.DrawMeshInstancedIndirectで大量のオブジェクトを描画する

今回はComputeShaderで大量のオブジェクトの座標を計算して、それをGPUインスタンシングを使って描画します。

オブジェクトの移動アルゴリズムBoidアルゴリズムを使用していますが、今回こちらの解説は行いません。 youtu.be

Step1 : ComputeShaderに必要な情報を渡し実行する

Boid.cs

using System;
using System.Runtime.InteropServices;
using Unity.Collections;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Rendering;
using Random = Unity.Mathematics.Random;

public class Boids : MonoBehaviour
{
    public struct BoidState
    {
        public Vector3 Position;
        public Vector3 Forward;
    }

    [Serializable]
    public class BoidConfig
    {
        public float moveSpeed = 1f;
        // 分離
        [Range(0f, 1f)] public float separationWeight = .5f;
        // 整列
        [Range(0f, 1f)] public float alignmentWeight = .5f;
        // 結合
        [Range(0f, 1f)] public float targetWeight = .5f;

        public Transform boidTarget;
    }

    public int boidCount = 10000;

    public float3 boidExtent = new(32f, 32f, 32f);

    public ComputeShader BoidComputeShader;

    public BoidConfig boidConfig;

    GraphicsBuffer _boidBuffer;
    GraphicsBuffer _argsBuffer;
    int _kernelIndex;

    [SerializeField]
    Mesh mesh;
    [SerializeField]
    Material drawMaterial;

    void Start()
    {
        InitializeArgsBuffer();
        InitializeBoidsBuffer();
    }

    private void InitializeArgsBuffer()
    {
        var args = new uint[] { 0, 0, 0, 0, 0 };

        args[0] = mesh.GetIndexCount(0);
        args[1] = (uint)boidCount;

e        _argsBuffer.SetData(args);
    }

    private void InitializeBoidsBuffer()
    {
        var random = new Random(256);
        var boidArray = new NativeArray<BoidState>(boidCount, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
        for (var i = 0; i < boidArray.Length; i++)
        {
            boidArray[i] = new BoidState
            {
                Position = random.NextFloat3(-boidExtent, boidExtent),
                Forward = math.rotate(random.NextQuaternionRotation(), Vector3.forward),
            };
        }
        // GraphicsBuffer生成
        _boidBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, boidArray.Length, Marshal.SizeOf<BoidState>());
        // boidBufferに値を設定
        _boidBuffer.SetData(boidArray);
    }

    void Update()
    {
        UpdateBoids();
        RenderMesh();
    }

    void UpdateBoids()
    {
        var boidTarget = boidConfig.boidTarget != null
            ? boidConfig.boidTarget.position
            : transform.position;
        BoidComputeShader.SetFloat("deltaTime", Time.deltaTime);
        BoidComputeShader.SetFloat("separationWeight", boidConfig.separationWeight);
        BoidComputeShader.SetFloat("alignmentWeight", boidConfig.alignmentWeight);
        BoidComputeShader.SetFloat("targetWeight", boidConfig.targetWeight);
        BoidComputeShader.SetFloat("moveSpeed", boidConfig.moveSpeed);
        BoidComputeShader.SetVector("targetPosition", boidTarget);
        // ComputeShaderに生成するインスタンスの数をセット
        BoidComputeShader.SetInt("numBoids", boidCount);
        _kernelIndex = BoidComputeShader.FindKernel("CSMain");

        BoidComputeShader.GetKernelThreadGroupSizes(_kernelIndex, out var x, out var y, out var z);

        // ComputeShaderにboidBufferをセット
        BoidComputeShader.SetBuffer(_kernelIndex, "boidBuffer", _boidBuffer);
        // ComputeShaderを実行
        BoidComputeShader.Dispatch(_kernelIndex, (int)(boidCount / x), 1, 1);
    }

    void RenderMesh()
    {
        if (!SystemInfo.supportsInstancing)
        {
            return;
        }
        drawMaterial.SetBuffer("_BoidDataBuffer", _boidBuffer);
        BoidState[] data = new BoidState[10];
        _boidBuffer.GetData(data);
        Debug.Log(data);
        Graphics.DrawMeshInstancedIndirect
        (
            mesh,
            0,
            drawMaterial,
            new Bounds(Vector3.zero, new Vector3(1000.0f, 1000.0f, 1000.0f)),
            _argsBuffer
        );
    }

    void OnDisable()
    {
        _boidBuffer?.Dispose();
        _argsBuffer?.Dispose();
    }
}

InitializeBoidsBufferでComputeShaderに渡すGraphicsBufferを作成しています。 BoidState(位置と進行方向を表す情報)を格納します。 boidBufferと名付けました。

// GraphicsBuffer生成
_boidBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, boidArray.Length, Marshal.SizeOf<BoidState>());
// boidBufferに値を設定
_boidBuffer.SetData(boidArray);

UpdateBoidsで必要な情報やboidBufferをComputeShaderに渡して、実行しています。

// ComputeShaderにboidBufferをセット
BoidComputeShader.SetBuffer(_kernelIndex, "boidBuffer", _boidBuffer);
// ComputeShaderを実行
BoidComputeShader.Dispatch(_kernelIndex, (int)(boidCount / x), 1, 1);

Step2 : ComputeShaderでオブジェクトの座標と進行方向を計算する

ComputeShaderはこちらを使用させていただきました。 github.com 先ほど送ったboidBufferはRWStructuredBufferで宣言されています。 これは読み書き可能なBuffferであることを意味しています。 今回は解説しませんが、Boid.csから送ったboidBufferの情報を使いつつ、計算結果をboidBufferに格納しています。

RWStructuredBuffer<BoidState> boidBuffer;

Step3 : GPUインスタンシングで描画を行う

Boid.cs

using System;
using System.Runtime.InteropServices;
using Unity.Collections;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Rendering;
using Random = Unity.Mathematics.Random;

public class Boids : MonoBehaviour
{
    public struct BoidState
    {
        public Vector3 Position;
        public Vector3 Forward;
    }

    [Serializable]
    public class BoidConfig
    {
        public float moveSpeed = 1f;
        // 分離
        [Range(0f, 1f)] public float separationWeight = .5f;
        // 整列
        [Range(0f, 1f)] public float alignmentWeight = .5f;
        // 結合
        [Range(0f, 1f)] public float targetWeight = .5f;

        public Transform boidTarget;
    }

    public int boidCount = 10000;

    public float3 boidExtent = new(32f, 32f, 32f);

    public ComputeShader BoidComputeShader;

    public BoidConfig boidConfig;

    GraphicsBuffer _boidBuffer;
    GraphicsBuffer _argsBuffer;
    int _kernelIndex;

    [SerializeField]
    Mesh mesh;
    [SerializeField]
    Material drawMaterial;

    void Start()
    {
        InitializeArgsBuffer();
        InitializeBoidsBuffer();
    }

    private void InitializeArgsBuffer()
    {
        var args = new uint[] { 0, 0, 0, 0, 0 };

        args[0] = mesh.GetIndexCount(0);
        args[1] = (uint)boidCount;

e        _argsBuffer.SetData(args);
    }

    private void InitializeBoidsBuffer()
    {
        var random = new Random(256);
        var boidArray = new NativeArray<BoidState>(boidCount, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
        for (var i = 0; i < boidArray.Length; i++)
        {
            boidArray[i] = new BoidState
            {
                Position = random.NextFloat3(-boidExtent, boidExtent),
                Forward = math.rotate(random.NextQuaternionRotation(), Vector3.forward),
            };
        }
        // GraphicsBuffer生成
        _boidBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, boidArray.Length, Marshal.SizeOf<BoidState>());
        // boidBufferに値を設定
        _boidBuffer.SetData(boidArray);
    }

    void Update()
    {
        UpdateBoids();
        RenderMesh();
    }

    void UpdateBoids()
    {
        var boidTarget = boidConfig.boidTarget != null
            ? boidConfig.boidTarget.position
            : transform.position;
        BoidComputeShader.SetFloat("deltaTime", Time.deltaTime);
        BoidComputeShader.SetFloat("separationWeight", boidConfig.separationWeight);
        BoidComputeShader.SetFloat("alignmentWeight", boidConfig.alignmentWeight);
        BoidComputeShader.SetFloat("targetWeight", boidConfig.targetWeight);
        BoidComputeShader.SetFloat("moveSpeed", boidConfig.moveSpeed);
        BoidComputeShader.SetVector("targetPosition", boidTarget);
        // ComputeShaderに生成するインスタンスの数をセット
        BoidComputeShader.SetInt("numBoids", boidCount);
        _kernelIndex = BoidComputeShader.FindKernel("CSMain");

        BoidComputeShader.GetKernelThreadGroupSizes(_kernelIndex, out var x, out var y, out var z);

        // ComputeShaderにboidBufferをセット
        BoidComputeShader.SetBuffer(_kernelIndex, "boidBuffer", _boidBuffer);
        // ComputeShaderを実行
        BoidComputeShader.Dispatch(_kernelIndex, (int)(boidCount / x), 1, 1);
    }

    void RenderMesh()
    {
        if (!SystemInfo.supportsInstancing)
        {
            return;
        }
        drawMaterial.SetBuffer("_BoidDataBuffer", _boidBuffer);
        BoidState[] data = new BoidState[10];
        _boidBuffer.GetData(data);
        Debug.Log(data);
        Graphics.DrawMeshInstancedIndirect
        (
            mesh,
            0,
            drawMaterial,
            new Bounds(Vector3.zero, new Vector3(1000.0f, 1000.0f, 1000.0f)),
            _argsBuffer
        );
    }

    void OnDisable()
    {
        _boidBuffer?.Dispose();
        _argsBuffer?.Dispose();
    }
}

描画するマテリアルにboidBufferを渡しています。 Graphics.DrawMeshInstancedIndirectは指定したマテリアルをGPUインスタンシングで描画することができます。

Graphics.DrawMeshInstancedIndirect(Mesh instanceMesh, int subMeshIndex, Material instanceMaterial, Vector3 bounds, ComputeBuffer argsBuffer);

Mesh instanceMesh : InstansingしたいMesh

int subMeshIndex : 何番目のSubMeshを使用するか

Material instanceMaterial : InstansingするMeshに適応させるMaterial

Vector3 bounds : 描画するインスタンスを囲む境界ボリューム

GraphicsBuffer argsBuffer : InstancingするためのMesh情報

 void RenderMesh()
    {
        if (!SystemInfo.supportsInstancing)
        {
            return;
        }
        drawMaterial.SetBuffer("_BoidDataBuffer", _boidBuffer);
        BoidState[] data = new BoidState[10];
        _boidBuffer.GetData(data);
        Debug.Log(data);
        Graphics.DrawMeshInstancedIndirect
        (
            mesh,
            0,
            drawMaterial,
            new Bounds(Vector3.zero, new Vector3(1000.0f, 1000.0f, 1000.0f)),
            _argsBuffer
        );
    }

argsBufferはuint型の配列を格納するBufferです。 Meshのインデックスと、Meshの数を持たせます。

    private void InitializeArgsBuffer()
    {
        var args = new uint[] { 0, 0, 0, 0, 0 };

        args[0] = mesh.GetIndexCount(0);
        args[1] = (uint)boidCount;

        _argsBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, args.Length, sizeof(uint));
        _argsBuffer.SetData(args);
    }

BoidBufferの情報を使って描画する

Graphics.DrawMeshInstancedIndirec

Boids.shader

Shader "Boids"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _ObjectScale ("Scale", Vector) = (1,1,1,1)
    }
    SubShader
    {
        Pass
        {
            Tags { "RenderType"="Opaque" }
            LOD 200
            
            HLSLPROGRAM
            #pragma instancing_options procedural:setup
            #pragma multi_compile_instancing
            #pragma target 4.5

            #pragma vertex vert
            #pragma fragment frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            struct Attributes
            {
                float4 positionOS   : POSITION;
                float3 normalOS     : NORMAL;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct Varyings
            {
                float2 uv                       : TEXCOORD0;
                float4 positionCS               : SV_POSITION;
                half3 data : TEXCOORD1;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            // Boidの構造体
            struct BoidState
            {
                float3 position; // 位置
                float3 forward; // 速度
            };

           #ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
                // Boidデータの構造体バッファ
                StructuredBuffer<BoidState> _BoidDataBuffer;
           #endif

            sampler2D _MainTex; // テクスチャ
            half3 _Color;      // カラー
            float3 _ObjectScale; // Boidオブジェクトのスケール

            // オイラー角(ラジアン)を回転行列に変換
            float4x4 eulerAnglesToRotationMatrix(float3 angles)
            {
                float ch = cos(angles.y); float sh = sin(angles.y); // heading
                float ca = cos(angles.z); float sa = sin(angles.z); // attitude
                float cb = cos(angles.x); float sb = sin(angles.x); // bank

                // Ry-Rx-Rz (Yaw Pitch Roll)
                return float4x4(
                ch * ca + sh * sb * sa, -ch * sa + sh * sb * ca, sh * cb, 0,
                cb * sa, cb * ca, -sb, 0,
                -sh * ca + ch * sb * sa, sh * sa + ch * sb * ca, ch * cb, 0,
                0, 0, 0, 1
                );
            }

            // 頂点シェーダ
            Varyings vert(Attributes input, uint instanceID : SV_InstanceID)
            {

                Varyings output = (Varyings)0;
                #ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED

                // インスタンスIDからBoidのデータを取得
                BoidState boidData = _BoidDataBuffer[instanceID]; 

                float3 pos = boidData.position.xyz; // Boidの位置を取得
                float3 scl = _ObjectScale;          // Boidのスケールを取得

                // ワールド行列
                float4x4 object2world = (float4x4)0; 
                // スケール値を代入
                object2world._11_22_33_44 = float4(scl.xyz, 1.0);

                // 速度からY軸についての回転を算出
                float rotY = atan2(boidData.forward.x, boidData.forward.z);
                // 速度からX軸についての回転を算出
                float rotX = -asin(boidData.forward.y / (length(boidData.forward.xyz) + 1e-8));
                // オイラー角(ラジアン)から回転行列を求める
                float4x4 rotMatrix = eulerAnglesToRotationMatrix(float3(rotX, rotY, 0));
                // 行列に回転を適用
                object2world = mul(rotMatrix, object2world);
                // 行列に位置(平行移動)を適用
                object2world._14_24_34 += pos.xyz;

                // 頂点を座標変換
                float4 positionWS = mul(object2world, input.positionOS);
                output.positionCS = TransformWorldToHClip(positionWS);
                #endif
                return output;
            }
            
            void setup()
            {
            }

            half4 frag(Varyings input) : SV_Target
            {
                return half4(_Color,1);
            }
            ENDHLSL
        }
    }
}

instancing_options proceduralはGraphics.DrawMeshInstancedIndirectと一緒に使用されます。 setupの部分は任意の関数名を指定できます。

ここで、指定した関数は頂点シェーダーが実行される前に各インスタンスごとに呼ばれます。

公式リファレンスではBufferの回転値から、変換行列を変更しているようですが、今回はsetup関数に処理を入れず頂点シェーダーで行います。

Unity - Scripting API: Graphics.DrawMeshInstancedIndirect

#pragma instancing_options procedural:setup

UNITY_PROCEDURAL_INSTANCING_ENABLEDはGraphics.DrawMeshInstancedIndirect(Graphics.DrawMeshInstancedも)を使用して描画するときに有効になります。

BoidComputeShader.SetBufferで、送られたデータが、StructuredBufferで宣言した_BoidDataBufferに格納されます。

// Boidの構造体
struct BoidState
{
   float3 position; // 位置
   float3 forward; // 速度
};

#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
     // Boidデータの構造体バッファ
     StructuredBuffer<BoidState> _BoidDataBuffer;
#endif

_BoidDataBuffer[instanceID]で、処理中の頂点のインスタンスからBoidDataを取得して、位置や回転の計算しています。

Varyings vert(Attributes input, uint instanceID : SV_InstanceID)
{
    Varyings output = (Varyings)0;
    #ifdef UNITY_PROCEDURAL_INSTANCING_ENABL
    // インスタンスIDからBoidのデータを取得
    BoidState boidData = _BoidDataBuffer[instanceID]; 
・
・
・

参考

edom18.hateblo.jp

www.youtube.com