したかみ ぶろぐ

Unity成分多め

DrawMeshInstancedIndirectとComputeShaderを組み合わせる

始めに

前回はDrawMeshInstancedIndirectについてまとめました。

shitakami.hatenablog.com

今回は前回の内容とComputeShaderを組み合わせた簡単なサンプルを作成したいと思います。


ComputeShaderについては以下の記事にまとめています。

shitakami.hatenablog.com

shitakami.hatenablog.com



プログラム

今回のプログラムは前回のプログラムにComputeShaderの機能を追加したものとなります。

前回と同様にDrawMeshInstancedIndirectでcubeを描画しますが、ComputeShaderを使用してcubeを回転させながら上下に動かしたいと思います。


C#プログラム

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using System.Runtime.InteropServices;   // Marshal.Sizeofの呼び出しに必要

public class DrawCubesWithMoving : MonoBehaviour
{

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

    [SerializeField]
    private Material m_material;

    [SerializeField]
    private Bounds m_bounds;

    [SerializeField]
    private ShadowCastingMode m_shadowCastingMode;

    [SerializeField]
    private bool m_receiveShadows;

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

    [Space(20)]
    [SerializeField]
    private ComputeShader m_computeShader;

    [SerializeField]
    private float m_moveSpeed;
    [SerializeField]
    private float m_rotateSpeed;

    [SerializeField]
    private float m_moveHeight;

    private int m_kernelID;

    private Vector3Int m_groupSize;

    private ComputeBuffer m_argsBuffer;

    private ComputeBuffer m_cubeParamBuffer;

    struct CubeParameter {
        public Vector3 position;
        public Vector3 angle;
        public float scale;
        public float randTime;
        public float baseHeight;
    }

    // Start is called before the first frame update
    void Start()
    {
        // 描画するメッシュの個数を最も近い2の累乗の値する
        m_instanceCount = Mathf.ClosestPowerOfTwo(m_instanceCount);

        InitializeArgsBuffer();
        InitializeCubeParamBuffer();
        InitializeComputeShader();
    }

    // Update is called once per frame
    void Update()
    {
        UpdatePositionAndAngle();
    }

    void LateUpdate() {

        Graphics.DrawMeshInstancedIndirect(
            m_mesh,
            0,
            m_material,
            m_bounds,
            m_argsBuffer,
            0,
            null,
            m_shadowCastingMode,
            m_receiveShadows
        );

    }

    private void InitializeArgsBuffer() {

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

        args[0] = (m_mesh != null) ? m_mesh.GetIndexCount(0) : 0;
        args[1] = (uint)m_instanceCount;

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

    }

    private void InitializeComputeShader() {

        // カーネルIDの取得
        m_kernelID = m_computeShader.FindKernel("ChangeCubeParameter");

        // グループサイズを求める
        m_computeShader.GetKernelThreadGroupSizes(m_kernelID, out uint x, out uint y, out uint z);
        m_groupSize = new Vector3Int((int)x, (int)y, (int)z);
        m_groupSize.x = m_instanceCount / m_groupSize.x;

        // パラメータをComputeShaderに設定
        m_computeShader.SetBuffer(m_kernelID, "cubeParamBuffer", m_cubeParamBuffer);
        m_computeShader.SetFloat("moveSpeed", m_moveSpeed);
        m_computeShader.SetFloat("rotateSpeed", m_rotateSpeed);
        m_computeShader.SetFloat("moveHeight", m_moveHeight);

    }

    private void InitializeCubeParamBuffer() {

        CubeParameter[] cubeParameters = new CubeParameter[m_instanceCount];

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

            cubeParameters[i].position = new Vector3(Random.Range(-100f, 100f), Random.Range(-100f, 100f), Random.Range(-100f, 100f));
            cubeParameters[i].angle = new Vector3(Random.Range(-180f, 180f), Random.Range(-180f, 180f), Random.Range(-180f, 180f));
            cubeParameters[i].scale = Random.Range(0.1f, 1f);
            cubeParameters[i].randTime = Random.Range(-Mathf.PI * 2, Mathf.PI * 2);
            cubeParameters[i].baseHeight = cubeParameters[i].position.y;

        }

