Search Unity

Confused on Example of surface shader

Discussion in 'Shaders' started by zhutianlun810, Jan 4, 2019.

  1. zhutianlun810

    zhutianlun810

    Joined:
    Sep 17, 2017
    Posts:
    171
    Hi,

    In this following example, Is the viewDir, s.Normal in world space or in local space?

    Code (CSharp):
    1.     CGPROGRAM
    2.     #pragma surface surf SimpleSpecular
    3.  
    4.     half4 LightingSimpleSpecular (SurfaceOutput s, half3 lightDir, half3 viewDir, half atten) {
    5.         half3 h = normalize (lightDir + viewDir);
    6.  
    7.         half diff = max (0, dot (s.Normal, lightDir));
    8.  
    9.         float nh = max (0, dot (s.Normal, h));
    10.         float spec = pow (nh, 48.0);
    11.  
    12.         half4 c;
    13.         c.rgb = (s.Albedo * _LightColor0.rgb * diff + _LightColor0.rgb * spec) * atten;
    14.         c.a = s.Alpha;
    15.         return c;
    16.     }
    17.  
    18.     struct Input {
    19.         float2 uv_MainTex;
    20.     };
    21.  
    22.     sampler2D _MainTex;
    23.  
    24.     void surf (Input IN, inout SurfaceOutput o) {
    25.         o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
    26.     }
    27.     ENDCG
    If they are in local space, how can I get the normal, viewDir, pixel position of world space in my custome lighting function?

    I am also confused on the description:
    • float3 worldNormal - contains world normal vector if surface shader does not write to o.Normal.
    • float3 worldNormal; INTERNAL_DATA - contains world normal vector if surface shader writes to o.Normal. To get the normal vector based on per-pixel normal map, use WorldNormalVector (IN, o.Normal).
    What do descriptions above mean?

    If I have:
    Code (CSharp):
    1.             void surf(Input IN, inout SurfaceOutputStandard o)
    2.             {
    3.                 fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
    4.                 o.Albedo = c.rgb;
    5.                 o.Metallic = _Metallic;
    6.                 o.Smoothness = _Glossiness;
    7.                 o.Alpha = c.a;
    8.                 o.p = IN.worldPos;
    9.                 o.Normal = IN.worldNormal;
    10.             }
    11.  
    12.             struct Input
    13.             {
    14.                 float2 uv_MainTex;
    15.                 float3 worldPos;
    16.                 float3 worldNormal;
    17.             };
    18.  
    19.             half4 LightingTest(SurfaceOutputStandard s, float3 viewDir, UnityGI gi)
    20.             {
    21.                 half oneMinusReflectivity;
    22.                 half3 specColor;
    23.                 s.Albedo = DiffuseAndSpecularFromMetallic(s.Albedo, s.Metallic, /*out*/ specColor, /*out*/ oneMinusReflectivity);
    24.  
    25.                 // shader relies on pre-multiply alpha-blend (_SrcBlend = One, _DstBlend = OneMinusSrcAlpha)
    26.                 // this is necessary to handle transparency in physically correct way - only diffuse component gets affected by alpha
    27.                 half outputAlpha;
    28.                 s.Albedo = PreMultiplyAlpha(s.Albedo, s.Alpha, oneMinusReflectivity, /*out*/ outputAlpha);
    29.                 half4 c = getColorArea(s.Albedo, specColor, s.Smoothness, s.Normal, viewDir, s.p);
    30.                 c.a = outputAlpha;
    31.                 return c;
    32.             }
    Are both s.Normal and s.p in world space in my LightingTest function?
     
    Last edited: Jan 4, 2019
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    Surface shaders require that the final o.Normal value output by the surf function is in tangent space. So o.Normal = IN.worldNormal would result in some very odd results. Actually, more specifically, it would result in o.Normal = float3(0,0,0); because IN.worldNormal is left uninitialized if the surface function writes to o.Normal even if the documentation says otherwise.

    In tangent space, a normal of float3(0,0,1) will always match the mesh's vertex normals, and there's no need to set it to anything if you're not modifying those normals in the shader. The s.Normal that the lighting function eventually gets is the world space normal. Between the surface function and the lighting function there's code that either transforms the tangent space direction output by the surface function into world space, or just sets the value to be the interpolated world space vertex normal if not set in the surface function.

    Some examples:
    Code (csharp):
    1. struct Input {
    2.     float2 uv_MainTex;
    3.     float3 worldNormal;
    4. };
    5.  
    6. void surf(Input IN, inout SurfaceOutputStandard o)
    7. {
    8.     fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
    9.     o.Albedo = c.rgb;
    10.     o.Emission = IN.worldNormal;
    11. }
    o.Normal is never assigned, so IN.worldNormal is the world space vertex normal, resulting in the emissive color showing the world space normals.


    Code (csharp):
    1. struct Input {
    2.     float2 uv_MainTex;
    3.     float3 worldNormal;
    4. };
    5.  
    6. void surf(Input IN, inout SurfaceOutputStandard o)
    7. {
    8.     fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
    9.     o.Albedo = c.rgb;
    10.     o.Normal = IN.worldNormal;
    11. }
    o.Normal is assigned, but IN.worldNormal is left as an uninitialized value of float3(0,0,0) resulting in broken lighting.


    Code (csharp):
    1. struct Input {
    2.     float2 uv_MainTex;
    3.     float3 worldNormal;
    4.     INTERNAL_DATA
    5. };
    6.  
    7. void surf(Input IN, inout SurfaceOutputStandard o)
    8. {
    9.     fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
    10.     o.Albedo = c.rgb;
    11.     o.Normal = IN.worldNormal;
    12. }
    o.Normal is assigned, and INTERNAL_DATA is added to the struct, but IN.worldNormal is still left as an uninitialized value of float3(0,0,0) counter to the documentation.


    Code (csharp):
    1. struct Input {
    2.     float2 uv_MainTex;
    3.     float3 worldNormal;
    4.     INTERNAL_DATA
    5. };
    6.  
    7. void surf(Input IN, inout SurfaceOutputStandard o)
    8. {
    9.     fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
    10.     o.Albedo = c.rgb;
    11.     o.Normal = WorldNormalVector(IN, float3(0,0,1));
    12. }
    o.Normal is assigned, and INTERNAL_DATA is added to the struct, and the world normal is accessed using the function. Note, lighting is broken in this example since o.Normal should not be set to the world normal! You're assigning a world normal as the tangent normal, which Unity will try to transform from tangent space into world space for the lighting function, which will result in a s.Normal that's going in seemingly random directions.


    Code (csharp):
    1. struct Input {
    2.     float2 uv_MainTex;
    3.     float3 worldNormal;
    4.     INTERNAL_DATA
    5. };
    6.  
    7. void surf(Input IN, inout SurfaceOutputStandard o)
    8. {
    9.     fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
    10.     o.Albedo = c.rgb;
    11.     o.Normal = float3(0,0,1);
    12.     o.Emission = WorldNormalVector(IN, float3(0,0,1));
    13. }
    o.Normal is assigned, and INTERNAL_DATA is added to the struct, and the world normal is accessed using the function and assigned to the emissive color. Basically identical to the first example, but slower.


    TLDR; If you're not using normal maps, don't worry about o.Normal, and s.Normal and viewDir will always be in world space in the lighting function regardless of what you do.

    Also:
    Actually, in your example, your shader fails to compile with an errors since "p" doesn't exist in the SurfaceOutputStandard struct, so you'd need to use a custom one, and once you fixed that would result in your object being completely black due to an uninitialized normal, as explained above.
     
    Last edited: Jan 4, 2019
    Chestnut0066 and FM-Productions like this.
  3. zhutianlun810

    zhutianlun810

    Joined:
    Sep 17, 2017
    Posts:
    171
    Thank you very much. The "world space" in the lighting function is same as the unity's gameObject's "world space", right?
     
  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    Yes.
     
  5. zhutianlun810

    zhutianlun810

    Joined:
    Sep 17, 2017
    Posts:
    171
    I am also confused on the matrix convention of the surface shader.
    If I initialize a float3x3 test = float3x3(float3(a,b,c), float3(d,e,f), float3(g,h,i)); All float3 vectors will be my row vector, right?

    If I want to convert a matrix Minv back to local frame, Is my following code right?

    Code (CSharp):
    1.                 float3 T1, T2;
    2.                 T1 = normalize(V - N * dot(V, N));
    3.                 T2 = cross(N, T1);
    4.  
    5.                 float3x3 basis = float3x3(T1, T2, N);
    6.                 basis = transpose(basis);
    7.                 // rotate area light in (T1, T2, N) basis
    8.                 Minv = mul(basis, Minv);
    V and N are viewDirection and Normal passed by lighting function.
     
  6. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    I'm terrible at remembering the row / column setup of matrices in shaders, but yes that should create a basis with each float3 as the rows. I'm also not entirely sure what you're trying to accomplish with that code though, or what you mean by local frame in this context, or what Minv is representing.
     
  7. zhutianlun810

    zhutianlun810

    Joined:
    Sep 17, 2017
    Posts:
    171
    I am just wondering how to convert a vector from world space back to tangent space.
     
  8. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    Ah, well, that code certainly isn't doing that. Tangent space is defined by the UVs of the mesh. Effectively it's the "flow" of the UVs. For real time rendering generally the tangent direction is stored as a per vertex value, and the bitangent is a cross product of the vertex normal and vertex tangent vectors.

    Surface shaders make it a real pain to get access to that data even though it's used internally all over the place. In the surface function you can get the tangent to world matrix using the WorldNormalVector() function to extract the data from the hidden matrix.

    Code (csharp):
    1.         struct Input {
    2.             // stuff
    3.             float3 worldNormal;
    4.             INTERNAL_DATA
    5.         };
    6.  
    7.         // in the surf() function
    8.             // construct world to tangent matrix
    9.             half3 worldT =     WorldNormalVector(IN, half3(1,0,0));
    10.             half3 worldB =     WorldNormalVector(IN, half3(0,1,0));
    11.             half3 worldN =     WorldNormalVector(IN, half3(0,0,1));
    12.             half3x3 world2Tangent = half3x3(worldT, worldB, worldN);
    13.  
    14.             tangentVector = mul(world2Tangent, worldVector);
    15.             worldVector = mul(tangentVector, world2Tangent);
    Getting that data in the lighting function ... that's more work. You'd need to use a custom SurfaceOutput struct that you can store the matrix in.
     
  9. zhutianlun810

    zhutianlun810

    Joined:
    Sep 17, 2017
    Posts:
    171
    Thank you very much.