Achieving a multi pass effect with a Surface Shader

Heya! I’ve been scratching my head over this and was hoping someone might be able to point me in the right direction.

I’m trying to add a silhouette effect to some characters so they’re visible through objects, this is a mockup of what I’m going for :

Normally I’d do this with a flat shaded second pass which uses “ztest Greater” but I’m using a Surface Shader which don’t seem compatible with additional passes.

Is there a way to do this or get an equivalent result while still using a Surface Shader?

Shader "Character Indicator" {
Properties {
	_Color ("Main Color", Color) = (1,1,1,1)
	_MainTex ("Base (RGB)", 2D) = "white" {}
	_WrapTex ("Wrap ramp (RGBA)", 2D) = "black" {}
	_IndicatorTex ("Indicator Lights (RGB)", 2D) = "white" {}
	_Indicator ("Indicator Color", Color) = (1,1,1,1)
	_Cutoff ("Alpha cutoff", Range (0,1)) = 0.0
}
 
SubShader {
	Tags { "RenderType" = "Opaque" }

CGPROGRAM
#pragma surface surf Ramp alphatest:_Cutoff

uniform float4 _Color;
uniform float4 _Indicator;
uniform sampler2D _MainTex;
uniform sampler2D _WrapTex;
uniform sampler2D _IndicatorTex;

half4 LightingRamp (SurfaceOutput s, half3 lightDir, half atten) {
	half NdotL = dot (s.Normal, lightDir);
	half diff = NdotL * 0.5 + 0.5;
	half3 ramp = tex2D (_WrapTex, float2(diff)).rgb;
	half4 c;
	c.rgb = s.Albedo * _LightColor0.rgb * ramp * (atten * 2);
	c.a = s.Alpha;
	return c;
}

struct Input {
	float2 uv_MainTex;
	float2 uv_BumpMap;
	float3 viewDir;
};

void surf (Input IN, inout SurfaceOutput o) {
	o.Albedo = tex2D ( _MainTex, IN.uv_MainTex).rgb * _Color;
	o.Emission = tex2D ( _IndicatorTex, IN.uv_MainTex).rgb * _Indicator * 2;
	
	half rim = 1.0 - saturate(dot (normalize(IN.viewDir), o.Normal));
	o.Emission += tex2D ( _MainTex, IN.uv_MainTex).rgb * 2 * pow (rim, 3.0);
	o.Alpha = tex2D ( _MainTex, IN.uv_MainTex).a;
}

ENDCG

}

Fallback " Glossy", 0
 
}

Any help would be much appreciated!

You can’t add arbitrary passes with surface shaders, but what you can do is use #pragma debug in your surface shader to get the generated passes in comments when you open the compiled shader. You can use this as a starting point and then add your extra passes by hand.

You can add arbitrary passes while using a surface shader.

You just stick everything else in Pass {} tags and ensure that your surface shader isn’t inside any of them (as Unity will generate them as it parses the surface shader).

For my river shader, I’ve successfully done a depth-based pass then a grab pass then a distortion pass which then has a surface shader rendering on top.

Example code;