        // Marshal.SizeOfで構造体CubeParameterのサイズを取得する
        m_cubeParamBuffer = new ComputeBuffer(m_instanceCount, Marshal.SizeOf(typeof(CubeParameter)));

        m_cubeParamBuffer.SetData(cubeParameters);

        m_material.SetBuffer("cubeParamBuffer", m_cubeParamBuffer);
    }

    readonly private int m_TimeId = Shader.PropertyToID("_Time");

    private void UpdatePositionAndAngle() {

        m_computeShader.SetFloat(m_TimeId, Time.deltaTime);
        m_computeShader.Dispatch(m_kernelID, m_groupSize.x, m_groupSize.y, m_groupSize.z);

    }


    private void OnDisable() {

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

        m_argsBuffer = null;

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

        m_cubeParamBuffer = null;

    }

}


Shader

Shader "Custom/InstancedWithMove"
{
    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;

        struct Input
        {
            float2 uv_MainTex;
        };

        struct CubeParameter {
            float3 position;
            float3 angle;
            float scale;
            float randTime;
            float baseHeight;
        };

#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
    StructuredBuffer<CubeParameter> cubeParamBuffer;
#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
            float3 position = cubeParamBuffer[unity_InstanceID].position;
            float3 angle = cubeParamBuffer[unity_InstanceID].angle;
            float scale = cubeParamBuffer[unity_InstanceID].scale;

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

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

            // 座標
            unity_ObjectToWorld._14_24_34_44 = float4(position, 1);

            // モデル行列を求める(間違っているかも. . .)
            // 参考:https://qiita.com/yuji_yasuhara/items/8d63455d1d277af4c270
            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 /= scale * scale;
            unity_WorldToObject._21_22_23 /= scale * scale;
            unity_WorldToObject._31_32_33 /= scale * scale;
        #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"
}


ComputeShader

#pragma kernel ChangeCubeParameter

struct CubeParameter {
    float3 position;
    float3 angle;
    float scale;
    float randTime;
    float baseHeight;
};

RWStructuredBuffer<CubeParameter> cubeParamBuffer;

float _Time;
float rotateSpeed;
float moveSpeed;
float moveHeight;


[numthreads(64, 1, 1)]
void ChangeCubeParameter(uint id : SV_DISPATCHTHREADID) {

    cubeParamBuffer[id.x].angle.x += rotateSpeed * _Time;

    float time = cubeParamBuffer[id.x].randTime + _Time * moveSpeed;
    cubeParamBuffer[id.x].randTime = time;

    float baseHeight = cubeParamBuffer[id.x].baseHeight;

    cubeParamBuffer[id.x].position.y = baseHeight + sin(time) * moveHeight;

}



結果

解説が少し長くなるので、先に結果をお見せします

f:id:vxd-naoshi-19961205-maro:20200826224530g:plain



解説

今回はシェーダーについては解説しません。興味がある方は前回の記事を参照してください。


構造体の定義、使用

今回のプログラムでは複数の値を渡すため、構造体を使用します。

C#プログラムでは次のように構造体CubeParameterが定義されます。

    struct CubeParameter {
        public Vector3 position;
        public Vector3 angle;
        public float scale;
        public float randTime;
        public float baseHeight;
    }


次にこの構造体と同じ構造体をシェーダー側とComputeShader側にも定義します。

ここでC#の構造体とメンバ変数の型や個数が異なれば、エラーになりませんが正しく計算が行われずバグになります。

        // シェーダー側
        struct CubeParameter {
            float3 position;
            float3 angle;
            float scale;
            float randTime;
            float baseHeight;
        };

#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
    StructuredBuffer<CubeParameter> cubeParamBuffer;
#endif
// ComputeShader側
struct CubeParameter {
    float3 position;
    float3 angle;
    float scale;
    float randTime;
    float baseHeight;
};

RWStructuredBuffer<CubeParameter> cubeParamBuffer;



構造体の初期化

今回も前回と同様にランダムにcubeを生成したいので座標をランダムにします。

また、上下に動かす動きをcubeごとに別々にしたかったのでアニメーション時に使用する時間パラメータもランダムにします。

