Blacksmith Wrinkle Maps in Fragment shader

So, I have been experimenting with animated wrinkle maps from the blacksmith demo. The example given along with black smith demo is in a custom surface shader. I want to replicate the same in a fragment shader. I have coded a very basic Normal Map shader and incorporated the normal map lines from the surface shader and made some adjustments.

This is what I have understood from the surface shader code. The _NormalAndOcclusion is being written to from in screenspace buffer and is being handled by a different shader and script. I output the normal map themselves as color from the fragment shader to visualize. Here are the screen shots

Fragment Shader Test Failed(The normals are in world space? Not sure)

Blacksmith surface shader with normal as Albedo output from surface shader.

Here is surface shader given by unity in blacksmith the demo. Line number 128 onwards is where the wrinkle map part is.

Shader "Volund/Standard Character (Specular, Surface)" {
    Properties {
        _Color("Color", Color) = (1,1,1,1)
        _MainTex("Albedo", 2D) = "white" {}
       
        _Cutoff("Alpha Cutoff", Range(0.0, 1.0)) = 0.5

        _Glossiness("Smoothness", Range(0.0, 1.0)) = 0.5
        _SpecColor("Specular", Color) = (0.2,0.2,0.2)
        _SpecGlossMap("Specular", 2D) = "white" {}

        _BumpScale("Scale", Float) = 1.0
        _BumpMap("Normal Map", 2D) = "bump" {}
       
        _Parallax ("Height Scale", Range (0.005, 0.08)) = 0.02
        _ParallaxMap ("Height Map", 2D) = "black" {}
       
        _OcclusionStrength("Strength", Range(0.0, 1.0)) = 1.0
        _OcclusionMap("Occlusion", 2D) = "white" {}
       
        _EmissionColor("Color", Color) = (0,0,0)
        _EmissionMap("Emission", 2D) = "white" {}
       
        _DetailMask("Detail Mask", 2D) = "white" {}
       
        _DetailAlbedoMap("Detail Albedo x2", 2D) = "grey" {}
        _DetailNormalMapScale("Scale", Float) = 1.0
        _DetailNormalMap("Normal Map", 2D) = "bump" {}

        [Enum(UV0,0,UV1,1)] _UVSec ("UV Set for secondary textures", Float) = 0

        // Blending state
        [HideInInspector] _Mode ("__mode", Float) = 0.0
        [HideInInspector] _SrcBlend ("__src", Float) = 1.0
        [HideInInspector] _DstBlend ("__dst", Float) = 0.0
        [HideInInspector] _ZWrite ("__zw", Float) = 1.0
       
        // Volund properties
        [HideInInspector] _CullMode ("__cullmode", Float) = 2.0
        [HideInInspector] _SmoothnessInAlbedo ("__smoothnessinalbedo", Float) = 0.0
        [HideInInspector] _SmoothnessTweaks ("__smoothnesstweaks", Vector) = (1,0,0,0)
        _SmoothnessTweak1("Smoothness Scale", Range(0.0, 2.0)) = 1.0
        _SmoothnessTweak2("Smoothness Bias", Range(-1.0, 1.0)) = 0.0
        _SpecularMapColorTweak("Specular Color Tweak", Color) = (1,1,1,1)
    }
   
    SubShader {
        Tags { "RenderType"="Opaque" "Special"="Wrinkles" "PerformanceChecks"="False" }
        LOD 300
       
        Blend [_SrcBlend] [_DstBlend]
        ZWrite [_ZWrite]
        Cull [_CullMode]

        CGPROGRAM
        // - Physically based Standard lighting model, specular workflow
        // - 'fullforwardshadows' to enable shadows on all light types
        // - 'addshadow' to ensure alpha test works for depth/shadow passes
        // - 'keepalpha' to allow alpha blended output options
        // - 'interpolateview' because that's what the non-surface Standard does
        // - Custom vertex function to setup detail UVs as expected by Standard shader
        // - Custom finalcolor function to output controlled final alpha
        // - 'nolightmap' and 'nometa' since this shader is only for dynamic objects
        // - 'exclude_path:prepass' since we have no use for this legacy path
    
        #pragma surface SurfSpecular StandardSpecular fullforwardshadows addshadow keepalpha interpolateview vertex:StandardSurfaceVertex finalcolor:StandardSurfaceSpecularFinal nolightmap nometa exclude_path:deferred exclude_path:prepass

        // Use shader model 3.0 target, to get nicer looking lighting (PBS toggles internally on shader model)
        #pragma target 3.0

        // This shader probably works fine for console/mobile platforms as well, but
        // these are the ones we've actually tested.
        #pragma only_renderers d3d11 d3d9 opengl glcore
       
        // Standard shader feature variants
        #pragma shader_feature _NORMALMAP
        #pragma shader_feature _ _ALPHATEST_ON _ALPHABLEND_ON _ALPHAPREMULTIPLY_ON
        #pragma shader_feature _SPECGLOSSMAP
        #pragma shader_feature _DETAIL_MULX2
       
        // Standard, but unused in this project
        //#pragma shader_feature _EMISSION
        //#pragma shader_feature _PARALLAXMAP
       
        // Volund additional variants
        #pragma multi_compile _ WRINKLE_MAPS
        #pragma multi_compile UNITY_SAMPLE_FULL_SH_PER_PIXEL

        // Volund uniforms
        uniform sampler2D    _NormalAndOcclusion;
        uniform half3         _SpecularMapColorTweak;
        uniform half2        _SmoothnessTweaks;

        // Need screen pos if wrinkle maps are active
#ifdef WRINKLE_MAPS
        struct SurfaceInput {
            float4    texcoord;
    #ifdef _PARALLAXMAP
            half3    viewDir;
    #endif
            float4    screenPos;
        };
        #define Input SurfaceInput
#endif
       
        // Include all the Standard shader surface helpers
        #include "UnityStandardSurface.cginc"

        // Our main surface entry point
        void SurfSpecular(Input IN, inout SurfaceOutputStandardSpecular o)
        {
            StandardSurfaceSpecular(IN, o);

            // Apply specular color tweak if we sampled spec color from a texture
#ifdef _SPECGLOSSMAP
            o.Specular *= _SpecularMapColorTweak;
#endif

            // Optionally sample smoothness from albedo texture alpha channel instead of sg texture
#ifdef SMOOTHNESS_IN_ALBEDO
            o.Smoothness = tex2D(_MainTex, IN.texcoord.xy).a;
#endif

            // Apply smoothness scale and bias (always active)
            o.Smoothness = saturate(o.Smoothness * _SmoothnessTweaks.x + _SmoothnessTweaks.y);

            // Sample occlusion and normals from screen-space buffer when wrinkle maps are active
#ifdef WRINKLE_MAPS
            float3 normalOcclusion = tex2D(_NormalAndOcclusion, IN.screenPos.xy / IN.screenPos.w).rgb;
            o.Occlusion = normalOcclusion.r;
    #ifdef _NORMALMAP
            o.Normal.xy = normalOcclusion.gb * 2.f - 1.f;
            o.Normal.z = sqrt(saturate(1.f - dot(o.Normal.xy, o.Normal.xy)));
            o.Albedo = o.Normal;
    #endif
#endif
        }
        ENDCG
    }
   
    CustomEditor "VolundMultiStandardShaderGUI"
    FallBack "Diffuse"
}

