始めに
前回はDrawMeshInstancedIndirectについてまとめました。
今回は前回の内容とComputeShaderを組み合わせた簡単なサンプルを作成したいと思います。
ComputeShaderについては以下の記事にまとめています。
プログラム
今回のプログラムは前回のプログラムに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;
}
結果
解説が少し長くなるので、先に結果をお見せします
解説
今回はシェーダーについては解説しません。興味がある方は前回の記事を参照してください。
構造体の定義、使用
今回のプログラムでは複数の値を渡すため、構造体を使用します。
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の初期化は次の順序で行われています。
- カーネルIDを取得する
- 実行する際のグループサイズを調べる
- パラメータを入れる(毎フレーム入れる必要がないもの)
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で実装したいと思います。
恐らくもうちょっと続くと思うのでよろしくお願いします。