Search Unity

Flat lighting without separate smoothing groups?

Discussion in 'Shaders' started by Iron-Warrior, Nov 14, 2014.

  1. Iron-Warrior

    Iron-Warrior

    Joined:
    Nov 3, 2009
    Posts:
    671
    Hi there,

    I'm pretty new to ShaderLab so I might be missing something obvious, but I'm having trouble writing a shader that simulates flat lightning, like so:


    Now, the obvious way to do this is to just break the smoothing groups apart when importing the model. The problem with doing this is that most of my animated meshes have a sort of shielding effect layered on top of them (similar to Halo). The shield uses a transparent shader that pushes the mesh's vertices out by a specified amount, like seen in the demo shader on this page. If you set smoothing groups the flat, it breaks apart the mesh's triangles, and each vertex on each triangle has the same normal, so when they are pushed out the triangles don't stay together. My solution would be to write a shader that does flat lightning and keep the mesh fully smoothed, but it seems tough. When fully smoothed, each vertex's normal seems to be the average of the triangles around it (?). Is it possible to access the triangle normal either directly or through deriving it?

    Thanks,
    Erik
     
    Bioinformatizer likes this.
  2. jistyles

    jistyles

    Joined:
    Nov 6, 2013
    Posts:
    34
    You could derive it with the cross product of the partial derivatives of the position. Eg I think something like:
    float3 posddx = ddx(viewSpacePos.xyz);
    float3 posddy = ddy(viewSpacePos.xyz);
    float3 derivedNormal = cross( normalize(posddx), normalize(posddy) );

    I've used this in a few view space situations, but should work the same for world space normals too (based off world space pos).
    Note: due to quad messaging on different hardware, the partial derivative instructions are implemented a bit oddly on some setups, but in general should be stable... just test to be certain! If you're using dx11, test with the _fine and _coarse versions too!
     
  3. jistyles

    jistyles

    Joined:
    Nov 6, 2013
    Posts:
    34
    PS, alternatively you could bake the smooth normal data into vertex colours too, so you have two sets of vertex inputs representing normals.
     
  4. Iron-Warrior

    Iron-Warrior

    Joined:
    Nov 3, 2009
    Posts:
    671
    Okay, thanks for the help! Took me a bit of work but I got off to somewhat of a start so far.



    And heres the shader.

    Code (csharp):
    1.  
    2. Shader "Example" {
    3.     Properties {
    4.         _MainTex ("Base (RGB)", 2D) = "white" {}
    5.     }
    6.     SubShader {
    7.         Tags { "RenderType"="Opaque" }
    8.         LOD 200
    9.        
    10.         Pass {
    11.             CGPROGRAM
    12.                 #pragma target 3.0
    13.                 #pragma glsl
    14.                 #pragma vertex vert
    15.                 #pragma fragment frag
    16.                 #include "UnityCG.cginc"
    17.            
    18.                 struct v2f {
    19.                     float4 pos : SV_POSITION;
    20.                     float2 uv_MainTex : TEXCOORD0;
    21.                     float4 worldPos : TEXCOORD1;
    22.                 };
    23.            
    24.                 float4 _MainTex_ST;
    25.            
    26.                 v2f vert(appdata_base v) {
    27.                     v2f o;
    28.                     o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
    29.                     o.uv_MainTex = TRANSFORM_TEX(v.texcoord, _MainTex);
    30.                    
    31.                     // worldPosition(?)
    32.                     o.worldPos = v.vertex;
    33.  
    34.                     return o;
    35.                 }
    36.            
    37.                 sampler2D _MainTex;
    38.            
    39.                 float4 frag(v2f IN) : COLOR {
    40.                     float3 posddx = ddx(IN.worldPos.xyz);
    41.                     float3 posddy = ddy(IN.worldPos.xyz);
    42.                     float3 derivedNormal = cross( normalize(posddx), normalize(posddy));
    43.                    
    44.                     // Add in lighting, somehow
    45.                    
    46.                     half4 c = (0.0, 0.0, 0.0, 0.3);
    47.                     c.rgb += derivedNormal;
    48.                     return c;
    49.                 }
    50.             ENDCG
    51.         }
    52.     }
    53. }
    So I'm not quite sure how to retrieve the view position, or if I'm doing it right. v.vertex passes in the vertex position in world space (?) so I should just directly be able to pass it through to the fragment shader. Provided this is done correctly, my next step is to somehow access lighting data and then use it to define the colour of each face. Unity suggests that shaders that interact with lighting be written as Surface Shaders, but since I'm not really interacting in a standardized way I don't know if that's the way to go. Can fragment shaders access lighting data, like color and intensity?

    Thanks again for the help, made this much easier!
     
  5. Iron-Warrior

    Iron-Warrior

    Joined:
    Nov 3, 2009
    Posts:
    671
    Progress report!

    Shiny. And the code:
    Code (csharp):
    1.  
    2. Shader "Example" {
    3.     Properties {
    4.         _MainTex ("Base (RGB)", 2D) = "white" {}
    5.     }
    6.     SubShader {
    7.         Tags { "LightMode" = "ForwardBase" }
    8.         LOD 200
    9.        
    10.         Pass {
    11.             CGPROGRAM
    12.                 #pragma target 3.0
    13.                 #pragma glsl
    14.                 #pragma vertex vert
    15.                 #pragma fragment frag
    16.                 #include "UnityCG.cginc"
    17.                
    18.                 uniform float4 _LightColor0;
    19.            
    20.                 struct v2f {
    21.                     float4 pos : SV_POSITION;
    22.                     float2 uv_MainTex : TEXCOORD0;
    23.                     float4 worldPos : TEXCOORD1;
    24.                     float3 vertexLighting : TEXCOORD2;
    25.                 };
    26.            
    27.                 float4 _MainTex_ST;
    28.            
    29.                 v2f vert(appdata_base v) {
    30.                     v2f o;
    31.                     o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
    32.                     o.uv_MainTex = TRANSFORM_TEX(v.texcoord, _MainTex);
    33.                    
    34.                     // worldPosition(?)
    35.                     o.worldPos = v.vertex;
    36.                    
    37.                    
    38.                     o.vertexLighting = float3(0.0, 0.0, 0.0);
    39.                    
    40.                     float3 normalDir = normalize(mul(float4(v.normal, 0.0), _World2Object).xyz);
    41.                    
    42.                     for (int index = 0; index < 4; index++)
    43.                     {    
    44.                         float4 lightPosition = float4(unity_4LightPosX0[index], unity_4LightPosY0[index], unity_4LightPosZ0[index], 1.0);
    45.                        
    46.                         float3 vertexToLightSource = lightPosition.xyz - mul(_Object2World, v.vertex);
    47.                         float3 lightDirection = normalize(vertexToLightSource);
    48.                         float squaredDistance = dot(vertexToLightSource, vertexToLightSource);
    49.                         float3 diffuseReflection = unity_LightColor[index].rgb * squaredDistance * max(0.0, dot(normalDir, lightDirection));        
    50.  
    51.                         o.vertexLighting = o.vertexLighting + diffuseReflection;
    52.                     }
    53.                    
    54.                     return o;
    55.                 }
    56.            
    57.                 sampler2D _MainTex;
    58.            
    59.                 float4 frag(v2f IN) : COLOR {
    60.                     float3 lightDirection = normalize(_WorldSpaceLightPos0.xyz);
    61.                
    62.                     float3 posddx = ddx(IN.worldPos.xyz);
    63.                     float3 posddy = ddy(IN.worldPos.xyz);
    64.                     float3 derivedNormal = cross( normalize(posddx), normalize(posddy));
    65.                    
    66.                     // Add in lighting, somehow
    67.                    
    68.                     half4 c = (0.0, 0.0, 0.0, 0.0);
    69.                     c.rgb += derivedNormal + IN.vertexLighting * _LightColor0.rgb * max(0.0, dot(derivedNormal, lightDirection));;
    70.                     return c;
    71.                 }
    72.             ENDCG
    73.         }
    74.     }
    75. }
    I'm mostly butchering the code on this page to try and figure out how to write shaders. As it is right now the lighting is a complete and total mess.
     
  6. Marco-Sperling

    Marco-Sperling

    Joined:
    Mar 5, 2012
    Posts:
    568
    I found your task interesting so I solved it using the "normals to vertex color" approach and Shader Forge - which works quite well I think (sorry for the crazy gif):
    SF_NormalsToVertexColor.jpg
    SF_NormalsToVertexColor_Graph.jpg
    SF_NormalsFromVertexColorForSmoothScale.gif

    Maybe this is of help to you.
     
    Bioinformatizer likes this.
  7. Iron-Warrior

    Iron-Warrior

    Joined:
    Nov 3, 2009
    Posts:
    671
    Got it to work, mostly! Ended up just pulling that entire shader I posted and changing the normal to be the derived one. Code and gif.

    Code (csharp):
    1.  
    2. Shader "Custom/Default" {
    3.    Properties {
    4.       _Color ("Diffuse Material Color", Color) = (1,1,1,1)
    5.       _SpecColor ("Specular Material Color", Color) = (1,1,1,1)
    6.       _Shininess ("Shininess", Float) = 10
    7.    }
    8.    SubShader {
    9.       Pass {      
    10.          Tags { "LightMode" = "ForwardBase" } // pass for
    11.             // 4 vertex lights, ambient light & first pixel light
    12.  
    13.          CGPROGRAM
    14.         #pragma target 3.0
    15.          #pragma multi_compile_fwdbase
    16.          #pragma vertex vert
    17.          #pragma fragment frag
    18.  
    19.          #include "UnityCG.cginc"
    20.          uniform float4 _LightColor0;
    21.             // color of light source (from "Lighting.cginc")
    22.  
    23.          // User-specified properties
    24.          uniform float4 _Color;
    25.          uniform float4 _SpecColor;
    26.          uniform float _Shininess;
    27.  
    28.          struct vertexInput {
    29.             float4 vertex : POSITION;
    30.             float3 normal : NORMAL;
    31.          };
    32.          struct vertexOutput {
    33.             float4 pos : SV_POSITION;
    34.             float4 posWorld : TEXCOORD0;
    35.             float3 normalDir : TEXCOORD1;
    36.             float3 vertexLighting : TEXCOORD2;
    37.          };
    38.  
    39.          vertexOutput vert(vertexInput input)
    40.          {          
    41.             vertexOutput output;
    42.  
    43.             float4x4 modelMatrix = _Object2World;
    44.             float4x4 modelMatrixInverse = _World2Object;
    45.                // unity_Scale.w is unnecessary here
    46.  
    47.             output.posWorld = mul(modelMatrix, input.vertex);
    48.             output.normalDir = normalize(
    49.                mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
    50.             output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
    51.  
    52.             // Diffuse reflection by four "vertex lights"            
    53.             output.vertexLighting = float3(0.0, 0.0, 0.0);
    54.             #ifdef VERTEXLIGHT_ON
    55.             for (int index = 0; index < 4; index++)
    56.             {    
    57.                float4 lightPosition = float4(unity_4LightPosX0[index],
    58.                   unity_4LightPosY0[index],
    59.                   unity_4LightPosZ0[index], 1.0);
    60.  
    61.                float3 vertexToLightSource =
    62.                   lightPosition.xyz - output.posWorld.xyz;        
    63.                float3 lightDirection = normalize(vertexToLightSource);
    64.                float squaredDistance =
    65.                   dot(vertexToLightSource, vertexToLightSource);
    66.                float attenuation = 1.0 / (1.0 +
    67.                   unity_4LightAtten0[index] * squaredDistance);
    68.                float3 diffuseReflection = attenuation
    69.                   * unity_LightColor[index].rgb * _Color.rgb
    70.                   * max(0.0, dot(output.normalDir, lightDirection));        
    71.  
    72.                output.vertexLighting =
    73.                   output.vertexLighting + diffuseReflection;
    74.             }
    75.             #endif
    76.             return output;
    77.          }
    78.  
    79.          float4 frag(vertexOutput input) : COLOR
    80.          {
    81.             float3 posddx = ddx(input.posWorld.xyz);
    82.             float3 posddy = ddy(input.posWorld.xyz);
    83.             float3 derivedNormal = cross( normalize(posddx), normalize(posddy));
    84.          
    85.             // float3 normalDirection = normalize(input.normalDir);
    86.             float3 normalDirection = normalize(derivedNormal);
    87.             float3 viewDirection = normalize(
    88.                _WorldSpaceCameraPos - input.posWorld.xyz);
    89.             float3 lightDirection;
    90.             float attenuation;
    91.  
    92.             if (0.0 == _WorldSpaceLightPos0.w) // directional light?
    93.             {
    94.                attenuation = 1.0; // no attenuation
    95.                lightDirection =
    96.                   normalize(_WorldSpaceLightPos0.xyz);
    97.             }
    98.             else // point or spot light
    99.             {
    100.                float3 vertexToLightSource =
    101.                   _WorldSpaceLightPos0.xyz - input.posWorld.xyz;
    102.                float distance = length(vertexToLightSource);
    103.                attenuation = 1.0 / distance; // linear attenuation
    104.                lightDirection = normalize(vertexToLightSource);
    105.             }
    106.  
    107.             float3 ambientLighting =
    108.                 UNITY_LIGHTMODEL_AMBIENT.rgb * _Color.rgb;
    109.  
    110.             float3 diffuseReflection =
    111.                attenuation * _LightColor0.rgb * _Color.rgb
    112.                * max(0.0, dot(normalDirection, lightDirection));
    113.  
    114.             float3 specularReflection;
    115.             if (dot(normalDirection, lightDirection) < 0.0)
    116.                // light source on the wrong side?
    117.             {
    118.                specularReflection = float3(0.0, 0.0, 0.0);
    119.                   // no specular reflection
    120.             }
    121.             else // light source on the right side
    122.             {
    123.                specularReflection = attenuation * _LightColor0.rgb
    124.                   * _SpecColor.rgb * pow(max(0.0, dot(
    125.                   reflect(-lightDirection, normalDirection),
    126.                   viewDirection)), _Shininess);
    127.             }
    128.  
    129.             return float4(input.vertexLighting + ambientLighting
    130.                + diffuseReflection + specularReflection, 1.0);
    131.          }
    132.          ENDCG
    133.       }
    134.    }
    135. }


    I'll need to add in the ability to handle textures and currently point lights don't work, but I'm happy with it for now.

    ShaderForge looks really great, I'm impressed at how easy it looks to build that. Good to know that both solutions work! And your gif is way higher quality than mine :(

    Thanks for all the help guys,

    Erik
     
    eobet, Bioinformatizer and pea like this.
  8. pea

    pea

    Joined:
    Oct 29, 2013
    Posts:
    98
    Really interesting approach. Thanks for sharing!
     
    Last edited: Nov 14, 2015
  9. Bioinformatizer

    Bioinformatizer

    Joined:
    Nov 27, 2014
    Posts:
    3
    Another great post. Thank you!
     
  10. eobet

    eobet

    Joined:
    May 2, 2014
    Posts:
    176
    I've been looking for this for so long, can't believe I finally found it.

    To have true flat lighting is really, really rare in Unity for some reason!

    Now I just need to learn shaders enough to add more than one directional light, and the other types of light as well (and textures)... :)
     
  11. Iron-Warrior

    Iron-Warrior

    Joined:
    Nov 3, 2009
    Posts:
    671
  12. eobet

    eobet

    Joined:
    May 2, 2014
    Posts:
    176
    I've seen that. I couldn't find any of his examples that worked with Point Lights either. It's all one single directional light only. Also, I wonder how all of these will work with the coming LW pipeline?

    Maybe in a few more years, I'll stumble on a solution. I have time.
     
  13. jRocket

    jRocket

    Joined:
    Jul 12, 2012
    Posts:
    479
    Any ideas as to how this could be accomplished in a Surface Shader? The normal needs to be in tangent space.
     
  14. Aithoneku

    Aithoneku

    Joined:
    Dec 16, 2013
    Posts:
    66
    Were you able to find the solution? I'm trying to solve something similar...
     
  15. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    7,160
    Try this:
    Code (CSharp):
    1. Shader "Custom/FlatSurfaceShader" {
    2.     Properties {
    3.         _Color ("Color", Color) = (1,1,1,1)
    4.         _MainTex ("Albedo (RGB)", 2D) = "white" {}
    5.     }
    6.     SubShader {
    7.         Tags { "RenderType"="Opaque" }
    8.  
    9.         CGPROGRAM
    10.         #pragma surface surf Standard fullforwardshadows vertex:vert
    11.         #pragma target 3.0
    12.  
    13.         sampler2D _MainTex;
    14.  
    15.         struct Input {
    16.             float2 uv_MainTex;
    17.             float3 cameraRelativeWorldPos;
    18.             float3 worldNormal;
    19.             INTERNAL_DATA
    20.         };
    21.  
    22.         half _Glossiness;
    23.         half _Metallic;
    24.         fixed4 _Color;
    25.  
    26.         // pass camera relative world position from vertex to fragment
    27.         void vert(inout appdata_full v, out Input o)
    28.         {
    29.             UNITY_INITIALIZE_OUTPUT(Input,o);
    30.             o.cameraRelativeWorldPos = mul(unity_ObjectToWorld, float4(v.vertex.xyz, 1.0)) - _WorldSpaceCameraPos.xyz;
    31.         }
    32.  
    33.         void surf (Input IN, inout SurfaceOutputStandard o) {
    34.  
    35.             fixed4 c = tex2D (_MainTex, IN.uv_MainTex);
    36.             o.Albedo = c.rgb * _Color.rgb;
    37.  
    38.             // flat world normal from position derivatives
    39.             half3 flatWorldNormal = normalize(cross(ddy(IN.cameraRelativeWorldPos.xyz), ddx(IN.cameraRelativeWorldPos.xyz)));
    40.  
    41.             // construct world to tangent matrix
    42.             half3 worldT =  WorldNormalVector(IN, half3(1,0,0));
    43.             half3 worldB =  WorldNormalVector(IN, half3(0,1,0));
    44.             half3 worldN =  WorldNormalVector(IN, half3(0,0,1));
    45.             half3x3 tbn = half3x3(worldT, worldB, worldN);
    46.  
    47.             // apply world to tangent transform to flat world normal
    48.             o.Normal = mul(tbn, flatWorldNormal);
    49.         }
    50.         ENDCG
    51.     }
    52.     FallBack "Diffuse"
    53. }
    I have a slightly more optimized example of transforming world space normals into tangent space normals from my triplanar normals article, but that technique doesn't work for this particular situation since Unity's Surface Shaders like to aggressive "optimize" away stuff that the shader is actually still using causing all sorts of annoying bugs. The same aggressive optimization is the reason I'm passing in a custom world position even though Unity is already passing that data from the vertex to the fragment. Using camera relative world space also reduces some floating point error issues.
     
    God-at-play, briank and Aithoneku like this.
  16. Aithoneku

    Aithoneku

    Joined:
    Dec 16, 2013
    Posts:
    66
    Thank you very much, it works and it helped me to modify a shader I needed.
     
  17. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    7,160
    Adding some example Shader Graph node graphs for doing flat shading.

    Basic form:


    Note, because the position node is always using the interpolated vertex world space for the LWRP, after about 1000 units from the world origin you will start to see noise appear in the surface normal. If you need to get very close to the object noise will appear at around 50 units from the world origin. The original derivative based vertex fragment shader above has the same problem, but the surface shader I posted does not since it uses camera relative coordinates.

    edit: Since people will likely continue to come across this post, the above node graph has a lot of stuff only needed because early versions of Shader Graph where missing an easy way to transform from world space to tangent space. If you're using a more recent version of Shader Graph (like any version for Unity 2019) the graph looks like this:
    upload_2019-7-29_14-48-33.png
     
    Last edited: Jul 29, 2019
  18. Peter77

    Peter77

    Joined:
    Jun 12, 2013
    Posts:
    4,032
    Why don't you add the shader graph file itself as well, so people don't have to recreate it from the screenshot?
     
    bgolus likes this.
  19. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    7,160
    Here's the shader graph itself. (only includes the pre-Unity 2019 shader)
     

    Attached Files:

    Last edited: Jul 29, 2019
    God-at-play and Peter77 like this.
  20. God-at-play

    God-at-play

    Joined:
    Nov 3, 2006
    Posts:
    303
    Thank you for these! I'm having an issue with the surface shader, though. I'm using this for a procedurally-generated mesh, and I'm noticing that the shading isn't actually flat. There are always some areas shading in the wrong direction, leading to an ugly result. Why would that be?

    upload_2019-9-12_11-51-27.png
     
    Last edited: Sep 12, 2019
  21. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    7,160
    Your geometry still needs valid normals and tangents. They can be smooth normals, but they're still needed. Call mesh.RecalculateNormals() and mesh.RecalculateTangents() on your procedural mesh.

    The reason for this is because there's no way to tell Shader Graph to have the Master node accept world or object space normals directly. The normal input is always a tangent space normal, so if the mesh doesn't at least have usable information there the resulting flat normals created by the above shader won't be usable either.
     
  22. DeltaPiSystems

    DeltaPiSystems

    Joined:
    Jul 26, 2019
    Posts:
    18
    The shader graph doesn't seem to generate flat-shading for me. I am using relatively large coordinates (each cube is 1000x1000x1000)
    notflat.png
    The previously mentioned surface shader had no issues, but surface shaders aren't supported in SRP, and it no longer works.
     
  23. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    7,160
    Hmm ... I wonder if Unity broke something then. @God-at-play & @DeltaPiSystems what version of Unity & which SRP version are you using?
     
  24. DeltaPiSystems

    DeltaPiSystems

    Joined:
    Jul 26, 2019
    Posts:
    18
    2019.1.12f1 (old I guess, might be the issue)
    LWRP 5.7.2