Here is my fragment Shader. Line number 77 onward should be where wrinkle map lines have been pasted and modified a bit.

// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'

Shader "Custom/VidRimTex" {
    Properties {
        _MainTex("Texture", 2D) = "white" {}
        _NormalMap("Normal Map", 2D) = "bump" {}
        _BumpDepth("Bump Depth", Float) = 1
    }
    SubShader {
        Pass{
        Tags{ "LightMode" = "ForwardBase"}
        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag

        #pragma target 3.0
        #pragma multi_compile _ WRINKLE_MAPS
        #include "UnityCG.cginc"

        struct VertexInput {
            float4 position: POSITION;
            float3 normal: NORMAL;
            float4 tex: TEXCOORD0;
            float4 tangent: TANGENT;
        };
       
        struct VertexOutput {
            float4 position: POSITION;
            float4 tex: TEXCOORD0;
            float3 posWorld: POS;
            float3 normalWorld: TEXCOORD1;
            float3 tangentWorld: TEXCOORD2;
            float3 binormalWorld: TEXCOORD3;
            float4 screenPos: TEXCOORD4;
        };

        sampler2D _MainTex;
        sampler2D _NormalMap;
        sampler2D _NormalAndOcclusion;
        float4 _MainTex_ST;
        float4 _LightColor0;
        float _BumpDepth;
       
        VertexOutput vert(VertexInput i){
            VertexOutput o;
           
            o.posWorld = mul(unity_ObjectToWorld, i.position);
            o.position = mul(UNITY_MATRIX_MVP, i.position);
            o.tex = i.tex;
            float4 pos = UnityObjectToClipPos(i.position);
            o.screenPos = ComputeScreenPos (pos);

            o.normalWorld = normalize(mul(float4(i.normal, 0.0), unity_WorldToObject).xyz);
            o.tangentWorld = normalize(mul(unity_ObjectToWorld, i.tangent).xyz);
            o.binormalWorld = normalize(cross(o.normalWorld, o.tangentWorld) * i.tangent.w);
           
            return o;
        }
       
        float4 frag(VertexOutput o) : COLOR{
            float3 lightDirection = normalize(_WorldSpaceLightPos0.xyz);
            float3 viewDirection = normalize( _WorldSpaceCameraPos.xyz - o.posWorld.xyz );

//            float3x3 local2WorldTranspose = float3x3(
//                    float3(o.tangentWorld.x, o.binormalWorld.x, o.normalWorld.x),
//                    float3(o.tangentWorld.y, o.binormalWorld.y, o.normalWorld.y),
//                    float3(o.tangentWorld.z, o.binormalWorld.z, o.normalWorld.z)
//                );

            float3x3 local2WorldTranspose = float3x3(
                    o.tangentWorld,
                    o.binormalWorld,
                    o.normalWorld
                );

            #ifdef WRINKLE_MAPS

                //Taken from source exactly
                float3 normalOcclusion = tex2D(_NormalAndOcclusion, o.screenPos.xy / o.screenPos.w).rgb;
//                float3 rescaledNormal = float3((2 * normalOcclusion.gb) - float2( 1, 1 ), _BumpDepth);

                float3 rescaledNormal;
                rescaledNormal.xy = normalOcclusion.gb * 2.f - 1.f;
                rescaledNormal.z = sqrt(saturate(1.f - dot(rescaledNormal.xy, rescaledNormal.xy)));
                //End of Taken from source exactly

                float3 normalDirection = mul( rescaledNormal, local2WorldTranspose );

                return float4(normalDirection, 1);
            #else
                float3 rescaledNormal = UnpackNormal(tex2D(_NormalMap, o.tex.xy));
//                float4 normalColor = tex2D(_NormalMap, o.tex.xy);
//                float3 rescaledNormal = float3((2 * normalColor.ag) - float2( 1, 1 ), _BumpDepth);

                float3 normalDirection = normalize( mul( rescaledNormal, local2WorldTranspose ));

                float3 finalDiffuseColor = _LightColor0 * saturate(dot(normalDirection, lightDirection));

                float4 texColor = tex2D(_MainTex, o.tex.xy);

                return float4(normalDirection, 1);
//                return float4(finalDiffuseColor * texColor, 1);
            #endif
        }
       
        ENDCG
        }
    }
    FallBack "Diffuse"
}

I’d appreciate it if anybody from Unity could help me with this I’m desperate for an answer.

Hi @sanmn19

To compare apples to apples you should be outputting rescaledNormal from your fragment shader, since what you’re dumping from the surface shader is in tangent space. It should be identical to the debug output you’re getting from the surface shader. I’d also suggest you dump normals using n*0.5+0.5 so you see the entire data range instead of negative values as black.

Personally, I’d start by looking in the frame debugger to make sure the screen space texture is passed correctly to your fragment shader. (compare it to the surface one)

Regards, Torbjorn

Hey Torbjorn. Thanks for the reply. It helped me debug my output. I finally got it to work. It was mainly because of the missing “special” = “wrinkle” tag along with other minor issues.