Surface shader clip() but for z write?

I have a project for which I am rendering a 3D scene, and then comping in a secondary image-with-depth-information on the basis of whether or not its depth is nearer than the final state of the Z buffer. The end result is pretty magical, excepting that we can’t use any transparent materials because they aren’t going to hit the depth buffer, and retroactive blending is basically impossible.

I had the vague notion that if we really needed to play with transparency, I could hack out a basic Bayer dithering shader, and get only a scatter of opaque pixels represented on a given translucent surface, and comp the secondary data in around that.

But having finally sorted out the correct way to use clip() to control whether or not a given fragment writes, the result I’m seeing is that yes, clipped fragments don’t write to the color buffer, but… they still write to the depth buffer. So I then get a solid chunk of see-through-to-the-original-background rendering “on top” of my secondary content (or rather, my secondary content unable to render over pixels which are colored as if transparent, but depth’d as if opaque.

Am I missing something here, or is there another way to write to the Z buffer only on a conditional basis?

As I think about it now, I could maybe-possibly do my external image comp between the opaque and transparent render passes and thereby get some transparency over it, but then it in turn couldn’t render in front of transparent materials because it’s equally painful to write arbitrary values directly to the depth buffer, at least in my recollection.

Edit: I’ve also tried the basic pragma to “auto-generate a shadowcaster from the fragment shader” and it didn’t do anything useful (I forget how the result was differently-incorrect). A custom shadowcaster pass would probably be the right direction, but I haven’t spotted the proper syntax for surface+shadowcaster, and our artists might riot if I constrain them to a custom vertex+fragment+shadow shader that’s so much more limited/finnicky than the stock Unity surface shader.

collecting snippets of bgolus’ sage advice from other shadowcaster threads, I’ve bodged one of my own, but no dice. It still seems to render the depth fragments as full solid. I am 100% certain that I’m missing something, but with so little thorough documentation on custom passes and what is expected of them, I’m still not certain what I’m even aiming to output from the shadowcaster. All examples I’ve found declare a float4 vertex shader, but then output a scalar 0 after doing pretty ordinary conversions in the vertex shader. Full current code, with obviously lots of trial-and-error commentary and discontinued approaches:

Shader "Custom/Dissolve"
{
	Properties
	{
		_Color ("Color", Color) = (1,1,1,1)
		_MainTex ("Albedo (RGB)", 2D) = "white" {}
		_Glossiness ("Smoothness", Range(0,1)) = 0.5
		_Metallic ("Metallic", Range(0,1)) = 0.0
	}
	SubShader
	{
		//Tags { "Queue"="AlphaTest" "RenderType"="TransparentCutout" }
		Tags { "RenderType"="Opaque" }
		//Blend srcAlpha oneMinusSrcAlpha
		LOD 200
		//ZWrite On
		//Alphatest Greater 0

		CGPROGRAM
		// Physically based Standard lighting model, and enable shadows on all light types
		//#pragma surface surf Standard fullforwardshadows
		//#pragma surface surf Standard addshadow alpha alphatest:_AlphaCut
		//#pragma surface surf Standard alpha alphatest:_AlphaCut
		#pragma surface surf Standard fullforwardshadows addshadow

		// Use shader model 3.0 target, to get nicer looking lighting
		#pragma target 3.0

		sampler2D _MainTex;

		struct Input
		{
			float2 uv_MainTex;
			float4 screenPos;
		};

		half _Glossiness;
		half _Metallic;
		fixed4 _Color;

		//float4 _ScreenParams;

		// https://en.wikipedia.org/wiki/Ordered_dithering
		// static const uint BAYER[8][8] = {
		// 	{  0, 32,  8, 40,  2, 34, 10, 42 },
		// 	{ 48, 16, 56, 24, 50, 18, 58, 26 },
		// 	{ 12, 44,  4, 36, 14, 46,  6, 38 },
		// 	{ 60, 28, 52, 20, 62, 30, 54, 22 },
		// 	{  3, 35, 11, 43,  1, 33,  9, 41 },
		// 	{ 51, 19, 59, 27, 49, 17, 57, 25 },
		// 	{ 15, 47,  7, 39, 13, 45,  5, 37 },
		// 	{ 63, 31, 55, 23, 61, 29, 53, 21 }
		// };
		static const uint BAYER[64] = {
			  0, 32,  8, 40,  2, 34, 10, 42,
			  48, 16, 56, 24, 50, 18, 58, 26,
			  12, 44,  4, 36, 14, 46,  6, 38,
			  60, 28, 52, 20, 62, 30, 54, 22,
			   3, 35, 11, 43,  1, 33,  9, 41,
			  51, 19, 59, 27, 49, 17, 57, 25,
			  15, 47,  7, 39, 13, 45,  5, 37,
			  63, 31, 55, 23, 61, 29, 53, 21
		};

		// 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;
			// Metallic and smoothness come from slider variables
			o.Metallic = _Metallic;
			o.Smoothness = _Glossiness;
			o.Alpha = c.a;

			//https://discussions.unity.com/t/generate-random-float-between-0-and-1-in-shader/728060/8
			float2 screenUV = IN.screenPos.xy / IN.screenPos.w;
			float2 screenCoord = screenUV * _ScreenParams.xy; // results should be identical to the ShaderToy fragcoord
			int bx = round(screenCoord.x)%8;
			int by = round(screenCoord.y)%8;
			//int bayer = BAYER[by][bx];
			int bayer = BAYER[by*8+bx];
			//o.Alpha = (c.a*64)>bayer ? 1 : 0;
			clip(c.a*64-bayer);
		}
		ENDCG

		// https://discussions.unity.com/t/custom-shadowcaster-for-indirect-surface-shader/808795/2
		// https://discussions.unity.com/t/how-to-get-position-of-current-pixel-in-screen-space-in-framgment-shader-function/524186/2
		// https://github.com/przemyslawzaworski/Unity3D-CG-programming/blob/master/shadowcaster.shader
		Pass
		{
			Name "ShadowCaster"
			Tags { "LightMode" = "ShadowCaster" }

			ZWrite On
			ZTest LEqual

			CGPROGRAM

			#pragma vertex vertShadowCaster
			#pragma fragment fragShadowCaster
			#include "UnityCG.cginc"

			StructuredBuffer<float4x4> obj2world;
			sampler2D _MainTex;
			float4 _MainTex_ST;
			fixed4 _Color;

			static const uint BAYER[64] = {
				  0, 32,  8, 40,  2, 34, 10, 42,
				  48, 16, 56, 24, 50, 18, 58, 26,
				  12, 44,  4, 36, 14, 46,  6, 38,
				  60, 28, 52, 20, 62, 30, 54, 22,
				   3, 35, 11, 43,  1, 33,  9, 41,
				  51, 19, 59, 27, 49, 17, 57, 25,
				  15, 47,  7, 39, 13, 45,  5, 37,
				  63, 31, 55, 23, 61, 29, 53, 21
			};

			struct v2f
			{
				float4 vertex : SV_POSITION;
				float2 uv : TEXCOORD0;
				float4 screenPos : TEXCOORD1;
			};

			v2f vertShadowCaster(appdata_base v, uint i : SV_InstanceID)
			{
				v2f o;

				v.vertex = mul(obj2world[i], v.vertex);
				o.vertex = mul(UNITY_MATRIX_VP, v.vertex);

				o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
				o.screenPos = ComputeScreenPos(o.vertex);

				// the third link above seems to be running its own trig dither
				// and just offsetting the vertex by the result, omitting
				// the fragment shader entirely.  Trying to eject the geo
				// from clip space as a way to make it not render feels dirty...
				//vertex.xyz-=(sin(_Time.g)*0.5+0.5)*normal*hash(float(id));
				//return UnityObjectToClipPos(vertex);

				return o;
			}

			float4 fragShadowCaster(v2f i) : SV_Target
			{
				//return 0;

				fixed4 c = tex2D (_MainTex, i.uv) * _Color;

				float2 screenUV = i.screenPos.xy / i.screenPos.w;
				float2 screenCoord = screenUV * _ScreenParams.xy;
				int bx = round(screenCoord.x)%8;
				int by = round(screenCoord.y)%8;
				//int bayer = BAYER[by][bx];
				int bayer = BAYER[by*8+bx];
				//o.Alpha = (c.a*64)>bayer ? 1 : 0;
				clip(c.a*64-bayer);

				//return 0; // still not sure I understand how shadowcasters work when they all return 0...
				SHADOW_CASTER_FRAGMENT(i);
			}

			ENDCG
		}
	}
	//FallBack "Diffuse" // keep backup renderers from providing solid shadows
	//Fallback "Legacy Shaders/Transparent/Cutout/VertexLit"
}

Once I’m even confident in what I’m trying to output, there’s obviously some independent optimization which could be done of how I generate the values, but for now I’d love to understand how the shadowcaster even leads to the depth render, since at no point do any examples I see seem to even be processing distance/depth on a per-fragment basis…