Shader "Exploration/River" {
	Properties {
		_Color ("Main Color", Color) = (1,1,1,1)
		_DepthColor ("Depth Color", Color) = (1,1,1,1)
		_WaterDepth ("Water Depth", Range (0, 10)) = 1
		_BumpMap ("Normal Shading (Normal)", 2D) = "bump" {}
		_WaterSpeed ("Water Speed", Range (0, 10)) = 1
		_WaterSpeed2 ("Water Speed", Range (0, 10)) = 0.37
		_Fresnel ("Fresnel Value", Float) = 0.028
	}
	SubShader{
		Tags { "RenderType" = "Transparent" "Queue" = "Transparent" }
		Blend One One

		Pass
		{
			Name "RiverDepth"
			Blend SrcAlpha OneMinusSrcAlpha
			CGPROGRAM
				#pragma vertex vert
				#pragma fragment frag
				#pragma fragmentoption ARB_precision_hint_fastest
				#include "UnityCG.cginc"

				struct v2f {
					float4 pos			: POSITION;
					float4 screenPos	: TEXCOORD0;
				};

				v2f vert (appdata_full v)
				{
					v2f o;
					o.pos = mul(UNITY_MATRIX_MVP, v.vertex);	
					o.screenPos = ComputeScreenPos(o.pos);
					return o;
				}

				sampler2D _CameraDepthTexture;
				float4 _DepthColor;
				float _WaterDepth;

				half4 frag( v2f i ) : COLOR
				{
					float depth = 1 - saturate(_WaterDepth - (LinearEyeDepth(tex2D(_CameraDepthTexture, i.screenPos.xy / i.screenPos.w).r) - i.screenPos.z));
					return half4(_DepthColor.rgb, depth * _DepthColor.a);
				}
			ENDCG			
		}

		GrabPass {
			Name "RiverGrab"
		}

		Pass
		{
			Name "RiverDistortion"
			Blend Off
			CGPROGRAM
				#pragma vertex vert
				#pragma fragment frag
				#pragma fragmentoption ARB_precision_hint_fastest
				#include "UnityCG.cginc"

				struct v2f {
					float4 pos			: POSITION;
					float4 uvgrab		: TEXCOORD0;
					float2 uv			: TEXCOORD1;
					float4 screenPos	: TEXCOORD2;
				};

				v2f vert (appdata_full v)
				{
					v2f o;
					o.pos = mul(UNITY_MATRIX_MVP, v.vertex);	
					#if UNITY_UV_STARTS_AT_TOP
					float scale = -1.0;
					#else
					float scale = 1.0;
					#endif
					o.uvgrab.xy = (float2(o.pos.x, o.pos.y * scale) + o.pos.w) * 0.5;
					o.uvgrab.zw = o.pos.zw;
					o.uv = v.texcoord.xy;
					return o;
				}

				sampler2D _BumpMap;
				float _WaterSpeed, _WaterSpeed2;
				sampler2D _GrabTexture;
				float4 _GrabTexture_TexelSize;

				half4 frag( v2f i ) : COLOR
				{
					float2 riverUVs = i.uv;
					riverUVs.y += _Time * _WaterSpeed;
					float3 normal1 = UnpackNormal(tex2D(_BumpMap, riverUVs));
					riverUVs = i.uv;
					riverUVs.x *= -1;
					riverUVs.y += 0.3 + _Time * _WaterSpeed2;
					float3 normal2 = UnpackNormal(tex2D(_BumpMap, riverUVs));
					normal2 *= float3(1, 1, 0.5);
					
					float3 combinedNormal = normalize(normal1 * normal2);
				
					float2 offset = combinedNormal.xy * 5 * _GrabTexture_TexelSize.xy;
					i.uvgrab.xy = (offset * i.uvgrab.z) + i.uvgrab.xy;
					return half4(tex2Dproj(_GrabTexture, UNITY_PROJ_COORD(i.uvgrab)).rgb, 1);
				}
			ENDCG
		}

		CGPROGRAM
			#include "ExplorationLighting.cginc"
			#pragma surface surf ExplorationRiver noambient novertexlights nolightmap
			#pragma target 3.0
			
			struct Input
			{
				float2 uv_BumpMap;
			};

			sampler2D _BumpMap, _CameraDepthTexture;
			float _Specular, _Gloss, _WaterSpeed, _WaterSpeed2;

			void surf (Input IN, inout SurfaceOutput o)
			{
				float2 riverUVs = IN.uv_BumpMap;
				riverUVs.y += _Time * _WaterSpeed;
				float3 normal1 = UnpackNormal(tex2D(_BumpMap, riverUVs));
				riverUVs = IN.uv_BumpMap;
				riverUVs.x *= -1;
				riverUVs.y += 0.3 + _Time * _WaterSpeed2;
				float3 normal2 = UnpackNormal(tex2D(_BumpMap, riverUVs));
				normal2 *= float3(1, 1, 0.5);
				
				float3 combinedNormal = normalize(normal1 * normal2);
				
				o.Albedo = fixed3(1);
				o.Normal = combinedNormal;
				o.Alpha = 0;
			}
		ENDCG
	}
	
	Fallback "Transparent/VertexLit"
}

You can even use multiple surface shaders on top of one another and change the blending/zwrite/blah values of them;

/*
Alpha tested pass.

Alpha blended pass w/ ZWrite off and alphatest greater than _Cutoff.

Anisotropic highlight.
*/

