Billboard shader for GPU Instancing

I’m trying to use GPU instancing to spawn some billboard elements in my screen, but the billboard shader isn’t working.

I’m using the shader code provided by Unity here as a starting point. I modified it and now it is only spawning the objects at the given world position data.xyz.

This is the current state of the shader code:

Shader "Instanced/InstancedShader" {

    Properties{
        _MainTex("Albedo (RGB)", 2D) = "white" {}
    }

    SubShader{

        Pass {

            Tags {"LightMode" = "SRPDefaultUnlit"}

            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fwdbase nolightmap nodirlightmap nodynlightmap novertexlight
            #pragma target 4.5

            #include "UnityCG.cginc"
            #include "UnityLightingCommon.cginc"
            #include "AutoLight.cginc"

            sampler2D _MainTex;

        #if SHADER_TARGET >= 45
            StructuredBuffer<float4> positionBuffer;
        #endif

            struct v2f
            {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
                SHADOW_COORDS(4)
            };

            v2f vert(appdata_full v, uint instanceID : SV_InstanceID)
            {
            #if SHADER_TARGET >= 45
                float4 data = positionBuffer[instanceID];
            #else
                float4 data = 0;
            #endif
                float3 localPosition = v.vertex.xyz;
                float3 worldPosition = data.xyz + localPosition;
                float3 worldNormal = v.normal;

                v2f o;
                o.pos = mul(UNITY_MATRIX_VP, float4(outPos, 1.0f));
                o.uv = v.texcoord;                
                TRANSFER_SHADOW(o)
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                return tex2D(_MainTex, i.uv);;
            }

            ENDCG
        }
    }
}

How can I make a billboard out from this shader? I’ve been trying a bunch of stuff but in the end the instantiated meshes always look wierd or simply disappear or spawn in the wrong positions (they should be centered in data.xyz).

I have been working on a custom particle system too, and the solution I use for now is instancing a mesh with a single vertex at position (0,0,0) and using a geometry shader to generate the billboard from that. I haven’t looked at this code in quite a while, so it can probably be optimized, or steps could be skipped, but it works for me. At least this should give you a jumping off point, someone else might have something more elegant than this.

v2g vert(inputVert i)
{
   v2g o;

   //  Different instancing setup, but that shouldnt matter
   UNITY_INITIALIZE_OUTPUT(v2g, o);
   UNITY_SETUP_INSTANCE_ID(i);
   UNITY_TRANSFER_INSTANCE_ID(i, o);
  
   #if defined(ENABLE_INSTANCING)
   ParticleData particle = _ParticleDataBuffer[unity_InstanceID];
   // No transform matrix here, happens in the geometry shader!
   o.vertex = float4(particle.worldPosition, 1);
   o.color = float4(particle.albedo, 1);

   #else
   o.vertex = i.vertex;
   o.color = float4(1, 0, 1, 1);
   #endif
   return o;
}


[maxvertexcount(4)]
void geom(point v2g IN[1], inout TriangleStream<g2f> triStream)
{
   g2f o;
  
   UNITY_INITIALIZE_OUTPUT(g2f, o);
   UNITY_SETUP_INSTANCE_ID(IN[0]);
   UNITY_TRANSFER_INSTANCE_ID(IN[0], o);

     
   float4 vert;

   o.color = IN[0].color;

   // Make a quad out of the input position. We add an offset of 0.5 in local space to each corner so a quad is 1 with
   //_BillboardScale of 1 is 1 unit tall/wide. We need to multiply that offset vector with the model-view matrix in reverse order,
   // so that the perspective skewing from UnityObjectToClipPos is cancelled later. I guess. Kinda forgot.

   vert = IN[0].vertex + mul(float4(-0.5, -0.5, 0, 0) * _BillboardScale, UNITY_MATRIX_MV);
   o.clipPos = UnityObjectToClipPos(vert);
   o.uv = float2(0, 0);
   triStream.Append(o);

   vert = IN[0].vertex + mul(float4(-0.5, 0.5, 0, 0) * _BillboardScale, UNITY_MATRIX_MV);
   o.clipPos = UnityObjectToClipPos(vert);
   o.uv = float2(0, 1);
   triStream.Append(o);

   vert = IN[0].vertex + mul(float4(0.5, -0.5, 0, 0) * _BillboardScale, UNITY_MATRIX_MV);
   o.clipPos = UnityObjectToClipPos(vert);
   o.uv = float2(1, 0);
   triStream.Append(o);

   vert = IN[0].vertex + mul(float4(0.5, 0.5, 0, 0) * _BillboardScale, UNITY_MATRIX_MV);
   o.clipPos = UnityObjectToClipPos(vert);
   o.uv = float2(1, 1);
   triStream.Append(o);

   triStream.RestartStrip();
}

float4 frag(g2f i) : SV_Target
{
   UNITY_SETUP_INSTANCE_ID(i);

   float3 col = i.color * tex2D(_MainTex, i.uv);
   return float4(col, 1);
}