What is the best way to draw thousands of circles with minimum cpu. I need them for a unity selection system they have to be rendered on units feet.
Example of rendering one milion blue circles with single draw call (add CS cript to main camera).
using UnityEngine;
public class DrawCircles : MonoBehaviour
{
public Shader shader;
protected Material material;
void Start()
{
material = new Material(shader);
}
void OnRenderObject()
{
material.SetPass(0);
Graphics.DrawProcedural(MeshTopology.Triangles, 6 * 1000000, 1);
}
}
Shader:
Shader "Draw Circles"
{
Subshader
{
Pass
{
Cull Off
CGPROGRAM
#pragma vertex VSMain
#pragma fragment PSMain
#pragma target 5.0
float mod(float x, float y)
{
return x - y * floor(x/y);
}
float4 VSMain (uint id:SV_VertexID, out float2 UV : TEXCOORD1) : SV_POSITION
{
float q = floor(id / 6.0);
float3 center = float3(mod(q,1000.0),0.0, floor(q/1000.0));
center *= 2..xxx;
if (mod(float(id),6)==0)
{
UV = float2(0,0);
float3 worldPos = float3(-0.5,0,-0.5) + center;
return UnityObjectToClipPos(float4(worldPos,1.0));
}
else if (mod(float(id),6)==1)
{
UV = float2(1,0);
float3 worldPos = float3(0.5,0,-0.5) + center;
return UnityObjectToClipPos(float4(worldPos,1.0));
}
else if (mod(float(id),6)==2)
{
UV = float2(0,1);
float3 worldPos = float3(-0.5,0,0.5) + center;
return UnityObjectToClipPos(float4(worldPos,1.0));
}
else if (mod(float(id),6)==3)
{
UV = float2(1,0);
float3 worldPos = float3(0.5,0,-0.5) + center;
return UnityObjectToClipPos(float4(worldPos,1.0));
}
else if (mod(float(id),6)==4)
{
UV = float2(1,1);
float3 worldPos = float3(0.5,0,0.5) + center;
return UnityObjectToClipPos(float4(worldPos,1.0));
}
else
{
UV = float2(0,1);
float3 worldPos = float3(-0.5,0,0.5) + center;
return UnityObjectToClipPos(float4(worldPos,1.0));
}
}
float4 PSMain (float4 vertex:SV_POSITION, float2 UV : TEXCOORD1) : SV_Target
{
float2 S = UV*2.0-1.0;
if (dot(S.xy, S.xy) > 1.0) discard;
return float4(0,0,1,1);
}
ENDCG
}
}
}
Thanks a lot for the reply I didn’t know about procedural drawing. I thought about baking a quad on my characters meshes but this is much better!
I removed branching from vertex shader. Now, for VS, I have 24 math instructions and 2 temp registers. Previous example was related with calculating circles centers procedurally as grid of circles, now example with array:
// Add script to camera and assign shader "DrawCircles".
// Script renders 2048 circles with single draw call and their center coordinates
// are calculated once on the CPU and sent to GPU array.
// To use more than 2048 circles, you can use structured buffer, bake point data to texture,
// or just calculate circles center coordinates procedurally (directly inside vertex shader).
using UnityEngine;
public class DrawCircles : MonoBehaviour
{
public Shader shader;
protected Material material;
void Awake()
{
material = new Material(shader);
float[] bufferX = new float[2048];
float[] bufferY = new float[2048];
for (int i=0; i<2048; i++)
{
bufferX[i] = Random.Range(0.0f, 120.0f);
bufferY[i] = Random.Range(0.0f, 120.0f);
}
material.SetFloatArray("BufferX", bufferX);
material.SetFloatArray("BufferY", bufferY);
}
void OnRenderObject()
{
material.SetPass(0);
Graphics.DrawProcedural(MeshTopology.Triangles, 6, 2048);
}
}
Shader "Draw Circles"
{
Subshader
{
Pass
{
Cull Off
CGPROGRAM
#pragma vertex VSMain
#pragma fragment PSMain
#pragma target 5.0
float BufferX[2048];
float BufferY[2048];
float mod(float x, float y)
{
return x - y * floor(x/y);
}
float3 hash(float p)
{
float3 p3 = frac(p.xxx * float3(.1239, .1237, .2367));
p3 += dot(p3, p3.yzx+63.33);
return frac((p3.xxy+p3.yzz)*p3.zyx);
}
float4 VSMain (uint id:SV_VertexID, out float2 uv:TEXCOORD0, inout uint instance:SV_INSTANCEID) : SV_POSITION
{
float3 center = float3(BufferX[instance], 0.0, BufferY[instance]);
float u = mod(float(id),2.0);
float v = sign(mod(126.0,mod(float(id),6.0)+6.0));
uv = float2(u,v);
return UnityObjectToClipPos(float4(float3(sign(u)-0.5, 0.0, sign(v)-0.5) + center,1.0));
}
float4 PSMain (float4 vertex:SV_POSITION, float2 uv:TEXCOORD0, uint instance:SV_INSTANCEID) : SV_Target
{
float2 S = uv*2.0-1.0;
if (dot(S.xy, S.xy) > 1.0) discard;
return float4(hash(float(instance)), 1.0);
}
ENDCG
}
}
}
Thank you so much for sharing this awesome shader!
Just one thing: I don’t know why exactly, but in order for this to work for me I need to use 64bit precision in the shader for the line where you calculate v, so basically I have to change the mod() function to use doubles and also make the line where v is calculated into
float v = sign(mod(126.0L,mod(double(id),6.0L)+6.0L));
. If I don’t do this I only get the 2nd half of the circle. The first triangle gets calculated wrong, so either 126 mod (0 mod 6 + 6), 126 mod (1 mod 6 + 6), or 126 mod (2 mod 6 + 6) is giving me the wrong value instead of the expected 0,0,1. Hope this can help someone else in the future.
Yes, I discovered that previous formula can give incorrect results on some GPUs. Try to replace u,v with:
float u = sign(mod(20.0, mod(float(id), 6.0) + 2.0));
float v = sign(mod(18.0, mod(float(id), 6.0) + 2.0));
Example with ComputeBuffer:
using UnityEngine;
using System.Runtime.InteropServices;
public class DrawCircles : MonoBehaviour
{
public Shader DrawCirclesShader;
private ComputeBuffer _ComputeBuffer;
private Material _Material;
private const int _Count = 256 * 256; // 65536
public struct Circle
{
public Vector3 Position;
public Vector3 Color;
};
void Awake()
{
_ComputeBuffer = new ComputeBuffer(_Count, Marshal.SizeOf(typeof(Circle)), ComputeBufferType.Default);
_Material = new Material(DrawCirclesShader);
Circle[] circles = new Circle[_Count];
for (uint i = 0; i < _Count; i++)
{
float x = Random.Range(0.0f, 512.0f);
float y = Random.Range(0.0f, 1.0f);
float z = Random.Range(0.0f, 512.0f);
float r = Random.Range(0.0f, 1.0f);
float g = Random.Range(0.0f, 1.0f);
float b = Random.Range(0.0f, 1.0f);
circles[i].Position = new Vector3(x, y, z);
circles[i].Color = new Vector3(r, g, b);
}
_ComputeBuffer.SetData(circles);
_Material.SetBuffer("_ComputeBuffer", _ComputeBuffer);
}
void OnRenderObject()
{
_Material.SetPass(0);
Graphics.DrawProcedural(MeshTopology.Triangles, 6, _Count);
}
void OnDestroy()
{
Destroy(_Material);
_ComputeBuffer.Release();
}
}
Shader "Draw Circles"
{
Subshader
{
Pass
{
Cull Off
CGPROGRAM
#pragma vertex VSMain
#pragma fragment PSMain
#pragma target 5.0
struct Circle
{
float3 Position;
float3 Color;
};
StructuredBuffer<Circle> _ComputeBuffer;
float Mod(float x, float y)
{
return x - y * floor(x/y);
}
float4 VSMain (uint id : SV_VertexID, out float2 uv : TEXCOORD0, inout uint instance : SV_INSTANCEID) : SV_POSITION
{
float3 center = _ComputeBuffer[instance].Position;
float u = sign(Mod(20.0, Mod(float(id), 6.0) + 2.0));
float v = sign(Mod(18.0, Mod(float(id), 6.0) + 2.0));
uv = float2(u,v);
float4 position = float4(float3(sign(u) - 0.5, 0.0, sign(v) - 0.5) + center, 1.0);
return UnityObjectToClipPos(position);
}
float4 PSMain (float4 vertex : SV_POSITION, float2 uv : TEXCOORD0, uint instance : SV_INSTANCEID) : SV_Target
{
float2 s = uv * 2.0 - 1.0;
if (dot(s.xy, s.xy) > 1.0) discard;
return float4(_ComputeBuffer[instance].Color, 1.0);
}
ENDCG
}
}
}