The right search term makes all the difference. Thanks to @broxxar I found this blog post by @prime31 and to not just copy the work of others I changed the “Occluded” shader a tiny bit so the replacement color can be set via script on the game object with the sprite renderer.
Here are the two shaders as explained in the blog post plus my small adjustment and script to set the replacement color:
Shader "Sprites/Occluder"
{
Properties
{
[PerRendererData] _MainTex ( "Sprite Texture", 2D ) = "white" {}
_Color ( "Tint", Color ) = ( 1, 1, 1, 1 )
[MaterialToggle] PixelSnap ( "Pixel snap", Float ) = 0
_AlphaCutoff ( "Alpha Cutoff", Range( 0.01, 1.0 ) ) = 0.1
}
SubShader
{
Tags
{
"Queue" = "Transparent"
"IgnoreProjector" = "True"
"RenderType" = "TransparentCutout"
"PreviewType" = "Plane"
"CanUseSpriteAtlas" = "True"
}
Cull Off
Lighting Off
ZWrite Off
Blend One OneMinusSrcAlpha
Pass
{
Stencil
{
Ref 4
Comp Always
Pass Replace
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile _ PIXELSNAP_ON
#include "UnityCG.cginc"
struct appdata_t
{
float4 vertex : POSITION;
float4 color : COLOR;
float2 texcoord : TEXCOORD0;
};
struct v2f
{
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
half2 texcoord : TEXCOORD0;
};
fixed4 _Color;
fixed _AlphaCutoff;
v2f vert( appdata_t IN )
{
v2f OUT;
OUT.vertex = mul( UNITY_MATRIX_MVP, IN.vertex );
OUT.texcoord = IN.texcoord;
OUT.color = IN.color * _Color;
#ifdef PIXELSNAP_ON
OUT.vertex = UnityPixelSnap( OUT.vertex );
#endif
return OUT;
}
sampler2D _MainTex;
sampler2D _AlphaTex;
fixed4 frag( v2f IN ) : SV_Target
{
fixed4 c = tex2D( _MainTex, IN.texcoord ) * IN.color;
c.rgb *= c.a;
// here we discard pixels below our _AlphaCutoff so the stencil buffer only gets written to
// where there are actual pixels returned. If the occluders are all tight meshes (such as solid rectangles)
// this is not necessary and a non-transparent shader would be a better fit.
clip( c.a - _AlphaCutoff );
return c;
}
ENDCG
}
}
}
Shader for the occluded sprite:
Shader "Sprites/Occluded"
{
Properties
{
[PerRendererData] _MainTex ( "Sprite Texture", 2D ) = "white" {}
_Color ( "Tint", Color ) = ( 1, 1, 1, 1 )
[MaterialToggle] PixelSnap ( "Pixel snap", Float ) = 0
// Making the occluded Color configurable by script
[PerRendererData] _OccludedColor ( "Occluded Tint", Color ) = ( 0, 0, 0, 0.5 )
}
CGINCLUDE
// shared structs and vert program used in both the vert and frag programs
struct appdata_t
{
float4 vertex : POSITION;
float4 color : COLOR;
float2 texcoord : TEXCOORD0;
};
struct v2f
{
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
half2 texcoord : TEXCOORD0;
};
fixed4 _Color;
sampler2D _MainTex;
v2f vert( appdata_t IN )
{
v2f OUT;
OUT.vertex = UnityObjectToClipPos( IN.vertex );
OUT.texcoord = IN.texcoord;
OUT.color = IN.color * _Color;
#ifdef PIXELSNAP_ON
OUT.vertex = UnityPixelSnap( OUT.vertex );
#endif
return OUT;
}
ENDCG
SubShader
{
Tags
{
"Queue" = "Transparent"
"IgnoreProjector" = "True"
"RenderType" = "Transparent"
"PreviewType" = "Plane"
"CanUseSpriteAtlas" = "True"
}
Cull Off
Lighting Off
ZWrite Off
Blend One OneMinusSrcAlpha
Pass
{
Stencil
{
Ref 4
Comp NotEqual
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile _ PIXELSNAP_ON
#include "UnityCG.cginc"
fixed4 frag( v2f IN ) : SV_Target
{
fixed4 c = tex2D( _MainTex, IN.texcoord ) * IN.color;
c.rgb *= c.a;
return c;
}
ENDCG
}
// occluded pixel pass. Anything rendered here is behind an occluder
Pass
{
Stencil
{
Ref 4
Comp Equal
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile _ PIXELSNAP_ON
#include "UnityCG.cginc"
fixed4 _OccludedColor;
fixed4 frag( v2f IN ) : SV_Target
{
fixed4 c = tex2D( _MainTex, IN.texcoord );
return _OccludedColor * c.a;
}
ENDCG
}
}
}
And a script that lets you set the replacement color on a per-object-basis with the same shader - inspired by this blog post by @Thomas-Mountainborn:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[ExecuteInEditMode]
public class ReplacementColor : MonoBehaviour
{
public Color ReplColor;
private Renderer _renderer;
private MaterialPropertyBlock _propBlock;
void OnGUI()
{
#if UNITY_EDITOR
_propBlock = new MaterialPropertyBlock();
_renderer = GetComponent<Renderer>();
setReplacementColor(ReplColor);
#endif
}
void Awake()
{
_propBlock = new MaterialPropertyBlock();
_renderer = GetComponent<Renderer>();
setReplacementColor(ReplColor);
}
private void setReplacementColor( Color newColor)
{
if (_propBlock != null ){
// Get the current value of the material properties in the renderer.
_renderer.GetPropertyBlock(_propBlock);
// Assign our new value.
_propBlock.SetColor("_OccludedColor", newColor );
// Apply the edited values to the renderer.
_renderer.SetPropertyBlock(_propBlock);
}
}
}
Hope this will help someone who is searching for this like I have.