Graphics.DrawMeshInstancedIndirect/Procedural

0. What is DMII?

1. The Shader

Shader "DMIIShader" {
Properties {
_MainTex("Texture", 2D) = "white" {}
}
SubShader {
Tags {
"Queue" = "Transparent"
}
Cull Off
Lighting Off
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha

Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_instancing
#include "UnityCG.cginc"
#pragma instancing_options procedural:setup

struct vertex {
float4 loc : POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct fragment {
float4 loc : SV_POSITION;
float2 uv : TEXCOORD0;
};

CBUFFER_START(MyData)
float4 posDirBuffer[7];
CBUFFER_END

#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
void setup() {
float2 position = posDirBuffer[unity_InstanceID].xy;
float2 direction = posDirBuffer[unity_InstanceID].zw;

unity_ObjectToWorld = float4x4(
direction.x, -direction.y, 0, position.x,
direction.y, direction.x, 0, position.y,
0, 0, 1, 0,
0, 0, 0, 1
);
}
#endif

sampler2D _MainTex;
float _FadeInT; //We'll use this later

fragment vert(vertex v) {
fragment f;
UNITY_SETUP_INSTANCE_ID(v);
f.loc = UnityObjectToClipPos(v.loc);
f.uv = v.uv;
//f.uv = TRANSFORM_TEX(v.uv, _MainTex);
return f;
}

float4 frag(fragment f) : SV_Target{
float4 c = tex2D(_MainTex, f.uv);
return c;
}
ENDCG
}
}
}
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_instancing
#include "UnityCG.cginc"
#pragma instancing_options procedural:setup
struct vertex {
float4 loc : POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
CBUFFER_START(MyData)
float4 posDirBuffer[7];
float timeBuffer[7];
CBUFFER_END
#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
void setup() {
float2 position = posDirBuffer[unity_InstanceID].xy;
float2 direction = posDirBuffer[unity_InstanceID].zw;

unity_ObjectToWorld = float4x4(
direction.x, -direction.y, 0, position.x,
direction.y, direction.x, 0, position.y,
0, 0, 1, 0,
0, 0, 0, 1
);
}
#endif
fragment vert(vertex v) {
UNITY_SETUP_INSTANCE_ID(v);
fragment f;
f.loc = UnityObjectToClipPos(v.loc);
f.uv = v.uv;
//f.uv = TRANSFORM_TEX(v.uv, _MainTex);
f.c = float4(1.0, 1.0, 1.0, 1.0);
return f;
}

2. Mesh and Material

public readonly struct RenderInfo {
private static readonly int MainTexPropertyId = Shader.PropertyToID("_MainTex");
public readonly Mesh mesh;
public readonly Material mat;

public RenderInfo(Mesh m, Material material) {
mesh = m;
mat = material;
}

public static RenderInfo FromSprite(Material baseMaterial, Sprite s) {
var renderMaterial = UnityEngine.Object.Instantiate(baseMaterial);
renderMaterial.enableInstancing = true;
renderMaterial.SetTexture(MainTexPropertyId, s.texture);
Mesh m = new Mesh {
vertices = s.vertices.Select(v => (Vector3)v).ToArray(),
triangles = s.triangles.Select(t => (int)t).ToArray(),
uv = s.uv
};
return new RenderInfo(m, renderMaterial);
}
}

3. An Object Manager

3.1 An Object

public class FObject {
private static readonly Random r = new Random();
public Vector2 position;
public readonly float scale;
private readonly Vector2 velocity;
public float rotation;
private readonly float rotationRate;
public float time;

public FObject() {
position = new Vector2((float)r.NextDouble() * 10f - 5f, (float)r.NextDouble() * 8f - 4f);
velocity = new Vector2((float)r.NextDouble() * 0.4f - 0.2f, (float)r.NextDouble() * 0.4f - 0.2f);
rotation = (float)r.NextDouble();
rotationRate = (float)r.NextDouble() * 0.6f - 0.2f;
scale = 0.6f + (float) r.NextDouble() * 0.8f;
time = (float) r.NextDouble() * 6f;
}

public void DoUpdate(float dT) {
position += velocity * dT;
rotation += rotationRate * dT;
time += dT;
}
}

3.2 A Manager

private static readonly int posDirPropertyId = Shader.PropertyToID("posDirBuffer");
private static readonly int timePropertyId = Shader.PropertyToID("timeBuffer");

private MaterialPropertyBlock pb;
private readonly Vector4[] posDirArr = new Vector4[batchSize];
private readonly float[] timeArr = new float[batchSize];
private const int batchSize = 7;
public int instanceCount;

public Sprite sprite;
public Material baseMaterial;
private RenderInfo ri;
public string layerRenderName;
private int layerRender;
private FObject[] objects;
...
private void Start() {
pb = new MaterialPropertyBlock();
layerRender = LayerMask.NameToLayer(layerRenderName);
ri = RenderInfo.FromSprite(baseMaterial, sprite);
Camera.onPreCull += RenderMe;
objects = new FObject[instanceCount];
for (int ii = 0; ii < instanceCount; ++ii) {
objects[ii] = new FObject();
}
}

private void Update() {
float dT = Time.deltaTime;
for (int ii = 0; ii < instanceCount; ++ii) {
objects[ii].DoUpdate(dT);
}
}
private void RenderMe(Camera c) {
if (!Application.isPlaying) { return; }
for (int done = 0; done < instanceCount; done += batchSize) {
int run = Math.Min(instanceCount - done, batchSize);
for (int batchInd = 0; batchInd < run; ++batchInd) {
var obj = objects[done + batchInd];
posDirArr[batchInd] = new Vector4(obj.position.x, obj.position.y,
Mathf.Cos(obj.rotation) * obj.scale, Mathf.Sin(obj.rotation) * obj.scale);
timeArr[batchInd] = obj.time;
}
pb.SetVectorArray(posDirPropertyId, posDirArr);
pb.SetFloatArray(timePropertyId, timeArr);
CallRender(c, run);
}
}
private void CallRender(Camera c, int count) {
Graphics.DrawMeshInstancedProcedural(ri.mesh, 0, ri.mat,
bounds: new Bounds(Vector3.zero, Vector3.one * 1000f),
count: count,
properties: pb,
castShadows: ShadowCastingMode.Off,
receiveShadows: false,
layer: layerRender,
camera: c);
}

4. Adding a Feature: Fade-In Time

Properties {
_MainTex("Texture", 2D) = "white" {}
_FadeInT("Fade in time", Float) = 10 // New
}
CBUFFER_START(MyData)
float4 posDirBuffer[7];
float timeBuffer[7]; // New
CBUFFER_END
struct fragment {
float4 loc : SV_POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID // New
};
fragment vert(vertex v) {
fragment f;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_TRANSFER_INSTANCE_ID(v, f); // New
f.loc = UnityObjectToClipPos(v.loc);
f.uv = v.uv;
//f.uv = TRANSFORM_TEX(v.uv, _MainTex);
return f;
}
float4 frag(fragment f) : SV_Target{
UNITY_SETUP_INSTANCE_ID(f); // New
float4 c = tex2D(_MainTex, f.uv);
return c;
}
float _FadeInT;                                                         // New

float4 frag(fragment f) : SV_Target{
UNITY_SETUP_INSTANCE_ID(f);
float4 c = tex2D(_MainTex, f.uv);
#if defined(UNITY_PROCEDURAL_INSTANCING_ENABLED) || defined(UNITY_INSTANCING_ENABLED) // New
c.a *= smoothstep(0.0, _FadeInT, timeBuffer[unity_InstanceID]); // New
#endif // New
return c;
}

5. But What If My Computer Is From 2005?

private readonly Vector4[] posDirArr = new Vector4[batchSize];
private readonly float[] timeArr = new float[batchSize];
private readonly Matrix4x4[] posMatrixArr = new Matrix4x4[batchSize]; //New
private void RenderMe(Camera c) {
if (!Application.isPlaying) { return; }
for (int done = 0; done < instanceCount; done += batchSize) {
int run = Math.Min(instanceCount - done, batchSize);
for (int batchInd = 0; batchInd < run; ++batchInd) {
var obj = objects[done + batchInd];
//posDirArr[batchInd] = new Vector4(obj.position.x, obj.position.y,
// Mathf.Cos(obj.rotation) * obj.scale, Mathf.Sin(obj.rotation) * obj.scale);
timeArr[batchInd] = obj.time;
ref var m = ref posMatrixArr[batchInd];

m.m00 = m.m11 = Mathf.Cos(obj.rotation) * obj.scale;
m.m01 = -(m.m10 = Mathf.Sin(obj.rotation) * obj.scale);
m.m22 = m.m33 = 1;
m.m03 = obj.position.x;
m.m13 = obj.position.y;
}
//pb.SetVectorArray(posDirPropertyId, posDirArr);
pb.SetFloatArray(timePropertyId, timeArr);
//CallRender(c, run);
CallLegacyRender(c, run);
}
}
//Use this for legacy GPU support or WebGL support
private void CallLegacyRender(Camera c, int count) {
Graphics.DrawMeshInstanced(ri.mesh, 0, ri.mat,
posMatrixArr,
count: count,
properties: pb,
castShadows: ShadowCastingMode.Off,
receiveShadows: false,
layer: layerRender,
camera: c);
}
#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
void setup() {
float2 position = posDirBuffer[unity_InstanceID].xy;
float2 direction = posDirBuffer[unity_InstanceID].zw;
direction *= smoothstep(0, 10, timeBuffer[unity_InstanceID]); //New

unity_ObjectToWorld = float4x4(
direction.x, -direction.y, 0, position.x,
direction.y, direction.x, 0, position.y,
0, 0, 1, 0,
0, 0, 0, 1
);
}
#endif
//Clone of HLSL smoothstep
private float Smoothstep(float low, float high, float t) {
t = Mathf.Clamp01((t - low) / (high - low));
return t * t * (3 - 2 * t);
}

private void RenderMe(Camera c) {
if (!Application.isPlaying) { return; }
for (int done = 0; done < instanceCount; done += batchSize) {
int run = Math.Min(instanceCount - done, batchSize);
for (int batchInd = 0; batchInd < run; ++batchInd) {
var obj = objects[done + batchInd];
posDirArr[batchInd] = new Vector4(obj.position.x, obj.position.y,
Mathf.Cos(obj.rotation) * obj.scale, Mathf.Sin(obj.rotation) * obj.scale);
timeArr[batchInd] = obj.time;
ref var m = ref posMatrixArr[batchInd];

var scale = obj.scale * Smoothstep(0, 10, obj.time); //New
m.m00 = m.m11 = Mathf.Cos(obj.rotation) * scale; //Changed
m.m01 = -(m.m10 = Mathf.Sin(obj.rotation) * scale); //Changed
m.m22 = m.m33 = 1;
m.m03 = obj.position.x;
m.m13 = obj.position.y;
}
pb.SetVectorArray(posDirPropertyId, posDirArr);
pb.SetFloatArray(timePropertyId, timeArr);
//CallRender(c, run);
CallLegacyRender(c, run);
}
}

6. Annoying Details

#ifdef FT_FRAME_ANIM
f.uv.x = (f.uv.x + trunc(fmod(timeBuffer[unity_InstanceID] * _InvFrameT, _Frames))) / _Frames;
#endif

Conclusion

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store