Search Unity

[Solved] Issue with static world space direction to fake lighting in frag shader

Discussion in 'Shaders' started by LiterallyJeff, May 15, 2019.

  1. LiterallyJeff

    LiterallyJeff

    Joined:
    Jan 21, 2015
    Posts:
    2,807
    Hello!

    I'm currently attempting to create a simple 2D fake-toon-lighting shader that uses a static world-space direction as the light source, and which compares that to a normal map for the sprite to determine the shaded regions.

    I've almost got the whole thing working. When I rotate the sprite on the Z axis, everything is looking good. However when I rotate it on the Y or X axes, the lighting fades and then disappears when it reaches 180 degrees.

    Hopefully someone can help me understand what I've written here, and where I've gone wrong.

    The desired end result is either:
    • Lighting on the sprite is only affected by Z axis rotation, other axis rotations show no visible difference in the effect (object space light, ignoring Z rotation)
    • Lighting reacts globally both on the front and back of the sprite in any orientation (world space light)

    Here is what I have currently, which is built on top of the default Sprite shader:
    Code (CSharp):
    1. Shader "Custom/SpriteToonlit"
    2. {
    3.     Properties
    4.     {
    5.         [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
    6.         _Color ("Tint", Color) = (1,1,1,1)
    7.         [MaterialToggle] PixelSnap ("Pixel snap", Float) = 0
    8.         [HideInInspector] _RendererColor ("RendererColor", Color) = (1,1,1,1)
    9.         [HideInInspector] _Flip ("Flip", Vector) = (1,1,1,1)
    10.         [PerRendererData] _AlphaTex ("External Alpha", 2D) = "white" {}
    11.         [PerRendererData] _EnableExternalAlpha ("Enable External Alpha", Float) = 0
    12.  
    13.         [Header(Toon Lighting)]
    14.         _ShadowColor ("Shadow Color", Color) = (0.5,0.5,0.5,1)
    15.         _NormalMap ("Normal Map", 2D) = "bump" {}
    16.         _ShadowAmount("Shadow Amount", Range(0,1)) = 0.5
    17.         _ShadowSoftness("Shadow Softness", Range(0,1)) = 0.5
    18.         _LightDirectionX ("Light Direction X", Range(-1,1)) = 0
    19.         _LightDirectionY ("Light Direction Y", Range(-1,1)) = 0
    20.         _LightDirectionZ ("Light Direction Z", Range(-1,1)) = 1
    21.     }
    22.     SubShader
    23.     {
    24.         Tags
    25.         {
    26.             "Queue"="Transparent"
    27.             "IgnoreProjector"="True"
    28.             "RenderType"="Transparent"
    29.             "PreviewType"="Plane"
    30.             "CanUseSpriteAtlas"="True"
    31.         }
    32.         Pass
    33.         {
    34.             Cull Off
    35.             Lighting Off
    36.             ZWrite Off
    37.             Blend One OneMinusSrcAlpha
    38.  
    39.             CGPROGRAM
    40.             #pragma target 2.0
    41.             #pragma multi_compile_instancing
    42.             #pragma multi_compile_local _ PIXELSNAP_ON
    43.             #pragma multi_compile _ ETC1_EXTERNAL_ALPHA
    44.             #pragma instancing_options nolodfade nolightprobe nolightmap
    45.             #include "UnitySprites.cginc"
    46.  
    47.             #pragma vertex SpriteVert
    48.             #pragma fragment frag
    49.  
    50.             UNITY_INSTANCING_BUFFER_START(Props)
    51.                 UNITY_DEFINE_INSTANCED_PROP(float, _LightDirectionX)
    52.                 UNITY_DEFINE_INSTANCED_PROP(float, _LightDirectionY)
    53.                 UNITY_DEFINE_INSTANCED_PROP(float, _LightDirectionZ)
    54.             UNITY_INSTANCING_BUFFER_END(Props)
    55.  
    56.             sampler2D _NormalMap;
    57.             fixed4 _ShadowColor;
    58.             float _ShadowAmount;
    59.             float _ShadowSoftness;
    60.  
    61.             fixed4 frag(v2f IN) : SV_Target
    62.             {
    63.                 fixed4 color = SampleSpriteTexture (IN.texcoord) * IN.color;
    64.  
    65.                 // sample normal map, remap color values [0,1] to [-1,1] for directional usage
    66.                 float3 normalDirection = normalize((tex2D(_NormalMap, IN.texcoord).xyz - 0.5f) * 2.0f);
    67.  
    68.                 float3 lightDirection = float3(
    69.                     UNITY_ACCESS_INSTANCED_PROP(Props, _LightDirectionX),
    70.                     -UNITY_ACCESS_INSTANCED_PROP(Props, _LightDirectionY),
    71.                     UNITY_ACCESS_INSTANCED_PROP(Props, _LightDirectionZ)
    72.                 );
    73.  
    74.                 lightDirection = normalize(lightDirection);
    75.  
    76.                 // why does this work? Isn't the light in world space to begin with?
    77.                 lightDirection = UnityObjectToWorldNormal(lightDirection);
    78.  
    79.                 // get difference between light and normal directions [-1,1]
    80.                 float NdotL = dot(lightDirection, normalDirection);
    81.  
    82.                 // control shadow gradient using smoothstep
    83.                 float lightIntensity = smoothstep(_ShadowAmount, _ShadowAmount + _ShadowSoftness, NdotL);
    84.  
    85.                 // select shadow or texture color based on light intensity
    86.                 color.rgb = lerp(_ShadowColor, color, lightIntensity);
    87.  
    88.                 // apply texture alpha
    89.                 color.rgb *= color.a;
    90.                 return color;
    91.             }
    92.             ENDCG
    93.         }
    94.     }
    95. }
    96.  
     
    Last edited: May 15, 2019
    sama-van likes this.
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,343
    Normal maps are generally "tangent space normal maps". That is to say they're in a space that's defined by the UV's and the surface normal. When using a 2D camera with the default sprite orientation, that happens to line up pretty well with the world space, x is right, y is up, though z is inverted for the normal map vs. world space. If you rotate the sprite they no longer line up.

    For that you need a shader that calculates the tangent to world matrix. This is usually done in the vertex shader using the vertex normal and tangent and passes the matrix to the fragment shader, which the default SpriteVert vertex function absolutely does not do. It doesn't even look at the vertex normal at all.

    The alternative, if you're going to stick with a purely 2D camera, one that never rotates io any axis, would be calculate the matrix using UV derivatives, or a simplified version of it. To do it for real would require the world position in the fragment shader, which the SpriteVert also doesn't provide. Technically this will also "break" when you rotate the x and y of the sprite, but you already want non-standard normal map behavior there, which the below should replicate your "only z does anything" stipulation.

    Code (csharp):
    1. float2 dx = ddx(IN.texcoord);
    2. float2 dy = ddy(IN.texcoord);
    3. float2 xDir = normalize(dx.x, dy.x);
    4. float2 yDir = normalize(dx.y, dy.y);
    5.  
    6. float3 normalDirection = UnpackNormal(tex2D(_NormalMap, IN.texcoord));
    7.  
    8. normalDirection.xy = xDir * normalDirection.x + yDir * normalDirection.y;
    9. normalDirection.z = -normalDirection.z;
    edit: fix xDir and yDir being float instead of float2
     
    Last edited: May 16, 2019
    LiterallyJeff likes this.
  3. LiterallyJeff

    LiterallyJeff

    Joined:
    Jan 21, 2015
    Posts:
    2,807
    Thanks very much for your reply. I realize now that I've been thinking about normals the wrong way, and they are indeed in a different space, which explains why none of the vector comparisons I was doing gave me coherent results.

    I found in the Unity docs an example that converts tangent space normals into world space, so I adapted that into my solution, so my sprites now work in any orientation, which is I think the ideal solution for me at the moment.

    This is the current state of my shader, and it appears to be working very well. If you have any other criticisms or advice I'd be happy to hear it.

    Thanks again, you're a legend.

    Code (CSharp):
    1. Shader "Custom/SpriteToonlit"
    2. {
    3.     Properties
    4.     {
    5.         [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
    6.         _Color ("Tint", Color) = (1,1,1,1)
    7.         [MaterialToggle] PixelSnap ("Pixel snap", Float) = 0
    8.         [HideInInspector] _RendererColor ("RendererColor", Color) = (1,1,1,1)
    9.         [HideInInspector] _Flip ("Flip", Vector) = (1,1,1,1)
    10.         [PerRendererData] _AlphaTex ("External Alpha", 2D) = "white" {}
    11.         [PerRendererData] _EnableExternalAlpha ("Enable External Alpha", Float) = 0
    12.  
    13.         [Header(Toon Lighting)]
    14.         _ShadowColor ("Shadow Color", Color) = (0.5,0.5,0.5,1)
    15.         _NormalMap ("Normal Map", 2D) = "bump" {}
    16.         _ShadowAmount("Shadow Amount", Range(0,1)) = 0.5
    17.         _ShadowSoftness("Shadow Softness", Range(0,1)) = 0.5
    18.         _LightDirectionX ("Light Direction X", Range(-1,1)) = 0
    19.         _LightDirectionY ("Light Direction Y", Range(-1,1)) = 0
    20.         _LightDirectionZ ("Light Direction Z", Range(-1,1)) = 1
    21.     }
    22.     SubShader
    23.     {
    24.         Tags
    25.         {
    26.             "Queue"="Transparent"
    27.             "IgnoreProjector"="True"
    28.             "RenderType"="Transparent"
    29.             "PreviewType"="Plane"
    30.             "CanUseSpriteAtlas"="True"
    31.         }
    32.         Pass
    33.         {
    34.             Cull Off
    35.             Lighting Off
    36.             ZWrite Off
    37.             Blend One OneMinusSrcAlpha
    38.  
    39.             CGPROGRAM
    40.             #pragma target 2.0
    41.             #pragma multi_compile_local _ PIXELSNAP_ON
    42.             #pragma multi_compile _ ETC1_EXTERNAL_ALPHA
    43.             #include "UnitySprites.cginc"
    44.  
    45.             #pragma vertex vert
    46.             #pragma fragment frag
    47.  
    48.             sampler2D _NormalMap;
    49.             fixed4 _ShadowColor;
    50.             float _ShadowAmount;
    51.             float _ShadowSoftness;
    52.             float _LightDirectionX;
    53.             float _LightDirectionY;
    54.             float _LightDirectionZ;
    55.  
    56.             struct VertData
    57.             {
    58.                 float4 vertex : POSITION;
    59.                 float4 color    : COLOR;
    60.                 float3 normal : NORMAL;
    61.                 float4 tangent : TANGENT;
    62.                 float2 uv : TEXCOORD0;
    63.                 UNITY_VERTEX_INPUT_INSTANCE_ID
    64.             };
    65.  
    66.             struct FragData
    67.             {
    68.                 float4 pos   : SV_POSITION;
    69.                 fixed4 color    : COLOR;
    70.                 float3 worldPos : TEXCOORD0;
    71.                 // these three vectors will hold a 3x3 rotation matrix
    72.                 // that transforms from tangent to world space
    73.                 half3 tspace0 : TEXCOORD1; // tangent.x, bitangent.x, normal.x
    74.                 half3 tspace1 : TEXCOORD2; // tangent.y, bitangent.y, normal.y
    75.                 half3 tspace2 : TEXCOORD3; // tangent.z, bitangent.z, normal.z
    76.                 // texture coordinate for the normal map
    77.                 float2 uv : TEXCOORD4;
    78.                 UNITY_VERTEX_OUTPUT_STEREO
    79.             };
    80.  
    81.             FragData vert(VertData IN)
    82.             {
    83.                 FragData OUT;
    84.  
    85.                 UNITY_SETUP_INSTANCE_ID (IN);
    86.                 UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
    87.  
    88.                 float4 flippedVert = UnityFlipSprite(IN.vertex, _Flip);
    89.                 OUT.pos = UnityObjectToClipPos(flippedVert);
    90.                 OUT.worldPos = mul(unity_ObjectToWorld, flippedVert).xyz;
    91.  
    92.                 #ifdef PIXELSNAP_ON
    93.                 OUT.pos = UnityPixelSnap (OUT.pos);
    94.                 #endif
    95.  
    96.                 half3 wNormal = UnityObjectToWorldNormal(IN.normal);
    97.                 half3 wTangent = UnityObjectToWorldDir(IN.tangent.xyz);
    98.  
    99.                 // compute bitangent from cross product of normal and tangent
    100.                 half tangentSign = IN.tangent.w * unity_WorldTransformParams.w;
    101.                 half3 wBitangent = cross(wNormal, wTangent) * tangentSign;
    102.  
    103.                 // output the tangent space matrix
    104.                 OUT.tspace0 = half3(wTangent.x, wBitangent.x, wNormal.x);
    105.                 OUT.tspace1 = half3(wTangent.y, wBitangent.y, wNormal.y);
    106.                 OUT.tspace2 = half3(wTangent.z, wBitangent.z, wNormal.z);
    107.  
    108.                 OUT.uv = IN.uv;
    109.                 OUT.color = IN.color * _Color * _RendererColor;
    110.  
    111.                 return OUT;
    112.             }
    113.  
    114.             fixed4 frag(FragData IN) : SV_Target
    115.             {
    116.                 fixed4 colorOutput = SampleSpriteTexture (IN.uv) * IN.color;
    117.  
    118.                 // sample the normal map, and decode from the Unity encoding
    119.                 half3 tnormal = UnpackNormal(tex2D(_NormalMap, IN.uv));
    120.  
    121.                 // transform normal from tangent to world space
    122.                 half3 worldNormal;
    123.                 worldNormal.x = dot(IN.tspace0, tnormal);
    124.                 worldNormal.y = dot(IN.tspace1, tnormal);
    125.                 worldNormal.z = dot(IN.tspace2, tnormal);
    126.  
    127.                 float3 worldLightDirection = float3(_LightDirectionX, -_LightDirectionY, _LightDirectionZ);
    128.  
    129.                 worldLightDirection = normalize(worldLightDirection);
    130.  
    131.                 // get difference between light and normal directions [-1,1]
    132.                 float NdotL = dot(worldLightDirection, worldNormal);
    133.  
    134.                 // control shadow gradient using smoothstep
    135.                 float lightIntensity = smoothstep(0, 0.01, NdotL);
    136.  
    137.                 // select shadow or texture color based on light intensity
    138.                 colorOutput.rgb = lerp(_ShadowColor, colorOutput, lightIntensity);
    139.  
    140.                 // apply texture alpha
    141.                 colorOutput.rgb *= colorOutput.a;
    142.                 return colorOutput;
    143.             }
    144.             ENDCG
    145.         }
    146.     }
    147. }
    148.  
     
  4. LiterallyJeff

    LiterallyJeff

    Joined:
    Jan 21, 2015
    Posts:
    2,807
    Ah I spoke too soon.

    It appears the world space normals fixed many of my issues, but not all of them.

    It looks great, but I'm still seeing the shading get inverted when the sprite is back-facing. If that can be fixed so that both sides are identical, that would be ideal. This effect will be applied to a tileset that can be rotated randomly in increments of 90 degrees on Z and 180 degrees on X and Y.

    I feel like it makes sense that the shading is inverted on the back side, because the normals are facing the opposite direction relative to the light source. I just don't know how to make it so that both sides can use the same normal values, or reach the same result. I attempted some abs() usage on the NdotL but perhaps not correctly because it didn't help.

    However I've continued experimenting, and I took your code snippet and implemented it instead of the world space conversion. It does indeed only get affected by the Z axis rotation, but the lighting doesn't seem to be working on the Y axis anymore. Changing the Y direction of the light moves the lighting along the X axis.

    Additionally, it appears that the light is coming from the opposite direction when the sprite is rotated 90 degrees. That seems to be an issue with both implementations, so it may be related to something else.

    Code (CSharp):
    1. Shader "Custom/SpriteToonlit"
    2. {
    3.     Properties
    4.     {
    5.         [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
    6.         _Color ("Tint", Color) = (1,1,1,1)
    7.         [MaterialToggle] PixelSnap ("Pixel snap", Float) = 0
    8.         [HideInInspector] _RendererColor ("RendererColor", Color) = (1,1,1,1)
    9.         [HideInInspector] _Flip ("Flip", Vector) = (1,1,1,1)
    10.         [PerRendererData] _AlphaTex ("External Alpha", 2D) = "white" {}
    11.         [PerRendererData] _EnableExternalAlpha ("Enable External Alpha", Float) = 0
    12.  
    13.         [Header(Toon Lighting)]
    14.         _ShadowColor ("Shadow Color", Color) = (0.5,0.5,0.5,1)
    15.         _NormalMap ("Normal Map", 2D) = "bump" {}
    16.         _LightDirectionX ("Light Direction X", Range(-1,1)) = 0
    17.         _LightDirectionY ("Light Direction Y", Range(-1,1)) = 0
    18.         _LightDirectionZ ("Light Direction Z", Range(-1,1)) = 1
    19.     }
    20.     SubShader
    21.     {
    22.         Tags
    23.         {
    24.             "Queue"="Transparent"
    25.             "IgnoreProjector"="True"
    26.             "RenderType"="Transparent"
    27.             "PreviewType"="Plane"
    28.             "CanUseSpriteAtlas"="True"
    29.         }
    30.         Pass
    31.         {
    32.             Cull Off
    33.             Lighting Off
    34.             ZWrite Off
    35.             Blend One OneMinusSrcAlpha
    36.  
    37.             CGPROGRAM
    38.             #pragma target 2.0
    39.             #pragma multi_compile_local _ PIXELSNAP_ON
    40.             #pragma multi_compile _ ETC1_EXTERNAL_ALPHA
    41.             #include "UnitySprites.cginc"
    42.  
    43.             #pragma vertex vert
    44.             #pragma fragment frag
    45.  
    46.             sampler2D _NormalMap;
    47.             fixed4 _ShadowColor;
    48.             float _LightDirectionX;
    49.             float _LightDirectionY;
    50.             float _LightDirectionZ;
    51.  
    52.             struct VertData
    53.             {
    54.                 float4 vertex : POSITION;
    55.                 float4 color    : COLOR;
    56.                 float3 normal : NORMAL;
    57.                 float4 tangent : TANGENT;
    58.                 float2 uv : TEXCOORD0;
    59.                 UNITY_VERTEX_INPUT_INSTANCE_ID
    60.             };
    61.  
    62.             struct FragData
    63.             {
    64.                 float4 pos   : SV_POSITION;
    65.                 fixed4 color    : COLOR;
    66.                 float3 worldPos : TEXCOORD0;
    67.                 // these three vectors will hold a 3x3 rotation matrix
    68.                 // that transforms from tangent to world space
    69.                 half3 tspace0 : TEXCOORD1; // tangent.x, bitangent.x, normal.x
    70.                 half3 tspace1 : TEXCOORD2; // tangent.y, bitangent.y, normal.y
    71.                 half3 tspace2 : TEXCOORD3; // tangent.z, bitangent.z, normal.z
    72.                 // texture coordinate for the normal map
    73.                 float2 uv : TEXCOORD4;
    74.                 UNITY_VERTEX_OUTPUT_STEREO
    75.             };
    76.  
    77.             FragData vert(VertData IN)
    78.             {
    79.                 FragData OUT;
    80.  
    81.                 UNITY_SETUP_INSTANCE_ID (IN);
    82.                 UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
    83.  
    84.                 float4 flippedVert = UnityFlipSprite(IN.vertex, _Flip);
    85.                 OUT.pos = UnityObjectToClipPos(flippedVert);
    86.                 OUT.worldPos = mul(unity_ObjectToWorld, flippedVert).xyz;
    87.  
    88.                 #ifdef PIXELSNAP_ON
    89.                 OUT.pos = UnityPixelSnap (OUT.pos);
    90.                 #endif
    91.  
    92.                 half3 wNormal = UnityObjectToWorldNormal(IN.normal);
    93.                 half3 wTangent = UnityObjectToWorldDir(IN.tangent.xyz);
    94.  
    95.                 // compute bitangent from cross product of normal and tangent
    96.                 half tangentSign = IN.tangent.w * unity_WorldTransformParams.w;
    97.                 half3 wBitangent = cross(wNormal, wTangent) * tangentSign;
    98.  
    99.                 // output the tangent space matrix
    100.                 OUT.tspace0 = half3(wTangent.x, wBitangent.x, wNormal.x);
    101.                 OUT.tspace1 = half3(wTangent.y, wBitangent.y, wNormal.y);
    102.                 OUT.tspace2 = half3(wTangent.z, wBitangent.z, wNormal.z);
    103.  
    104.                 OUT.uv = IN.uv;
    105.                 OUT.color = IN.color * _Color * _RendererColor;
    106.  
    107.                 return OUT;
    108.             }
    109.  
    110.             fixed4 frag(FragData IN) : SV_Target
    111.             {
    112.                 fixed4 colorOutput = SampleSpriteTexture (IN.uv) * IN.color;
    113.  
    114.                 float2 dx = ddx(IN.uv);
    115.                 float2 dy = ddy(IN.uv);
    116.                 float xDir = normalize(float2(dx.x, dy.x));
    117.                 float yDir = normalize(float2(dx.y, dy.y));
    118.      
    119.                 float3 normalDirection = UnpackNormal(tex2D(_NormalMap, IN.uv));
    120.      
    121.                 normalDirection.xy = xDir * normalDirection.x + yDir * normalDirection.y;
    122.                 normalDirection.z = -normalDirection.z;
    123.  
    124.                 // // sample the normal map, and decode from the Unity encoding
    125.                 // half3 tnormal = UnpackNormal(tex2D(_NormalMap, IN.uv));
    126.  
    127.                 // // transform normal from tangent to world space
    128.                 // half3 worldNormal;
    129.                 // worldNormal.x = dot(IN.tspace0, tnormal);
    130.                 // worldNormal.y = dot(IN.tspace1, tnormal);
    131.                 // worldNormal.z = dot(IN.tspace2, tnormal);
    132.  
    133.                 float3 worldLightDirection = float3(_LightDirectionX, -_LightDirectionY, _LightDirectionZ);
    134.  
    135.                 worldLightDirection = normalize(worldLightDirection);
    136.  
    137.                 // get difference between light and normal directions [-1,1]
    138.                 float NdotL = dot(worldLightDirection, normalDirection);
    139.  
    140.                 // control shadow gradient using smoothstep
    141.                 float lightIntensity = smoothstep(0, 0.01, NdotL);
    142.  
    143.                 // select shadow or texture color based on light intensity
    144.                 colorOutput.rgb = lerp(_ShadowColor, colorOutput, lightIntensity);
    145.  
    146.                 // apply texture alpha
    147.                 colorOutput.rgb *= colorOutput.a;
    148.                 return colorOutput;
    149.             }
    150.             ENDCG
    151.             }
    152.     }
    153. }
    154.  
     
    Last edited: May 16, 2019
  5. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,343
    Code (csharp):
    1. fixed4 frag(FragData IN, float facing: VFACE) : SV_Target
    2. // ...
    3. tnormal.z *= facing;
     
    LiterallyJeff likes this.
  6. LiterallyJeff

    LiterallyJeff

    Joined:
    Jan 21, 2015
    Posts:
    2,807
    Well I'll be damned. That worked very well and is very elegant! Super convenient parameter. I'm checking out the docs now.

    So now the front and back are behaving the same way, which is fantastic.

    I want to thank you again for taking the time to help me out, I'm keen to understand shaders on a deeper level and I'm learning a lot!

    The last remaining issue I'm having is that when the sprite is rotated +/- 90 degrees (on the z axis), the shadow is on the wrong side. A full 180 is back to normal. I'm not sure if this is due to the vector math that I'm not familiar with, and perhaps im overlooking a simple error.

    Here's my frag function currently:
    Code (CSharp):
    1. fixed4 frag(FragData IN, float facing: VFACE) : SV_Target
    2. {
    3.     fixed4 colorOutput = SampleSpriteTexture (IN.uv) * IN.color;
    4.  
    5.     // sample the normal map, and decode from the Unity encoding
    6.     half3 tnormal = UnpackNormal(tex2D(_NormalMap, IN.uv));
    7.     // mirror normals for back & front face
    8.     tnormal.z *= facing;
    9.  
    10.     // transform normal from tangent to world space
    11.     half3 normalDirection;
    12.     normalDirection.x = dot(IN.tspace0, tnormal);
    13.     normalDirection.y = dot(IN.tspace1, tnormal);
    14.     normalDirection.z = dot(IN.tspace2, tnormal);
    15.  
    16.     float3 worldLightDirection = float3(_LightDirectionX, -_LightDirectionY, _LightDirectionZ);
    17.     worldLightDirection = normalize(worldLightDirection);
    18.  
    19.     // get difference between light and normal direction [-1,1]
    20.     float NdotL = dot(worldLightDirection, normalDirection);
    21.  
    22.     // control shadow gradient using smoothstep
    23.     float lightIntensity = smoothstep(0, saturate(_ShadowSoftness + 0.01), NdotL);
    24.  
    25.     // select shadow or texture color based on light intensity
    26.     colorOutput.rgb = lerp(_ShadowColor, colorOutput, lightIntensity);
    27.  
    28.     // apply texture alpha
    29.     colorOutput.rgb *= colorOutput.a;
    30.     return colorOutput;
    31. }
     
    Last edited: May 16, 2019
  7. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,343
    Woops! Those should both be float2 values.
     
    LiterallyJeff likes this.
  8. LiterallyJeff

    LiterallyJeff

    Joined:
    Jan 21, 2015
    Posts:
    2,807
    I spent a lot of time today going through trial and error, and I can't seem to figure out what's going on in my shader.

    I'm seeing the lighting rotating with my object at seemingly twice the rate of my object.

    Is there something about my world space light direction that isn't truly world space? The _LightDirectionX and Y are straight from the parameters
    Code (CSharp):
    1. _LightDirectionX ("Light Direction X", Range(-1,1)) = -1
    2. _LightDirectionY ("Light Direction Y", Range(-1,1)) = -1
    Code (CSharp):
    1. fixed4 frag(FragData IN, float facing: VFACE) : SV_Target
    2. {
    3.     fixed4 color = SampleSpriteTexture (IN.uv) * IN.color;
    4.  
    5.     // sample the normal map, and decode from the Unity encoding
    6.     half3 tnormal = UnpackNormal(tex2D(_NormalMap, IN.uv));
    7.     tnormal.z *= -facing;
    8.  
    9.     // transform normal from tangent to world space
    10.     half3 normalDirection;
    11.     normalDirection.x = dot(IN.tspace0, tnormal);
    12.     normalDirection.y = dot(IN.tspace1, tnormal);
    13.     normalDirection.z = dot(IN.tspace2, tnormal);
    14.  
    15.     float3 worldLightDirection = float3(_LightDirectionX, -_LightDirectionY, 0);
    16.     worldLightDirection = normalize(worldLightDirection);
    17.  
    18.     // get difference between light and normal direction [-1,1]
    19.     float NdotL = dot(normalDirection, worldLightDirection);
    20.  
    21.     // if the lighting angle is less than the shadow offset, and the offset is not 0
    22.     float shadeFactor = 1 - step(_ShadowOffset-1, NdotL);
    23.  
    24.     // if the lighting angle is greater than the highlight offset, and the offset is not 0
    25.     // highlight offset is inverted so both shadow and light diminish towards 0
    26.     float lightFactor = step(1-_HighlightOffset, NdotL);
    27.  
    28.     color.rgb = lerp(color, _HighlightColor, lightFactor);
    29.     color.rgb = lerp(color, _ShadowColor, shadeFactor);
    30.  
    31.     // apply texture alpha
    32.     color.rgb *= color.a;
    33.  
    34.     return color;
    35. }
     
    Last edited: May 17, 2019
  9. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,343
    Are your normal maps +Y or -Y up? (if you look at the green channel alone, does it look like the light is coming from above, or below?) Unity uses +Y (light coming from above). If you have your normal maps using -Y it can sometimes look like what you're seeing.
     
  10. LiterallyJeff

    LiterallyJeff

    Joined:
    Jan 21, 2015
    Posts:
    2,807
    Searching online led me to that possibility, so I imported an inverted normal map. Unfortunately I'm seeing the same results, just mirrored on the Y.

    I'm signing off for the night, so thanks again for your help. If I can't figure out the reason for the rotation, I can always code a workaround that reverses the light direction if the sprite is rotated 90 degrees temporarily until I figure it out. :D
     
    Last edited: Jun 4, 2019
  11. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,343
    Unity is also +X, and unless that shape is supposed to be an inset, it's -X.
     
    LiterallyJeff likes this.
  12. LiterallyJeff

    LiterallyJeff

    Joined:
    Jan 21, 2015
    Posts:
    2,807
    I dont fully understand why inverting the red axis fixed this, as in my mind the lighting should look wrong at all angles if that is the case.

    Regardless, that fixed it. Thanks once again for your insight and suggestions. I will be sure to avoid these types of issues in the future now that I know how it affects the results!

    Also, photoshop pales in comparison to this online tool for normal mapping: https://cpetry.github.io/NormalMap-Online/

    Thanks for being rad.
     
  13. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,343
    Yeah, that normal map tool defaults to the x (red channel) being backwards compared to the standard used by every single graphics API and 3D tool I know of.
     
  14. LiterallyJeff

    LiterallyJeff

    Joined:
    Jan 21, 2015
    Posts:
    2,807
    Oddly enough, Photoshop also has the red channel backwards by default with no interface option to invert it. They have "invert height" but that's all. That tool at allows me to invert each channel individually which is nice.

    Good to know!
     
  15. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,343
    I don't know if I've ever used the built in photoshop tool. I've used the Nvidia plugins in the past or for the last decade or so xNormal.