構造体のComputeBufferを作る際はsizeof関数ではなく、Marshal.SizeOf(typeof(構造体名))で行うと構造体のサイズを調べることが出来ます。

    private void InitializeCubeParamBuffer() {

        CubeParameter[] cubeParameters = new CubeParameter[m_instanceCount];

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

            cubeParameters[i].position = new Vector3(Random.Range(-100f, 100f), Random.Range(-100f, 100f), Random.Range(-100f, 100f));
            cubeParameters[i].angle = new Vector3(Random.Range(-180f, 180f), Random.Range(-180f, 180f), Random.Range(-180f, 180f));
            cubeParameters[i].scale = Random.Range(0.1f, 1f);
            cubeParameters[i].randTime = Random.Range(-Mathf.PI * 2, Mathf.PI * 2);
            cubeParameters[i].baseHeight = cubeParameters[i].position.y;

        }

        // Marshal.SizeOfで構造体CubeParameterのサイズを取得する
        m_cubeParamBuffer = new ComputeBuffer(m_instanceCount, Marshal.SizeOf(typeof(CubeParameter)));

        m_cubeParamBuffer.SetData(cubeParameters);

        m_material.SetBuffer("cubeParamBuffer", m_cubeParamBuffer);
    }



ComputeShaderの初期化

ComputeShaderの初期化は次の順序で行われています。

  1. カーネルIDを取得する
  2. 実行する際のグループサイズを調べる
  3. パラメータを入れる(毎フレーム入れる必要がないもの)
    private void InitializeComputeShader() {

        // カーネルIDの取得
        m_kernelID = m_computeShader.FindKernel("ChangeCubeParameter");

        // グループサイズを求める
        m_computeShader.GetKernelThreadGroupSizes(m_kernelID, out uint x, out uint y, out uint z);
        m_groupSize = new Vector3Int((int)x, (int)y, (int)z);
        m_groupSize.x = m_instanceCount / m_groupSize.x;

        // パラメータをComputeShaderに設定
        m_computeShader.SetBuffer(m_kernelID, "cubeParamBuffer", m_cubeParamBuffer);
        m_computeShader.SetFloat("moveSpeed", m_moveSpeed);
        m_computeShader.SetFloat("rotateSpeed", m_rotateSpeed);
        m_computeShader.SetFloat("moveHeight", m_moveHeight);

    }



ComputeShaderの実行、描画

今回はUpdate関数でComputeShaderを実行して、LateUpdate関数で描画を行いました。

サイトによってはUpdate関数に両方実行しているのもあったので、好きなほうでいいと思います。(間違いがあれば訂正します)

    void Update()
    {
        UpdatePositionAndAngle();
    }

    void LateUpdate() {

        Graphics.DrawMeshInstancedIndirect(
            m_mesh,
            0,
            m_material,
            m_bounds,
            m_argsBuffer,
            0,
            null,
            m_shadowCastingMode,
            m_receiveShadows
        );

    }


ComputeShaderを実行する前に、Time.deltaTimeの値を渡すことに注意してください。

    private void UpdatePositionAndAngle() {

        m_computeShader.SetFloat(m_TimeId, Time.deltaTime);
        m_computeShader.Dispatch(m_kernelID, m_groupSize.x, m_groupSize.y, m_groupSize.z);

    }



cubeの回転、上下に動かす

cubeを回転させるときはx軸に対して回転させています。

上下に動かす処理は、基準になる高さにsin値 * 上下させる高さを足して高さを求めています。

void ChangeCubeParameter(uint id : SV_DISPATCHTHREADID) {

    cubeParamBuffer[id.x].angle.x += rotateSpeed * _Time;

    float time = cubeParamBuffer[id.x].randTime + _Time * moveSpeed;
    cubeParamBuffer[id.x].randTime = time;

    float baseHeight = cubeParamBuffer[id.x].baseHeight;

    cubeParamBuffer[id.x].position.y = baseHeight + sin(time) * moveHeight;

}



まとめ

ずっと勉強したかった内容をやっとまとめることが出来ました。

今回は特に変わったことはしていないので、Boidsアルゴリズムだったり興味があるものをDrawMeshInstancedIndirect+ComputeShaderで実装したいと思います。

恐らくもうちょっと続くと思うのでよろしくお願いします。



参考

gottaniprogramming.seesaa.net

docs.microsoft.com