Double sided normal mapped shader

Hi
I wanted to make a double sided version of the built-in “Transparent/Cutout/Bumped Specular” shader, but soon realised how the normals of the normal map would render lighting wrong on the back side, due to the tangent space rotation being wrong.
Because of that I decided to make the shader with two passes, first culling the front, and then the back. I then took the backside pass, and reversed the whole rotation thing:

// Change the rotation matrice done per vertex by inverting the tangent (removed the macro: TANGENT_SPACE_ROTATION, and did the thing directly in the shader) 

float3 binormal = cross( v.normal, -v.tangent.xyz) * v.tangent.w;
	float3x3 rotation = float3x3( -v.tangent.xyz, binormal, v.normal );
//Invert each normal in the fragment program

float3 normal = -(tex2D(_BumpMap, i.uv2).xyz * 2 - 1);

This works like a charm, and looks great, but I also feel like my little home thought hack is just clumsy. Is there a right way of doing this? Also, instead of doing two passes, I might just want to check if a given vertex is front or back, and then do the right rotation matrice. How would I check that? Would it be cheaper, or does the pre culling of vertices within the hardware work so fast, an “if-else” setup would be heavier?

EDIT: Had done some weird stuff to reverse the vectors (normal += 0-(2*normal)). It worked correct, just not very smart. Don’t know what was going on in my head…

EDIT, EDIT:
Just invert the normal in the rotation matrix, and the binormals inverted status related to the uv’s (as they are mirrored on the backside) will make your normals on the map be correct as well. Why didn’t I just test it like this from the start…
(answer: because I’m stupid. Always takes a couple of days (and sleep) to wrap my head around stuff like this… I want to be Feynman, but an alive version surely)

float3 binormal = cross( v.normal, v.tangent.xyz) * v.tangent.w; 
   float3x3 rotation = float3x3( v.tangent.xyz, binormal, -v.normal );

just have a look at this shader.
although it has some bugs it might be helpful.

bugs:

  • it produces artifacts: some kind of bright glow around the texture edge where the geometry receives shadows. i guess it is some kind of alpha blending issue… but using just a single pass should work correctely
  • backface does not get written into the shadow map’s depth buffer
    see: http://forum.unity3d.com/viewtopic.php?t=40440
