始めに
今回ブログを担当するなおしです。
GPUインスタンシング(DrawMeshInstancedIndirect)とComputeShaderを組み合わせた簡単なサンプルを今回作成していこうと思います。
これらを使うとたくさんのメッシュに動きを加えることが出来るのでとても楽しいです。
GPUインスタンシング
詳しくは解説しないので知りたいという方は以下の記事を参考にしてみてください。
ComputeShader
こちらもここでは解説しませんので、詳しく知りたい方はこちらの記事を参考にすると良いと思います。
今回作るもの
範囲を決めてその中を反射し続けるパーティクルを作成します。
プログラム
プログラムはC#プログラム、ComputeShader、Shaderの3つからなります。
C#プログラム
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using System.Runtime.InteropServices;
public class BallRenderer : 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;
[Space(20)]
[SerializeField]
private int m_instanceCount;
[SerializeField]
private ComputeShader m_particleCalclator;
[Header("パーティクル設定")]
[Space(20)]
[SerializeField]
private float m_range;
[SerializeField]
private float m_scale;
[SerializeField]
private Color m_color;
[SerializeField]
private float m_particleVelocity;
private int m_calcParticlePositionKernel;
private Vector3Int m_calcParticlePositionGroupSize;
private ComputeBuffer m_argsBuffer;
private ComputeBuffer m_particleBuffer;
private ComputeBuffer m_particleVelocityBuffer;
private readonly int m_DeltaTimePropId = Shader.PropertyToID("_DeltaTime");
struct Particle {
public Vector3 position;
public Vector4 color;
public float scale;
}
// Start is called before the first frame update
void Start()
{
InitializeArgsBuffer();
InitializeParticleBuffer();
InitializeVelocityBuffer();
SetUpParticleCalclator();
}
void Update() {
m_particleCalclator.SetFloat(m_DeltaTimePropId, Time.deltaTime);
m_particleCalclator.Dispatch(m_calcParticlePositionKernel,
m_calcParticlePositionGroupSize.x,
m_calcParticlePositionGroupSize.y,
m_calcParticlePositionGroupSize.z);
}
// Update is called once per frame
void LateUpdate()
{
Graphics.DrawMeshInstancedIndirect(
m_mesh,
0,
m_instanceMaterial,
m_bounds,
m_argsBuffer,
0,
null,
m_shadowCastingMode,
m_receiveShadows
);
}
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 InitializeParticleBuffer() {
Particle[] particles = new Particle[m_instanceCount];
for(int i = 0; i < m_instanceCount; ++i) {
particles[i].position = RandomVector(-m_range, m_range);
particles[i].color = m_color;
particles[i].scale = m_scale;
}
m_particleBuffer = new ComputeBuffer(m_instanceCount, Marshal.SizeOf(typeof(Particle)));
m_particleBuffer.SetData(particles);
m_instanceMaterial.SetBuffer("_ParticleBuffer", m_particleBuffer);
}
private void InitializeVelocityBuffer() {
Vector3[] velocities = new Vector3[m_instanceCount];
for(int i = 0; i < m_instanceCount; ++i) {
// Random.onUnitySphere:半径1の球面上のランダムな点を返す
// つまり、大きさm_particleVelocityのランダムなベクトルを計算
velocities[i] = Random.onUnitSphere * m_particleVelocity;
}
m_particleVelocityBuffer = new ComputeBuffer(m_instanceCount, Marshal.SizeOf(typeof(Vector3)));
m_particleVelocityBuffer.SetData(velocities);
}
private void SetUpParticleCalclator() {
m_calcParticlePositionKernel = m_particleCalclator.FindKernel("CalcParticlePosition");
m_particleCalclator.GetKernelThreadGroupSizes(m_calcParticlePositionKernel,
out uint x,
out uint y,
out uint z);
m_calcParticlePositionGroupSize = new Vector3Int(m_instanceCount/(int)x, (int)y, (int)z);
m_particleCalclator.SetFloat("_Range", m_range);
m_particleCalclator.SetBuffer(m_calcParticlePositionKernel, "_Particle", m_particleBuffer);
m_particleCalclator.SetBuffer(m_calcParticlePositionKernel, "_ParticleVelocity", m_particleVelocityBuffer);
}
private Vector3 RandomVector(float min, float max) {
return new Vector3(
Random.Range(min, max),
Random.Range(min, max),
Random.Range(min, max)
);
}
// 領域の解放
private void OnDisable() {
m_particleBuffer?.Release();
m_particleVelocityBuffer?.Release();
m_argsBuffer?.Release();
}
}
ComputeShader
#pragma kernel CalcParticlePosition
struct Particle {
float3 position;
float4 color;
float scale;
};
RWStructuredBuffer<Particle> _Particle;
RWStructuredBuffer<float3> _ParticleVelocity;
float _Range;
float _DeltaTime;
[numthreads(64, 1, 1)]
void CalcParticlePosition(uint id : SV_DISPATCHTHREADID) {
float3 pos = _Particle[id].position + _ParticleVelocity[id] * _DeltaTime;
if(abs(pos.x) > _Range) {
_ParticleVelocity[id].x *= -1;
pos.x = _Particle[id].position.x + _ParticleVelocity[id].x * _DeltaTime;
}
if(abs(pos.y) > _Range) {
_ParticleVelocity[id].y *= -1;
pos.y = _Particle[id].position.y + _ParticleVelocity[id].y * _DeltaTime;
}
if(abs(pos.z) > _Range) {
_ParticleVelocity[id].z *= -1;
pos.z = _Particle[id].position.z + _ParticleVelocity[id].z * _DeltaTime;
}
_Particle[id].position = pos;
}
Shader
Shader "Unlit/Particle"
{
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct Particle
{
float3 position;
float4 color;
float scale;
};
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float4 vertex : SV_POSITION;
float4 color : COLOR;
};
StructuredBuffer<Particle> _ParticleBuffer;
v2f vert (appdata v, uint instanceId : SV_InstanceID)
{
Particle p = _ParticleBuffer[instanceId];
v2f o;
float3 pos = (v.vertex.xyz * p.scale) + p.position;
o.vertex = mul(UNITY_MATRIX_VP, float4(pos, 1.0));
o.color = p.color;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return i.color;
}
ENDCG
}
}
}
簡単な解説
始めにStart関数内でDrawMeshInstancedIndirectで使用するComputeBuffer、パーティクル情報のComputeBuffer、パーティクルの速度ベクトルのComputeBuffer、ComputeShaderの初期化を行います。
ComputeShaderではパーティクル情報と速度ベクトルのComputeBufferが必要となるのでそれらの初期化が完了してから初期化を行います。
// Start is called before the first frame update void Start() { InitializeArgsBuffer(); InitializeParticleBuffer(); InitializeVelocityBuffer(); SetUpParticleCalclator(); }
Update関数では毎フレームのDeltaTimeをComputeShaderに設定してからそれぞれのパーティクルの座標をGPUで計算します。
void Update() { m_particleCalclator.SetFloat(m_DeltaTimePropId, Time.deltaTime); m_particleCalclator.Dispatch(m_calcParticlePositionKernel, m_calcParticlePositionGroupSize.x, m_calcParticlePositionGroupSize.y, m_calcParticlePositionGroupSize.z); }
パーティクルの位置の更新ですが、一度速度ベクトル*DeltaTimeを座標に足し合わせます。
そのとき、範囲外に出ようとしていた場合は速度速度ベクトルをマイナスにしてからもう一度計算を行っています。
[numthreads(64, 1, 1)] void CalcParticlePosition(uint id : SV_DISPATCHTHREADID) { float3 pos = _Particle[id].position + _ParticleVelocity[id] * _DeltaTime; if(abs(pos.x) > _Range) { _ParticleVelocity[id].x *= -1; pos.x = _Particle[id].position.x + _ParticleVelocity[id].x * _DeltaTime; } if(abs(pos.y) > _Range) { _ParticleVelocity[id].y *= -1; pos.y = _Particle[id].position.y + _ParticleVelocity[id].y * _DeltaTime; } if(abs(pos.z) > _Range) { _ParticleVelocity[id].z *= -1; pos.z = _Particle[id].position.z + _ParticleVelocity[id].z * _DeltaTime; } _Particle[id].position = pos; }
あとはLateUpdateでパーティクルの描画を行います。このサンプルではDrawMeshInstancedIndirectを使って描画を行います。
// Update is called once per frame void LateUpdate() { Graphics.DrawMeshInstancedIndirect( m_mesh, 0, m_instanceMaterial, m_bounds, m_argsBuffer, 0, null, m_shadowCastingMode, m_receiveShadows ); }
最後に
これらを使うことで沢山のオブジェクトを描画して様々な動きを付加することが出来ます。
まだ私も勉強中ですが、これからも何か面白いものが出来るか試していきたいです。