今回は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),
};
}
_boidBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, boidArray.Length, Marshal.SizeOf<BoidState>());
_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);
BoidComputeShader.SetInt("numBoids", boidCount);
_kernelIndex = BoidComputeShader.FindKernel("CSMain");
BoidComputeShader.GetKernelThreadGroupSizes(_kernelIndex, out var x, out var y, out var z);
BoidComputeShader.SetBuffer(_kernelIndex, "boidBuffer", _boidBuffer);
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と名付けました。
_boidBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, boidArray.Length, Marshal.SizeOf<BoidState>());
_boidBuffer.SetData(boidArray);
UpdateBoidsで必要な情報やboidBufferをComputeShaderに渡して、実行しています。
BoidComputeShader.SetBuffer(_kernelIndex, "boidBuffer", _boidBuffer);
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),
};
}
_boidBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, boidArray.Length, Marshal.SizeOf<BoidState>());
_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);
BoidComputeShader.SetInt("numBoids", boidCount);
_kernelIndex = BoidComputeShader.FindKernel("CSMain");
BoidComputeShader.GetKernelThreadGroupSizes(_kernelIndex, out var x, out var y, out var z);
BoidComputeShader.SetBuffer(_kernelIndex, "boidBuffer", _boidBuffer);
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