Shader "Exploration/Hair Soft Edge Surface" {
	Properties {
		_Color ("Main Color", Color) = (1,1,1,1)
		_MainTex ("Diffuse (RGB) Alpha (A)", 2D) = "white" {}
		_SpecularTex ("Specular (R) Gloss (G) Null (B)", 2D) = "gray" {}
		_BumpMap ("Normal (Normal)", 2D) = "bump" {}
		_AnisoTex ("Anisotropic Direction (RGB)", 2D) = "bump" {}
		_AnisoOffset ("Anisotropic Highlight Offset", Range(-0.5,0.5)) = -0.2
		_Cutoff ("Alpha Cut-Off Threshold", Range(0,1)) = 0.5
		_Fresnel ("Fresnel Value", Float) = 0.028
	}

	SubShader {
		Tags { "Queue"="AlphaTest" "RenderType"="TransparentCutout" }

		CGPROGRAM
			#include "ExplorationLighting.cginc"
			#pragma surface surf ExplorationSoftHairFirst fullforwardshadows exclude_path:prepass
			#pragma target 3.0
			
			struct Input
			{
				float2 uv_MainTex;
			};
			
			sampler2D _MainTex, _SpecularTex, _BumpMap, _AnisoDirection;
				
			void surf (Input IN, inout SurfaceOutputCharacter o)
			{
				fixed4 albedo = tex2D(_MainTex, IN.uv_MainTex);
				o.Albedo = albedo.rgb;
				o.Alpha = albedo.a;
				o.AnisoDir = tex2D(_AnisoDirection, IN.uv_MainTex);
				o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_MainTex));
				o.Specular = tex2D(_SpecularTex, IN.uv_MainTex).rgb;
			}
		ENDCG

		Blend SrcAlpha OneMinusSrcAlpha
		ZWrite Off

		CGPROGRAM
			#include "ExplorationLighting.cginc"
			#pragma surface surf ExplorationSoftHairSecond fullforwardshadows exclude_path:prepass noforwardadd
			#pragma target 3.0
			
			struct Input
			{
				float2 uv_MainTex;
			};
			
			sampler2D _MainTex, _SpecularTex, _BumpMap, _AnisoDirection;
				
			void surf (Input IN, inout SurfaceOutputCharacter o)
			{
				fixed4 albedo = tex2D(_MainTex, IN.uv_MainTex);
				o.Albedo = albedo.rgb;
				o.Alpha = albedo.a;
				o.AnisoDir = tex2D(_AnisoDirection, IN.uv_MainTex);
				o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_MainTex));
				o.Specular = tex2D(_SpecularTex, IN.uv_MainTex).rgb;
			}
		ENDCG
	}
	FallBack "Transparent/Cutout/VertexLit"
}
15 Likes

Ooh, count me interested in the outcome to this one : )

Count me in too, im interested in this one.

Edit :
Wait, how do you transfer the generated texture from the previous pass?

Very cool. I had no idea.

You could also write just two surface shaders and Unity will generate the right thing from it. What i mean is:

//1st pass
CGPROGRAM
#pragma surface surf
...
ENDCG

//2nd pass
CGPROGRAM
#pragma surface surf
...
ENDCG

Hope it helps :wink:

2 Likes

Ehr, you don’t… it just draws on top.

Wow, this could be so useful! I am watching intently.

Thankyou Farfarer! That was just what I needed to know!

I got my shader working the way I wanted it to, for anyone curious it ended up looking like this :

