Confusion about worldNormal in Surface shader

I’m trying to get worldNormal to work in a surface shader together with a normal map, but it’s proving difficult.

The documentation mentions you need to use worldNormal; INTERAL_DATA if writing to o.Normal, and then access it using WorldNormalVector, like so:

struct Input {
    float3 worldNormal; INTERNAL_DATA
};
float3 worldNormal = WorldNormalVector(IN, o.Normal);

However, the result of doing this isn’t what I’m expecting.

Here are two simple fresnel surface shaders - one writing to o.Normal and using WorldNormalVector, the other one does not. This is what you get:

As you can see, the starting right sphere behaves very oddly.

Here are the two shaders:

Writing to o.Normal and using WorldNormalVector (not working)

Shader "Custom/NormalTest" {
    Properties {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _BumpMap ("Normalmap", 2D) = "bump" {}
        _FresnelPower ("FresnelPower", Range(0,1)) = 0.5
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200
      
        CGPROGRAM
        // Physically based Standard lighting model, and enable shadows on all light types
        #pragma surface surf Standard fullforwardshadows

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

        sampler2D _MainTex;
        sampler2D _BumpMap;

        struct Input {
            float2 uv_MainTex;
            float3 worldNormal; INTERNAL_DATA
            float3 viewDir;
        };

        half _FresnelPower;
        fixed4 _Color;

        void surf (Input IN, inout SurfaceOutputStandard o) {
            // Albedo comes from a texture tinted by color
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * 0;
            o.Albedo = c.rgb;
            o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_MainTex));
            // Metallic and smoothness come from slider variables
            // o.Metallic = _Metallic;
            // o.Smoothness = _Glossiness;
            o.Alpha = c.a;
          
            float3 worldNormal = WorldNormalVector(IN, o.Normal);
            float fresnel = pow(abs(1 - dot(IN.viewDir, worldNormal)), _FresnelPower * 10);
          
            o.Emission = fresnel;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

Not writing to o.Normal (working as expected):

Shader "Custom/NormalTest 1" {
    Properties {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        // _BumpMap ("Normalmap", 2D) = "bump" {}
        _FresnelPower ("FresnelPower", Range(0,1)) = 0.5
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200
      
        CGPROGRAM
        // Physically based Standard lighting model, and enable shadows on all light types
        #pragma surface surf Standard fullforwardshadows

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

        sampler2D _MainTex;
        // sampler2D _BumpMap;

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

        half _FresnelPower;
        fixed4 _Color;

        void surf (Input IN, inout SurfaceOutputStandard o) {
            // Albedo comes from a texture tinted by color
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * 0;
            o.Albedo = c.rgb;
            // o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_MainTex));
            // Metallic and smoothness come from slider variables
            // o.Metallic = _Metallic;
            // o.Smoothness = _Glossiness;
            o.Alpha = c.a;
          
            // float3 worldNormal = WorldNormalVector(IN, o.Normal);
            float fresnel = pow(abs(1 - dot(IN.viewDir, IN.worldNormal)), _FresnelPower * 10);
          
            o.Emission = fresnel;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

What am I doing wrong?

The view dir for surface shaders is already in tangent space, so you just need to do:
float fresnel = pow(abs(1 - dot(IN.viewDir, float3(0,0,1)), _FresnelPower * 10);

Which if you’re not using normal maps can be simplified down to:
float fresnel = pow(abs(1 - IN.viewDir.z), _FresnelPower * 10);

2 Likes

Thanks a lot @bgolus ! Totally overlooked the fact that viewDir is in tangent space. This makes things so much easier.

Thanks again.

I know this is an old thread, but where is it documented that viewDir is in tangent space? The documentation for this seems very poor. I’ve been trying to reason about why viewDir.y is 0 when looking at a surface from top down (I had assumed it was in world space), and this thread provided the explanation I was looking for.

It’s not documented anywhere, and it’s only in tangent space if you’re also writing to o.Normal.

Shader "Custom/WorldViewDirSurfaceShader" {
    Properties {
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200
      
        CGPROGRAM
        // Physically based Standard lighting model, and enable shadows on all light types
        #pragma surface surf Standard fullforwardshadows

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

        struct Input {
            float3 viewDir;
        };

        void surf (Input IN, inout SurfaceOutputStandard o) {
            o.Emission = IN.viewDir; // world space view dir
        }
        ENDCG
    }
    FallBack "Diffuse"
}
Shader "Custom/TangentViewDirSurfaceShader" {
    Properties {
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200
      
        CGPROGRAM
        // Physically based Standard lighting model, and enable shadows on all light types
        #pragma surface surf Standard fullforwardshadows

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

        struct Input {
            float3 viewDir;
        };

        void surf (Input IN, inout SurfaceOutputStandard o) {
            o.Emission = IN.viewDir; // tangent space view dir because ...
            o.Normal = float3(0,0,1); // writing this line makes the Surface shader pass IN.viewDir to the surf function in tangent space
        }
        ENDCG
    }
    FallBack "Diffuse"
}
2 Likes

As usual you always shed a light in the darkest area on unity documentation

Ah, that would explain some other weird behavior I was seeing! I was trying to calculate the Blinn Phong specular value from within the surface shader, and I spent hours trying to understand why it was only “sort-of” working. It would be nice for them to add these details to the Shader Reference.