自己紹介
Unity公式オフラインイベント「U/DAY TOKYO」に参加して、モチベ爆上がりの民
U/DAYはアーカイブが公開されるらしいので気になる人は以下のツイートからどうぞ
(写真に私います)
ほんだーい!
今回はDrawMesh系のメソッド群に代わる新たなメソッド群RenderMeshにおけるRenderMeshIndirect
にフォーカスしつつ、大学の講義でまつぼっくり螺旋というものを習ったのでそれを描画します。
あくまでもメインはRenderMeshIndirect
です。
注意
私の理解も浅いので、間違っている可能性もあるという前提で読んでください!
実装例としてみてくれると嬉しいです。
用語整理
RenderMesh系APIとは?
Unityは基本的に「MeshRenderer」というコンポーネントを利用して、オブジェクトを描画していきます。
つまりゲームオブジェクト1つに対して、1つのオブジェクトを描画することが基本なわけです。
しかし、RenderMesh系のAPIはゲームオブジェクトなしで非常に高速にオブジェクトを描画できます。
詳しくは「Unity Technologies Japan」公式の動画がありますので、ご覧ください。
RenderMeshIndirectとは?
RenderMesh系のメソッドの中でも特に自由度が高いものがRenderMeshIndirectです。
その代わりに独自のシェーダーと結構長めの準備が必要になってきます。
ComputeShaderとは?
ComputeShaderは通常のC#スクリプトでCPUに処理させるようなものを、GPUに処理をさせる機能です。
GPUはCPUに比べ並列処理がとても強いため、状況によっては非常に有効なものとなります。
詳しくはゆっち~さんのこちらの記事やインターネットで調べるとより詳細な話が出てきます。
JobSystem vs. ComputeShader
少し話はそれますが、UnityにはJobSystemというCPUに並列処理をさせる強力な機能が搭載されています。
ComputeShaderとどちらを選択するべきなのか、結論から言うとCPUとGPUボトルネックになっていないほうに負荷がかかるようにだそうです。
ほかにも様々な考慮すべき点があるとされていますが、これが一番分かりやすいのではないでしょうか。
(私がテクスチャサイズによって選ぶべき理由を理解していないのもある)
まつぼっくり螺旋とは?
まつぼっくり螺旋と言っていますが、「アルキメデス螺旋」といったほうが恐らく一般的です。
(まつぼっくりの方がキャッチーじゃん?)
本題とそれ過ぎるので、以下の記事などをご覧ください。
プログラムを書いていく
RenderMeshIndirect
の引数には聞きなれない型が多くあります。
RenderParams
やら、CommandBuffer
やら…ひとつずつ作っていきましょう。
RenderParams
RenderParams
はその名の通り、描画に関する情報を詰め込むクラスです。
マテリアルや、独自のシェーダーに渡したい情報などを設定していきます。
// PositionsはComputeShaderで計算している状態
var rp = new RenderParams(_material);
var positionsBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, (int)_spiralLength, Marshal.SizeOf<float3>());
positionsBuffer.SetData(positions);
rp.matProps = new();
rp.matProps.SetBuffer(PositionsID, positionsBuffer);
という感じです。
前提が多く抜けているので、全てを理解する必要はありません。
理解すべきと考えられるものを解説します。
コンストラクタ
コンストラクタでは、描画に使用するマテリアルを指定します。
マテリアルというか、シェーダーを指定する感覚に近いです。
なぜなら独自のシェーダーを持ったマテリアルを設定することになるからです。
GraphicsBuffer
GraphicsBuffer
はシェーダーに情報を送るためのバッファーです。
コンストラクタでは、情報を送るためのサイズなどを指定します。
(正直私もよく分かっていないところもあります。)
Marshal.SizeOf<float3>()
というものはsizeof(float)
と同様の働きです。
しかし、float3
はsizeof
が使えないので、このような書き方をします。
Mesh
MeshはMeshですね…
MeshっていうのはMeshなんですよ…
GraphicsBuffer
2度目の登場です。こちらのただこちらのGraphicsBuffer
はやることが決められています。
var commandData = new GraphicsBuffer.IndirectDrawIndexedArgs[1];
commandData[0].indexCountPerInstance = _mesh.GetIndexCount(0);
commandData[0].baseVertexIndex = _mesh.GetBaseVertex(0);
commandData[0].startIndex = _mesh.GetIndexStart(0);
commandData[0].instanceCount = 10;
var indirectBuf = new GraphicsBuffer(GraphicsBuffer.Target.IndirectArguments, 1, GraphicsBuffer.IndirectDrawIndexedArgs.size);
indirectBuf.SetData(commandData);
ここではメッシュの設定や生成するインスタンスの個数を設定していきます。
基本的には上のコピペでOKで、instanceCount
だけ調整してください。
その他
その他は基本的にデフォルトで大丈夫です。
シェーダーを用意する
そもそもMaterialを用意しなければなりません。
では、今回使ったシェーダープログラムを貼ります。
Shader "Custom/FibonacciSpiralShader"
{
SubShader
{
Tags
{
"RenderType"="Opaque" "RenderPipeline"="UniversalRenderPipeline"
}
LOD 200
Pass
{
HLSLPROGRAM
#define UNITY_INDIRECT_DRAW_ARGS IndirectDrawIndexedArgs
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fog
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl"
struct Varying
{
float4 vertex : POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Attributes
{
float4 pos : POSITION;
float4 color : COLOR0;
};
CBUFFER_START(UnityPerMaterial);
uniform StructuredBuffer<float3> _Positions;
CBUFFER_END;
Attributes vert(Varying i, uint instanceID : SV_InstanceID)
{
Attributes o;
// 色を変える(Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlslにマクロが用意されてる)
float colorValue = instanceID / 20.0f;
float3 hsvColor = float3(colorValue, 1, 1);
o.color = float4(HsvToRgb(hsvColor), 1);
// サイズを変える
i.vertex *= instanceID * 1.1 + 1;
// ポジションを指定
float4 wPos = TransformObjectToHClip(i.vertex + _Positions[instanceID]);
o.pos = wPos;
return o;
}
half4 frag(Attributes i) : SV_Target
{
return i.color;
}
ENDHLSL
}
}
}
ポイントはVaryingの中に定義してあるUNITY_VERTEX_INPUT_INSTANCE_ID
です。
これによって、vertex
の引数の中でinstanceID
が取得できるようになります。
また、_Positions
という変数でRenderParams
からの情報が入る形になっています。
全てのソースコード
using System.Runtime.InteropServices;
using Unity.Mathematics;
using UnityEngine;
namespace ComputeShading.FibonacciSpiral
{
public class FibonacciSpiral : MonoBehaviour
{
[SerializeField] private ComputeShader _computeShader;
[SerializeField] private float _theta;
[SerializeField] private float _radius;
[SerializeField] private uint _spiralLength;
[SerializeField] private Mesh _mesh;
[SerializeField] private Material _material;
private int _kernelIndex;
private GraphicsBuffer.IndirectDrawIndexedArgs[] _commandData;
private static readonly int Theta = Shader.PropertyToID("theta");
private static readonly int R = Shader.PropertyToID("r");
private static readonly int BufferID = Shader.PropertyToID("buffer");
private static readonly int PositionsID = Shader.PropertyToID("_Positions");
private static readonly int CountID = Shader.PropertyToID("_Count");
private void Start()
{
// ComputeShaderの用意
_kernelIndex = _computeShader.FindKernel("CalculateFibonacciSpiral");
// RenderMeshIndirectの用意
_commandData = new GraphicsBuffer.IndirectDrawIndexedArgs[1];
_commandData[0].indexCountPerInstance = _mesh.GetIndexCount(0);
_commandData[0].baseVertexIndex = _mesh.GetBaseVertex(0);
_commandData[0].startIndex = _mesh.GetIndexStart(0);
}
private void Update()
{
_computeShader.SetFloat(Theta, _theta);
_computeShader.SetFloat(R, _radius);
var positions = new float3[_spiralLength];
var buffer = new ComputeBuffer((int)_spiralLength, Marshal.SizeOf(typeof(float2)));
_computeShader.SetBuffer(_kernelIndex, BufferID, buffer);
uint sizeX, sizeY, sizeZ;
_computeShader.GetKernelThreadGroupSizes(
_kernelIndex,
out sizeX,
out sizeY,
out sizeZ
);
var threadGroupX = (int)System.Math.Ceiling(_spiralLength / (float)sizeX);
_computeShader.Dispatch(_kernelIndex, threadGroupX, 1, 1);
var result = new float2[_spiralLength];
buffer.GetData(result);
for (var i = 0; i < _spiralLength; i++)
{
positions[i] = new float3(result[i].x, result[i].y, 0);
}
var rp = new RenderParams(_material);
var positionsBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, (int)_spiralLength, Marshal.SizeOf<float3>());
positionsBuffer.SetData(positions);
rp.matProps = new();
rp.matProps.SetBuffer(PositionsID, positionsBuffer);
rp.matProps.SetInt(CountID, (int)_spiralLength);
rp.material.SetInt("_InstanceCount", (int)_spiralLength);
_commandData[0].instanceCount = _spiralLength;
var indirectBuf = new GraphicsBuffer(GraphicsBuffer.Target.IndirectArguments, 1, GraphicsBuffer.IndirectDrawIndexedArgs.size);
indirectBuf.SetData(_commandData);
Graphics.RenderMeshIndirect(rp, _mesh, indirectBuf);
}
}
}
#pragma kernel CalculateFibonacciSpiral
#define PI 3.14159265
RWStructuredBuffer<float2> buffer;
float theta;
float r;
[numthreads(4, 1, 1)]
void CalculateFibonacciSpiral(uint3 dispatchThreadID : SV_DispatchThreadID)
{
float rad = theta * dispatchThreadID.x * PI / 180;
buffer[dispatchThreadID.x].x = r * dispatchThreadID.x * cos(rad);
buffer[dispatchThreadID.x].y = r * dispatchThreadID.x * sin(rad);
}
Shader "Custom/FibonacciSpiralShader"
{
SubShader
{
Tags
{
"RenderType"="Opaque" "RenderPipeline"="UniversalRenderPipeline"
}
LOD 200
Pass
{
HLSLPROGRAM
#define UNITY_INDIRECT_DRAW_ARGS IndirectDrawIndexedArgs
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fog
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl"
struct Varying
{
float4 vertex : POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Attributes
{
float4 pos : POSITION;
float4 color : COLOR0;
};
CBUFFER_START(UnityPerMaterial);
uniform StructuredBuffer<float3> _Positions;
CBUFFER_END;
Attributes vert(Varying i, uint instanceID : SV_InstanceID)
{
Attributes o;
// 色を変える(Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlslにマクロが用意されてる)
float colorValue = instanceID / 20.0f;
float3 hsvColor = float3(colorValue, 1, 1);
o.color = float4(HsvToRgb(hsvColor), 1);
// サイズを変える
i.vertex *= instanceID * 1.1 + 1;
// ポジションを指定
float4 wPos = TransformObjectToHClip(i.vertex + _Positions[instanceID]);
o.pos = wPos;
return o;
}
half4 frag(Attributes i) : SV_Target
{
return i.color;
}
ENDHLSL
}
}
}
まとめ
RenderMeshIndirectとComputeShaderで良い感じにアルキメデス螺旋を作れました。
やったね!!
ちなみに
今回登場した、ゆっち~さんとUnity Technologies Japan公式の動画に出演した方全員とU/DAYでお話しできたので、ハッピーすぎます。
Computer Shader × RenderMesh(DrawMesh)
ほかにもComputer ShaderとRenderMesh系APIで遊んでいるリポジトリがあります。
どうぞ、ご覧ください。