How to draw many circles efficiently

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
        }
       
    }
}

4 Likes

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
        }
    }
}

2 Likes

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
        }
    }
}
2 Likes