Thanks that helps a lot.
I have this almost working now. The shadows are being drawn with the CommandBuffer but it still has the shadow artifacts so the stencil part isn’t working for the shadows.

I’m not too worried about this being performant just yet, I’ll get it working first then optimize, so right now I only have 2 lights and 4 items in the scene and just dragged them onto public arrays in the inspector.
I’m not sure why the stencil buffer isn’t working for the shadows (works fine for the colour pass), so I guess I’ll just share my scripts, can you see anything obviously wrong?
Item Shader:
Shader "Custom/ItemShader"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_BumpMap("Bumpmap", 2D) = "bump" {}
_Glossiness ("Smoothness", Range(0,1)) = 0.5
_Metallic ("Metallic", Range(0,1)) = 0.0
}
SubShader
{
Tags { "Queue" = "Geometry" }
LOD 200
Stencil
{
Ref 1
Comp notequal
}
CGPROGRAM
// Physically based Standard lighting model, and enable shadows on all light types
#pragma surface surf Standard fullforwardshadows
// Use shader model 3.0 target, to get nicer looking lighting
#pragma target 3.0
sampler2D _MainTex;
sampler2D _BumpMap;
struct Input
{
float2 uv_MainTex;
float2 uv_BumpMap;
};
half _Glossiness;
half _Metallic;
fixed4 _Color;
// Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
// See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
// #pragma instancing_options assumeuniformscaling
UNITY_INSTANCING_BUFFER_START(Props)
// put more per-instance properties here
UNITY_INSTANCING_BUFFER_END(Props)
void surf (Input IN, inout SurfaceOutputStandard o)
{
// Albedo comes from a texture tinted by color
fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));
// Metallic and smoothness come from slider variables
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
o.Alpha = c.a;
}
ENDCG
Pass
{
Name "ItemStencilPass"
ColorMask 0
Stencil
{
Ref 1
Comp always
Pass replace
}
}
}
//FallBack "Diffuse"
}
Shadow caster shader:
Shader "Custom/ItemShadowCaster"
{
SubShader
{
Pass
{
Stencil
{
Ref 1
Comp notequal
}
Tags { "Queue" = "Transparent" "LightMode" = "ShadowCaster" }
ZWrite On ZTest Less Cull Off
Offset 1, 1
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_shadowcaster
#pragma fragmentoption ARB_precision_hint_fastest
#include "UnityCG.cginc"
struct v2f
{
V2F_SHADOW_CASTER;
};
v2f vert(appdata_base v)
{
v2f o;
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
return o;
}
float4 frag(v2f i) : COLOR
{
SHADOW_CASTER_FRAGMENT(i)
}
ENDCG
}
}
}
Shadow renderer on the camera (does most of the work):
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
//-------------------------------------------------------------------
//-------------------------------------------------------------------
[ExecuteInEditMode]
public class ItemShadowRenderer : MonoBehaviour
{
public Material m_ShadowCasterMaterial = null;
public GameObject[] m_aLights = null;
public GameObject[] m_aItems = null;
private Light[] m_aLightScripts = null;
private ItemBase[] m_aItemScripts = null;
//-------------------------------------------------------------------
//-------------------------------------------------------------------
private void ResetRenderer()
{
if(m_aLights == null)
return;
m_aLightScripts = new Light[m_aLights.Length];
for(int i = 0; i < m_aLights.Length; ++i)
{
if(m_aLights[i])
{
Light light = m_aLights[i].GetComponent<Light>();
light.RemoveAllCommandBuffers();
m_aLightScripts[i] = light;
}
}
m_aItemScripts = new ItemBase[m_aItems.Length];
for(int i = 0; i < m_aItems.Length; ++i)
{
if(m_aItems[i])
{
m_aItemScripts[i] = m_aItems[i].GetComponent<ItemBase>();
m_aItemScripts[i].CreateCommandBuffer(m_ShadowCasterMaterial);
}
}
}
//-------------------------------------------------------------------
//-------------------------------------------------------------------
private void OnDisable()
{
ResetRenderer();
}
//-------------------------------------------------------------------
//-------------------------------------------------------------------
private void OnEnable()
{
ResetRenderer();
}
//-------------------------------------------------------------------
//-------------------------------------------------------------------
private void SortItemList()
{
for(int i = 0; i < m_aItemScripts.Length - 1; ++i)
{
for(int j = 0; j < m_aItemScripts.Length - 1 - i; ++j)
{
if(m_aItemScripts[j].GetRenderDepth() < m_aItemScripts[j + 1].GetRenderDepth())
{
ItemBase swap = m_aItemScripts[j];
m_aItemScripts[j] = m_aItemScripts[j + 1];
m_aItemScripts[j + 1] = swap;
}
}
}
}
//-------------------------------------------------------------------
//-------------------------------------------------------------------
private void Update()
{
ResetRenderer();
if(m_aLightScripts == null)
return;
if(m_aItemScripts == null)
return;
SortItemList();
foreach(Light light in m_aLightScripts)
{
foreach(ItemBase item in m_aItemScripts)
{
light.AddCommandBuffer(LightEvent.AfterShadowMapPass, item.GetCommandBuffer());
}
}
}
}
On each items script to create the command buffer
//-------------------------------------------------------------------
//-------------------------------------------------------------------
public void CreateCommandBuffer(Material shadowCaster)
{
if(shadowCaster == null)
return;
if(m_CommandBuffer == null)
{
MeshRenderer renderer = GetComponent<MeshRenderer>();
m_CommandBuffer = new CommandBuffer();
m_CommandBuffer.name = "Item Shadow Caster";
m_CommandBuffer.DrawRenderer(renderer, shadowCaster, 0, 0);
int nStencilPass = renderer.sharedMaterial.FindPass("ItemStencilPass");
m_CommandBuffer.DrawRenderer(renderer, renderer.sharedMaterial, 0, nStencilPass);
}
}
//-------------------------------------------------------------------
//-------------------------------------------------------------------
public CommandBuffer GetCommandBuffer()
{
return m_CommandBuffer;
}
Any idea why the stencil buffer may not be working for the shadows?