Shader "Hidden/TerrainEngine/Details/Vertexlit" { 
   Properties { 
   // we get no color? 
   _Color ("Main Color", Color) = (1,1,1,1) 
   _MainTex ("Base (RGB) Trans (A)", 2D) = "white" {} 
   _Cutoff ("Alpha cutoff", Range(0,1)) = 0.5 
} 

Category { 
   Tags {"RenderType"="TransparentCutout"} 
   LOD 200 
   Alphatest Greater [_Cutoff] 
   AlphaToMask True 
   ColorMask RGB 
   Blend AppSrcAdd AppDstAdd 
   Cull Off 
    
    
    
   // ------------------------------------------------------------------ 
   // ARB fragment program 
    
   SubShader { 
      // Ambient pass 
      Pass { 
         Name "BASE" 
         Tags {"LightMode" = "PixelOrNone"} 
         Color [_PPLAmbient] 
         SetTexture [_MainTex] {constantColor [_Color] Combine texture * primary DOUBLE, texture * primary} 
      } 
        
      // Vertex lights 
      // not supported? 
      /* 
      Pass { 
         Name "BASE" 
         Tags {"LightMode" = "Vertex"} 
         Lighting On 
         Material { 
            Diffuse [_Color] 
            Emission [_PPLAmbient] 
         } 
         SetTexture [_MainTex] {combine texture * primary DOUBLE, texture * primary} 
      } 
      */ 
      
      // Pixel lights 
      Pass {    
         Name "PPL" 
         Tags { "LightMode" = "Pixel" } 
         Fog { Color [_AddFog] } 

CGPROGRAM 
#pragma fragment frag 
#pragma vertex vert 
#pragma multi_compile_builtin 
#pragma fragmentoption ARB_fog_exp2 
#pragma fragmentoption ARB_precision_hint_fastest 
#include "UnityCG.cginc" 
#include "AutoLight.cginc" 

struct v2f { 
   V2F_POS_FOG; 
   LIGHTING_COORDS 
   float2   uv; 
   float3   normal; 
   float3   lightDir; 
   float3   vertex; 
}; 

uniform float4 _MainTex_ST; 

v2f vert (appdata_base v) 
{ 
   v2f o; 
   PositionFog( v.vertex, o.pos, o.fog ); 
    
   //Viewdir in object space 
   // see: [url]http://forum.unity3d.com/viewtopic.php?t=12015[/url] 
   float3 ovd = v.vertex.xyz - _ObjectSpaceCameraPos; 
   float dp = dot(ovd, v.normal);    
   //If viewdir is same direction-ish as normal, flip it. 
   o.normal = v.normal * -sign(dp); 

   o.uv = TRANSFORM_TEX(v.texcoord,_MainTex); 
   o.lightDir = ObjSpaceLightDir( v.vertex ); 
   TRANSFER_VERTEX_TO_FRAGMENT(o); 
   return o; 
} 

uniform sampler2D _MainTex; 
uniform float4 _Color; 

float4 frag (v2f i) : COLOR 
{ 
   half4 texcol = tex2D( _MainTex, i.uv );    
   // its is not written to the shadowmap correctely... 

   #ifndef USING_DIRECTIONAL_LIGHT 
   i.lightDir = normalize(i.lightDir); 
   #endif 
    
   // original lighting 
   //half c = DiffuseLight( i.lightDir, i.normal, texcol, LIGHT_ATTENUATION(i) ); 
    
   half diffuse = dot( i.normal, i.lightDir ); 
   /// lighting hack: some baselight + diffuse lighting * x    
   diffuse = 0.4 + diffuse * 0.35; 
   half4 c; 
   c.rgb = texcol.rgb * _ModelLightColor0.rgb * (diffuse * LIGHT_ATTENUATION(i) * 2); 
    
   //c.a = 0; // diffuse passes by default don't contribute to overbright    
   c.a = texcol.a * _Color.a; 
   //c.a = texcol.a; 
   return c; 
} 
ENDCG 
         SetTexture [_MainTex] {combine texture} 
         SetTexture [_LightTexture0] {combine texture} 
         SetTexture [_LightTextureB0] {combine texture} 
          
      } 
    /// end 1st pass 
    
    /// 2nd pass 
      Pass { 
         Name "PPL2" 
         Tags { "LightMode" = "Pixel" } 
         // Only render pixels less or equal to the value 
         AlphaTest LEqual [_Cutoff] 
                  
         // Set up alpha blending 
         Blend SrcAlpha OneMinusSrcAlpha 
         // Dont write to the depth buffer 
         ZWrite Off 
         SetTexture [_MainTex] { constantColor [_Color] Combine texture * constant, texture * constant} 
      } 
    /// end 2nd pass 
    
   }    
    
    
    
    
    // ------------------------------------------------------------------ 
   // Radeon 9000 

   SubShader { 
      // Ambient pass 
      Pass { 
         Name "BASE" 
         Tags {"LightMode" = "PixelOrNone"} 
         Color [_PPLAmbient] 
         SetTexture [_MainTex] {constantColor [_Color] Combine texture * primary DOUBLE, texture * constant} 
      } 
      // Vertex lights 
      Pass { 
         Name "BASE" 
         Tags {"LightMode" = "Vertex"} 
         Lighting On 
         Material { 
            Diffuse [_Color] 
            Emission [_PPLAmbient] 
         } 
         SetTexture [_MainTex] {Combine texture * primary DOUBLE, texture * primary} 
      } 
        
      // Pixel lights with 0 light textures 
      Pass { 
         Name "PPL" 
         Tags { 
            "LightMode" = "Pixel" 
            "LightTexCount" = "0" 
         } 

CGPROGRAM 
#pragma vertex vert 
#include "UnityCG.cginc" 

struct v2f { 
   V2F_POS_FOG; 
   float2 uv      : TEXCOORD0; 
   float3 normal   : TEXCOORD1; 
   float3 lightDir   : TEXCOORD2; 
}; 

uniform float4 _MainTex_ST; 

v2f vert(appdata_base v) 
{ 
   v2f o; 
   PositionFog( v.vertex, o.pos, o.fog ); 
   o.normal = v.normal; 
   o.uv = TRANSFORM_TEX(v.texcoord,_MainTex); 
   o.lightDir = ObjSpaceLightDir( v.vertex ); 
   return o; 
} 
ENDCG 
         Program "" { 
            SubProgram { 
               Local 0, [_ModelLightColor0] 
               Local 1, [_Color] 

"!!ATIfs1.0 
StartConstants; 
   CONSTANT c0 = program.local[0]; 
   CONSTANT c1 = program.local[1]; 
EndConstants; 

StartOutputPass; 
   SampleMap r0, t0.str;         # main texture 
   SampleMap r1, t2.str;         # normalized light dir 
   PassTexCoord r2, t1.str;      # normal 
    
   DOT3 r5.sat, r2, r1.2x.bias;   # R5 = diffuse (N.L) 
    
   MUL r0.rgb, r0, r5; 
   MUL r0.rgb.2x, r0, c0; 
   MUL r0.a, r0, c1; 
EndPass; 
" 
            } 
         } 
         SetTexture[_MainTex] {combine texture} 
         SetTexture[_CubeNormalize] {combine texture} 
      } 
        
      // Pixel lights with 1 light texture 
      Pass { 
         Name "PPL" 
         Tags { 
            "LightMode" = "Pixel" 
            "LightTexCount" = "1" 
         } 

CGPROGRAM 
#pragma vertex vert 
#include "UnityCG.cginc" 

uniform float4 _MainTex_ST; 
uniform float4x4 _SpotlightProjectionMatrix0; 

struct v2f { 
   V2F_POS_FOG; 
   float2 uv      : TEXCOORD0; 
   float3 normal   : TEXCOORD1; 
   float3 lightDir   : TEXCOORD2; 
   float4 LightCoord0 : TEXCOORD3; 
}; 

v2f vert(appdata_tan v) 
{ 
   v2f o; 
   PositionFog( v.vertex, o.pos, o.fog ); 
   o.normal = v.normal; 
   o.uv = TRANSFORM_TEX(v.texcoord,_MainTex); 
   o.lightDir = ObjSpaceLightDir( v.vertex ); 
    
   o.LightCoord0 = mul(_SpotlightProjectionMatrix0, v.vertex); 
    
   return o; 
} 
ENDCG 
         Program "" { 
            SubProgram { 
               Local 0, [_ModelLightColor0] 
               Local 1, [_Color] 

"!!ATIfs1.0 
StartConstants; 
   CONSTANT c0 = program.local[0]; 
   CONSTANT c1 = program.local[1]; 
EndConstants; 

StartOutputPass; 
   SampleMap r0, t0.str;         # main texture 
   SampleMap r1, t2.str;         # normalized light dir 
   PassTexCoord r4, t1.str;      # normal 
   SampleMap r2, t3.str;         # a = attenuation 
    
   DOT3 r5.sat, r4, r1.2x.bias;   # R5 = diffuse (N.L) 
    
   MUL r0.rgb, r0, r5; 
   MUL r0.rgb.2x, r0, c0; 
   MUL r0.rgb, r0, r2.a;         # attenuate 
   MUL r0.a, r0, c1; 
EndPass; 
" 
            } 
         } 
         SetTexture[_MainTex] {combine texture} 
         SetTexture[_CubeNormalize] {combine texture} 
         SetTexture[_LightTexture0] {combine texture} 
      } 
        
      // Pixel lights with 2 light textures 
      Pass { 
         Name "PPL" 
         Tags { 
            "LightMode" = "Pixel" 
            "LightTexCount" = "2" 
         } 
CGPROGRAM 
#pragma vertex vert 
#include "UnityCG.cginc" 

uniform float4 _MainTex_ST; 
uniform float4x4 _SpotlightProjectionMatrix0; 
uniform float4x4 _SpotlightProjectionMatrixB0; 

struct v2f { 
   V2F_POS_FOG; 
   float2 uv      : TEXCOORD0; 
   float3 normal   : TEXCOORD1; 
   float3 lightDir   : TEXCOORD2; 
   float4 LightCoord0 : TEXCOORD3; 
   float4 LightCoordB0 : TEXCOORD4; 
}; 

v2f vert(appdata_tan v) 
{ 
   v2f o; 
   PositionFog( v.vertex, o.pos, o.fog ); 
   o.normal = v.normal; 
   o.uv = TRANSFORM_TEX(v.texcoord,_MainTex); 
   o.lightDir = ObjSpaceLightDir( v.vertex ); 
    
   o.LightCoord0 = mul(_SpotlightProjectionMatrix0, v.vertex); 
   o.LightCoordB0 = mul(_SpotlightProjectionMatrixB0, v.vertex); 
    
   return o; 
} 
ENDCG 
         Program "" { 
            SubProgram { 
               Local 0, [_ModelLightColor0] 
               Local 1, [_Color] 

"!!ATIfs1.0 
StartConstants; 
   CONSTANT c0 = program.local[0]; 
   CONSTANT c1 = program.local[1]; 
EndConstants; 

StartOutputPass; 
   SampleMap r0, t0.str;         # main texture 
   SampleMap r1, t2.str;         # normalized light dir 
   PassTexCoord r4, t1.str;      # normal 
   SampleMap r2, t3.stq_dq;      # a = attenuation 1 
   SampleMap r3, t4.stq_dq;      # a = attenuation 2 
    
   DOT3 r5.sat, r4, r1.2x.bias;   # R5 = diffuse (N.L) 
    
   MUL r0.rgb, r0, r5; 
   MUL r0.rgb.2x, r0, c0; 
   MUL r0.rgb, r0, r2.a;         # attenuate 
   MUL r0.rgb, r0, r3.a; 
   MUL r0.a, r0, c1; 
EndPass; 
" 
            } 
         } 
         SetTexture[_MainTex] {combine texture} 
         SetTexture[_CubeNormalize] {combine texture} 
         SetTexture[_LightTexture0] {combine texture} 
         SetTexture[_LightTextureB0] {combine texture} 
      } 
   } 
    
   // ------------------------------------------------------------------ 
   // Radeon 7000 
    
   Category { 
      Material { 
         Diffuse [_Color] 
         Emission [_PPLAmbient] 
      } 
      Lighting On 
      Cull Off 
      SubShader { 
         Pass { 
            Name "BASE" 
            Tags {"LightMode" = "PixelOrNone"} 
            Color [_PPLAmbient] 
            Lighting Off 
            SetTexture [_MainTex] {Combine texture * primary DOUBLE} 
            SetTexture [_MainTex] {Combine texture * primary DOUBLE} 
            SetTexture [_MainTex] {Combine texture * primary DOUBLE, primary * texture} 
         } 
         Pass {      
            Name "BASE" 
            Tags {"LightMode" = "Vertex"} 
            SetTexture [_MainTex] {Combine texture * primary DOUBLE, primary * texture} 
         } 
         Pass { 
            Name "PPL" 
            Tags { 
               "LightMode" = "Pixel" 
               "LightTexCount" = "2" 
            } 
            SetTexture [_LightTexture0]    { combine previous * texture alpha, previous } 
            SetTexture [_LightTextureB0]   { 
               combine previous * texture alpha + constant, previous 
               constantColor [_PPLAmbient] 
            } 
            SetTexture [_MainTex]    { combine previous * texture DOUBLE, primary * texture} 
         } 
         Pass { 
            Name "PPL" 
            Tags { 
               "LightMode" = "Pixel" 
               "LightTexCount"  = "1" 
            } 
            SetTexture [_LightTexture0] { 
               combine previous * texture alpha + constant, previous 
               constantColor [_PPLAmbient] 
            } 
            SetTexture [_MainTex]    { combine previous * texture DOUBLE, primary * texture} 
         } 
         Pass { 
            Name "PPL" 
            Tags { 
               "LightMode" = "Pixel" 
               "LightTexCount"  = "0" 
            } 
            SetTexture [_MainTex]    { combine previous * texture DOUBLE, primary * texture} 
         } 
      } 
   } 
} 

Fallback "Transparent/Cutout/VertexLit", 2 

}

