【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]; ・ ・ ・