DrawMeshInstancedProceduralは、Unity 6では使用できません。
公式からGraphics.RenderMeshPrimitivesの使用が推奨されています。
はじめに
こんにちは、かき氷です。今回はUnityの大量描画の一つを紹介します。
難易度が非常に高い訳ではありませんが、これ以上に難しいものを学ぶ時必ず必要な知識を使います。なるべく通っておきましょう。
環境:Unity 2022.3
この記事はDrawMeshInstancedの知識を前提としています。
Unityマニュアル:https://docs.unity3d.com/ja/2020.3/ScriptReference/Graphics.DrawMeshInstanced.html
DrawMeshInstancedProceduralとは
DrawMeshInstancedProceduralとは、
簡潔に言うとShaderの自作が必要なmesh描画方法です。
DrawMeshInstancedと違い、1024個の個数制限はなく数万個に及ぶ描画でも負荷が抑えられる利点があります。
今回はURPです
上記の通り、C#とShader両方の記述が必要です。それぞれ例のコードを添付します。
C#側のコード
using UnityEngine;
public class ProceduralQuadDrawWithSize : MonoBehaviour
{
public Material material;
public int instanceCount = 100;
private float[] sizes;
private Mesh mesh;
private Bounds bounds;
private GraphicsBuffer _sizeBuffer;
void Awake()
{
mesh = CreateQuadMesh();
bounds = new Bounds(Vector3.zero, Vector3.one * 1000f);
// インスタンスごとのサイズ配列
sizes = new float[instanceCount];
for (int i = 0; i < instanceCount; i++)
{
sizes[i] = Random.Range(0.3f, 1.0f);
}
}
void Update()
{
Graphics.DrawMeshInstancedProcedural(mesh,0,material,bounds,instanceCount);
}
private void OnEnable()
{
_sizeBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured,instanceCount,sizeof(float));
_sizeBuffer.SetData(sizes);
material.SetBuffer("_SizeBuffer", _sizeBuffer);
}
void OnDisable()
{
_sizeBuffer.Dispose();
}
Mesh CreateQuadMesh()
{
var m = new Mesh();
m.vertices = new Vector3[]
{
new Vector3(-0.5f, -0.5f, 0),
new Vector3( 0.5f, -0.5f, 0),
new Vector3(-0.5f, 0.5f, 0),
new Vector3( 0.5f, 0.5f, 0),
};
m.triangles = new int[]
{
0, 2, 1,
2, 3, 1
};
m.RecalculateNormals();
return m;
}
}
このコードの流れ
①material(shader)を入手
②mesh・boundsを設定
③shaderに渡す情報を設定(今回はサイズ)
④bufferを設定・データをセット
⑤Graphics.DrawMeshInstancedProceduralで描画指示
一つずつ確認していきます。
①materialを入手
特に注意すべきところはありません。Unity側でmaterialとUnlitシェーダーを用意しておきましょう。
②mesh・boundsを設定
ここでは、meshの設定を行っています。
頂点データ(vertices)・ポリゴン(triangles)・法線の有無(RecalculateNormals())などを設定します。
Mesh CreateQuadMesh()
{
var m = new Mesh();
m.vertices = new Vector3[]
{
new Vector3(-0.5f, -0.5f, 0),
new Vector3( 0.5f, -0.5f, 0),
new Vector3(-0.5f, 0.5f, 0),
new Vector3( 0.5f, 0.5f, 0),
};
m.triangles = new int[]
{
0, 2, 1,
2, 3, 1
};
m.RecalculateNormals();
return m;
}
boundsとは、簡単に言うと描画したものが見える範囲です。広いほど広範囲のものが描画されますが、その分負荷がかかります。
1つ目の要素が中心点、二つ目が範囲です。
bounds = new Bounds(Vector3.zero, Vector3.one * 1000f);
③shaderに渡す情報を設定
単純明快、描画個数分ランダムにサイズを振り分けている
sizes = new float[instanceCount];
for (int i = 0; i < instanceCount; i++)
{
sizes[i] = Random.Range(0.3f, 1.0f);
}
④bufferを設定・データをセット
OnEnable()は、このコンポーネントが有効になった時呼ばれるものです。
Awakeより後、Startより先に呼び出されます。
new GraphicsBufferでbufferを設定しています。
左から(Bufferタイプ,データの個数,一つ分のデータサイズ)です。
_sizeBuffer.SetData(sizes);にて設定してbufferに任意のデータを入れています。今回はsizes配列です。
そして、material.SetBufferでshaderへbufferデータを送っています。
private void OnEnable()
{
_sizeBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured,instanceCount,sizeof(float));
_sizeBuffer.SetData(sizes);
material.SetBuffer("_SizeBuffer", _sizeBuffer);
}
bufferを扱う際は、このメソッドを必ず入れてください。
使うbufferの数だけbufferの名前.Dispose();を書きましょう。
void OnDisable()
{
_sizeBuffer.Dispose();
}
⑤Graphics.DrawMeshInstancedProceduralで描画指示
最後に、Graphics.DrawMeshInstancedProceduralで描画指示をしています。
左から(mesh,何番目のmeshを使うか,material,bounds,描画する個数)
void Update()
{
Graphics.DrawMeshInstancedProcedural(mesh,0,material,bounds,instanceCount);
}
Shader側のコード
Shaderはそれだけで記事が書けるため、細かい説明は省きます。
Shader "Unlit/ProceduralInstancedWithSize"
{
SubShader
{
Pass
{
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
StructuredBuffer<float> _SizeBuffer;
struct Attributes
{
float4 positionOS : POSITION;
uint instancedID : SV_InstanceID;
};
struct Varyings
{
float4 positionCS : SV_POSITION;
uint instancedID : TEXCOORD0;
};
Varyings vert (Attributes IN)
{
Varyings OUT;
float size = _SizeBuffer[IN.instancedID];
// Quadをサイズでスケール
float3 posOS = IN.positionOS.xyz * size;
// InstanceIDで横に並べる
float3 offset = float3(IN.instancedID * 1.2, 0, 0);
posOS += offset;
OUT.positionCS = TransformObjectToHClip(posOS);
OUT.instancedID = IN.instancedID;
return OUT;
}
half4 frag (Varyings IN) : SV_Target
{
float t = frac(IN.instancedID * 0.1);
return half4(t, 0.8, 1.0 - t, 1.0);
}
ENDHLSL
}
}
}
URPのShaderを学び方は、こちらの記事をお勧めします。
引用:https://qiita.com/flankids/items/a92b14834792a10798e5
StructuredBuffer
ここだけ説明します。
StructuredBufferを使って、C#から送られてきたbufferデータを受け取ります。このまま配列として使うこともできます。
StructuredBuffer<型> bufferの名前;
必ずC#側とbufferの名前を揃えて下さい。
結果
これで、大量のmeshを描画することができます!
100個程度ではほぼ負荷がないと言っていいほど軽量です。

最後に
今回は DrawMeshInstancedProcedural を紹介しました。
Unity にある程度慣れてきたときに直面しがちな「軽量化」という壁を超える一つの知識になれば幸いです。
しかし
世の中にはこれを超える方法がいくつも存在します。軽量化の道は長い!
次に学ぶものはDrawMeshInstancedIndirectがお勧めです。