Hmmm…
It doesn’t seem that shader really deal with the same problems as mine. Normals in the detail shader are straight out from the mesh, whereas normals on a normalmapped shader can be all sorts of directions. As the backside of a double sided shader will have inverted UV’s as well, I can’t just invert the normal. That would give me wrong inverted values on two of the vector components.

That is why I invert the tangent as well, before doing the tangent rotation.
And it really works perfectly.
In your example that isn’t needed, because the detail normals, always will be lying perpendicular to the calculated binormal-tangent plane.
Unless I misread/misunderstand something…
But thanks for your suggestions! :slight_smile:

Just double your geometry. It’s easier and faster. Even a perfect double-sided, lit shader requires twice as many passes, which is twice as many draw calls.

Yeah… I know. But our character artist is sick and tired of having to deal with double geometry on hair etc. If he wants to update the model, he needs to work on double the geometry, or delete the doubled version, make his modifications, and re-double it (and reskin it).
It just feels like the exstra amount of drawcalls is worth it in these cases, to make the life easier for the artist. At least while he’s working on the character, testing stuff out.
Then, when everything is finished, you can double the geometry, and shift to a normal, one-sided shader.

I actually just found a cheaper way. Instead of inverting each normal in the fragment program (alongside the inversion of the tangent), I just did one overall inversion of the normal in the rotation matrix, and removed the inversion of the tangent. I suddenly realised (my head is obviously not that suited for these games, unless time go by… :smile: ) how the binormal will be inverted automatically, in relation to the uv’s, just by inverting the normal. And then the otherwise wrong normals of the map will be correct because of the binormal inversion. So… that’s cool!

float3 binormal = cross( v.normal, v.tangent.xyz) * v.tangent.w;
	float3x3 rotation = float3x3( v.tangent.xyz, binormal, -v.normal );

I’m glad you figured it out. However, you’re still probably better off with an AssetPostprocessor that doubles geometry on import. No new shaders, no work for the artist.