Shader "Character Indicator" {
Properties {
	_Color ("Main Color", Color) = (1,1,1,1)
	_MainTex ("Base (RGB)", 2D) = "white" {}
	_WrapTex ("Wrap ramp (RGBA)", 2D) = "black" {}
	_IndicatorTex ("Indicator Lights (RGB)", 2D) = "white" {}
	_Indicator ("Indicator Color", Color) = (1,1,1,1)
	_Cutoff ("Alpha cutoff", Range (0,1)) = 0.0
}
 
SubShader {
	Tags { [COLOR="red"]"Queue" = "Geometry+1"[/COLOR] "RenderType" = "Opaque" }
	
[COLOR="red"]		Pass {			
			Tags { "LightMode" = "Always" }
			AlphaTest Greater [_Cutoff]
			ZWrite Off
			ZTest Greater
			
			SetTexture [_MainTex] {
				constantColor [_Indicator]
				combine constant, texture
			}
		}[/COLOR]

CGPROGRAM
#pragma surface surf Ramp alphatest:_Cutoff

uniform float4 _Color;
uniform float4 _Indicator;
uniform sampler2D _MainTex;
uniform sampler2D _WrapTex;
uniform sampler2D _IndicatorTex;

half4 LightingRamp (SurfaceOutput s, half3 lightDir, half atten) {
	half NdotL = dot (s.Normal, lightDir);
	half diff = NdotL * 0.5 + 0.5;
	half3 ramp = tex2D (_WrapTex, float2(diff)).rgb;
	half4 c;
	c.rgb = s.Albedo * _LightColor0.rgb * ramp * (atten * 2);
	c.a = s.Alpha;
	return c;
}

struct Input {
	float2 uv_MainTex;
	float2 uv_BumpMap;
	float3 viewDir;
};

void surf (Input IN, inout SurfaceOutput o) {
	o.Albedo = tex2D ( _MainTex, IN.uv_MainTex).rgb * _Color;
	o.Emission = tex2D ( _IndicatorTex, IN.uv_MainTex).rgb * _Indicator * 2;
	
	half rim = 1.0 - saturate(dot (normalize(IN.viewDir), o.Normal));
	o.Emission += tex2D ( _MainTex, IN.uv_MainTex).rgb * 2 * pow (rim, 3.0);
	o.Alpha = tex2D ( _MainTex, IN.uv_MainTex).a;
}

ENDCG

}

Fallback " Glossy", 0
 
}
3 Likes

Thanks for the info guys, I was struggling with that one. Its funny, I’m doing something similar to what Sycle is doing (unifycommunity.com - unifycommunity Resources and Information.)

Wow, so we can combine Fixed Shader with Surface shader? This is interesting…

Thanks for posting this, sycle : )

Hey, is there anyone who can add this to the toon lighted shader? It would be really useful- You can have a dollar or two for doing it ;).
OR here, to save me a few bucks, would this part of code:

Pass {			
			Tags { "LightMode" = "Always" }
			AlphaTest Greater [_Cutoff]
			ZWrite Off
			ZTest Greater
			
			SetTexture [_MainTex] {
				constantColor [_Indicator]
				combine constant, texture
			}
		}

added into any shader using the same format work? I don’t need the texture, just the colour, so if I edited the SetTexture lines out, would it work?

EDIT: Forget it, I got it to work! YES!

Exactly what I was looking for, ChickenLord and FarFarer!

If this trick works it should definitely added to the documentation right next to where it mentions that surf shaders cannot be used in Pass {} blocks. And if it is already there, added with underlining :slight_smile:

1 Like

I think the reason the documents have been a little vague about using surface shaders in multiple passes is because although it does work there are some cases where it becomes a little unpredictable.

For instance if your first pass is using the geometry render queue and the second in a latter one such as transparent, the order of the transparent pass wont play well with other transparent objects( in my experience it no longer draws from back to front ).

Switching between deferred and forward passes also is unpredictable ( which i totally accept)

Another weird one Ive found is that Zwrites are ignored on multi pass transparent surface shaders as well.

Just thought id mention these to save others pulling their hair out.

Cheers
Brn

I’ve been playing around with multiple passes, and I think what’s actually going on is that each Subshader can only have one set of Tags associated with it, and it just takes the first definition for each tag mentioned and uses those values for all passes - meaning you can’t have multiple passes in different queues in a single shader (I’m not able to anyways). So the reason the back to front thing was messed up was probably both your geometry pass and the transparent pass of your shader rendered in the geometry layer, making it so your transparent pass was below all other transparent items.

… the way this is screwing things up for me, is IgnoreProjector - I want to have a multi pass shader where the self-illuminated parts of the scene render above blob shadow projectors, and it’s not working because I can’t make the self illuminated part render without the projector :frowning:

@thorbrian: Were you able to resolve this issue? Or did you have to resort to using multiple materials?

Personally I haven’t found a way around mixing Deferred and Forward passes or the render queue issue. Its a pity because it would open up many of possibility’s.

Is it possible to combine multiple surface shaders?
As opposed to combining a surface shader with non- surface shaders.