Tri-Planar textures with BumpMap

I’m not a shader writer but I needed a Tri-Planar shader for my project and was looking around the net collecting scraps and trying to combine them together in order to make a shader.

The shader I have now works as a Tri-Planar shader with MainColor.
I want to add BumpMap for this shader and I think I’m doing it right but what I get is a black shader.

Shader "Tri-Planar World Normal" {
  Properties {
        _Color ("Main Color", Color) = (1,1,1,1)
		_Side("Side", 2D) = "gray" {}
		_Top("Top", 2D) = "gray" {}
		_Bottom("Bottom", 2D) = "gray" {}
		_SideScale("Side Scale", Float) = 2
		_TopScale("Top Scale", Float) = 2
		_BottomScale ("Bottom Scale", Float) = 2
		_BumpMapSide ("Side Normal Map", 2D) = "bump" {}        
		_BumpMapTop ("Top Normal Map", 2D) = "bump" {}        
		_BumpMapBottom ("Bottom Normal Map", 2D) = "bump" {}        
	}
	
	SubShader {
		Tags {
			"Queue"="Geometry"
			"IgnoreProjector"="False"
			"RenderType"="Opaque"
		}
		

		Cull Back
		ZWrite On
		
		CGPROGRAM
		#pragma surface surf Lambert
		#pragma exclude_renderers flash

        half4 LightingCustom( SurfaceOutput s, half3 lightDir, half3 viewDir, half atten )

        {

            half pxlAtten = dot( lightDir, s.Normal );

            

            return half4(s.Albedo * pxlAtten,1.0);

        }
 
		struct Input {
			float3 worldPos;
			float3 worldNormal;
	        //float2 uv_BumpMapSide;
	        INTERNAL_DATA

		};

		sampler2D _Side, _Top, _Bottom;
		sampler2D _BumpMapSide, _BumpMapTop, _BumpMapBottom;

		float _SideScale, _TopScale, _BottomScale;
		fixed4 _Color;
		
        sampler2D _NormalMap;
			
		void surf (Input IN, inout SurfaceOutput o) {
			float3 projNormal = saturate(pow(IN.worldNormal * 1.4, 4));
			
			//Adjustments//
			fixed3 dY;
			fixed3 dZ;
			fixed3 dX;	
				
			sampler2D _cSide = _Side;
			sampler2D _cTop = _Top;
			sampler2D _cBottom = _Bottom;
			fixed4 cZ;
			fixed4 cX;
			fixed4 cY;
			
			
			// SIDE X
			float3 x = tex2D(_cSide, frac(IN.worldPos.zy * _SideScale)) * abs(IN.worldNormal.x);
			//NORMAL//
			dX = UnpackNormal(tex2D(_BumpMapSide, frac(IN.worldPos.zy * _SideScale)) * abs(IN.worldNormal.x));		

			// TOP / BOTTOM
			float3 y = 0;
			if (IN.worldNormal.y > 0) {
				y = tex2D(_cTop, frac(IN.worldPos.zx * _TopScale)) * abs(IN.worldNormal.y);
				//NORMAL//
				dY = UnpackNormal(tex2D(_BumpMapTop, frac(IN.worldPos.zx * _TopScale)) * abs(IN.worldNormal.y));
				
			} else {
				y = tex2D(_cBottom, frac(IN.worldPos.zx * _BottomScale)) * abs(IN.worldNormal.y);
				//NORMAL//
				dY = UnpackNormal(tex2D(_BumpMapBottom, frac(IN.worldPos.zx * _BottomScale)) * abs(IN.worldNormal.y));				
			}
			
			// SIDE Z	
			float3 z = tex2D(_cSide, frac(IN.worldPos.xy * _SideScale)) * abs(IN.worldNormal.z);
			//NORMAL//
			dZ = UnpackNormal(tex2D(_BumpMapSide, frac(IN.worldPos.xy * _SideScale)) * abs(IN.worldNormal.z));
			
			
			// Add Normal//
			
            half3 tempBump = dZ;
            tempBump = lerp(tempBump, dX, projNormal.x);
            tempBump = lerp(tempBump, dY, projNormal.y); 
            half3 resultBump = (tempBump);
			//Add Color//
			cZ.rgb = z * _Color.rgb;
			cX.rgb = x * _Color.rgb;
			cY.rgb = y * _Color.rgb;
			
			o.Albedo = cZ.rgb;
			o.Albedo = lerp(o.Albedo, cX.rgb, projNormal.x);
			o.Albedo = lerp(o.Albedo, cY.rgb, projNormal.y);
        	o.Normal = normalize(resultBump);
			//o.Normal = UnpackNormal(tex2D(_BumpMapSide, frac(IN.worldPos.xy * _SideScale)));
			//o.Normal = o.Albedo;
			//Original//
			//o.Albedo = z;
			//o.Albedo = lerp(o.Albedo, x, projNormal.x);
			//o.Albedo = lerp(o.Albedo, y, projNormal.y);

			
		} 
		ENDCG
	}
	Fallback "Diffuse"
}

OK, I’ve figured out a quasi work-around: If you’re writing o.Normal, use this to get the un-perturbed world normal instead of IN.worldNormal:

        float3 n = WorldNormalVector( IN, float3(0, 0, 1) );

According to my tests, @stevesan was only half way there.

If a shader writes to o.Normal, IN.worldNormal becomes invalid and WorldNormalVector(IN, o.Normal) should be used instead.

HOWEVER, what the documentation DOESN’T tell you is that before you can use WorldNormalVector, o.Normal should FIRST be initialized to (0,0,1).

So the solution is to initialize o.Normal at the very beginning of the shader, and use WorldNormalVector from then onwards. The new shader looks like this:

	void surf(Input IN, inout SurfaceOutput o) {
		o.Normal = float3(0, 0, 1);                       // Initialize o.Normal
		float3 n = WorldNormalVector(IN, o.Normal);       // This should now be correct
		float3 projNormal = saturate(pow(n * 1.4, 4));
        ...

And from that point on, replace all IN.worldNormal references with n