Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Mesh shader to outline colour changes

Discussion in 'Shaders' started by DeeCeptor, Mar 26, 2020.

  1. DeeCeptor

    DeeCeptor

    Joined:
    Aug 4, 2013
    Posts:
    31
    Howdy. I've written a shader to change colour on a deformable mesh's Z position. This is to create the effect of topographic/contour maps where the colour changes based on the height of terrain of the mesh.

    This is it viewed from the side:

    And viewed head-on (as it's meant to be:

    What I need is for a black outline between two colour levels. I have done this, but the problem is that the black outline is not pixel perfect (look at the black grouping near the bottom left of the image).

    My current shader simply uses if statements to determine which colour level each part should be in, and if it's within a certain threshold BETWEEN two colour layers, make it black. Obviously this is wrong.

    Here's my shader code:
    Code (JavaScript):
    1. Shader "Unlit/MapEditorShader"
    2. {
    3.     Properties {
    4.  
    5.     _Level8Color ("Level8Color", Color) = (0.8,0.9,0.9,1)  
    6.     _Level8 ("Level8", Float) = 30
    7.     _Level7Color ("Level7Color", Color) = (0.8,0.9,0.9,1)  
    8.     _Level7 ("Level7", Float) = 30
    9.     _Level6Color ("Level6Color", Color) = (0.8,0.9,0.9,1)  
    10.     _Level6 ("Level6", Float) = 30
    11.     _Level5Color ("Level5Color", Color) = (0.8,0.9,0.9,1)  
    12.     _Level5 ("Level5", Float) = 30
    13.     _Level4Color ("Level4Color", Color) = (0.8,0.9,0.9,1)  
    14.     _Level4 ("Level4", Float) = 30
    15.     _Level3Color ("Level3Color", Color) = (0.75,0.53,0,1)
    16.     _Level3 ("Level3", Float) = 20
    17.     _Level2Color ("Level2Color", Color) = (0.69,0.63,0.31,1)
    18.     _Level2 ("Level2", Float) = 10
    19.     _Level1Color ("Level1Color", Color) = (0.65,0.86,0.63,1)
    20.     _Level1 ("Level1", Float) = 0
    21.     _Level0Color ("Level0Color", Color) = (0.37,0.78,0.92,1)
    22.     _OutlineColor ("OutlineColor", Color) = (0,0,0,1)
    23.     _Slope ("Slope Fader", Range (0,1)) = 0
    24.  
    25. }
    26. SubShader {
    27.     Tags { "RenderType" = "Diffuse" }
    28.     CGPROGRAM
    29.     #pragma surface surf SimpleLight
    30.     struct Input {
    31.         float3 customColor;
    32.         float3 worldPos;
    33.     };
    34.  
    35.     half4 LightingSimpleLight (SurfaceOutput s, half3 lightDir, half atten) {
    36.               half4 c;
    37.               c.rgb = s.Albedo * 0.8;
    38.               c.a = s.Alpha;
    39.               return c;
    40.           }
    41.  
    42.     float _Level8;
    43.     float4 _Level8Color;
    44.     float _Level7;
    45.     float4 _Level7Color;
    46.     float _Level6;
    47.     float4 _Level6Color;
    48.     float _Level5;
    49.     float4 _Level5Color;
    50.     float _Level4;
    51.     float4 _Level4Color;
    52.     float _Level3;
    53.     float4 _Level3Color;
    54.     float _Level2;
    55.     float4 _Level2Color;
    56.     float _Level1;
    57.     float4 _Level1Color;
    58.     float _Slope;
    59.     float4 _Level0Color;
    60.     float4 _OutlineColor;
    61.  
    62.     void surf (Input IN, inout SurfaceOutput o) {
    63.     //at breakpoints we want a black line
    64.         if((IN.worldPos.z > _Level8 - _Slope && IN.worldPos.z < _Level8 + _Slope)||
    65.         (IN.worldPos.z > _Level7 - _Slope && IN.worldPos.z < _Level7 + _Slope)||
    66.         (IN.worldPos.z > _Level6 - _Slope && IN.worldPos.z < _Level6 + _Slope)||
    67.         (IN.worldPos.z > _Level5 - _Slope && IN.worldPos.z < _Level5 + _Slope)||
    68.         (IN.worldPos.z > _Level4 - _Slope && IN.worldPos.z < _Level4 + _Slope)||
    69.         (IN.worldPos.z > _Level3 - _Slope && IN.worldPos.z < _Level3 + _Slope)||
    70.         (IN.worldPos.z > _Level2 - _Slope && IN.worldPos.z < _Level2 + _Slope)||
    71.         (IN.worldPos.z > _Level1 - _Slope && IN.worldPos.z < _Level1 + _Slope))
    72.             o.Albedo = _OutlineColor;
    73.         else if (IN.worldPos.z > _Level8)
    74.             o.Albedo = _Level8Color;
    75.         else if (IN.worldPos.z > _Level7)
    76.             o.Albedo = _Level7Color;
    77.         else if (IN.worldPos.z > _Level6)
    78.             o.Albedo = _Level6Color;
    79.         else if (IN.worldPos.z > _Level5)
    80.             o.Albedo = _Level5Color;
    81.         else if (IN.worldPos.z > _Level4)
    82.             o.Albedo = _Level4Color;
    83.         else if (IN.worldPos.z > _Level3)
    84.             o.Albedo = _Level3Color;
    85.         else if (IN.worldPos.z > _Level2)
    86.             o.Albedo = _Level2Color;
    87.         else if (IN.worldPos.z > _Level1)
    88.             o.Albedo = _Level1Color;
    89.         else
    90.             o.Albedo = _Level0Color;
    91.  
    92.     }
    93.     ENDCG
    94. }
    95. Fallback "Opaque"
    96. }
    Any suggestions would be appreciated.
     
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    This is a much harder problem to solve than it might seem. Going from the mesh Z alone you can only guarantee the line thickness in that z, not in screen space. You can get something close-ish using screen space partial derivatives, but it won’t be perfect due to the derivatives being per triangle.
    Code (csharp):
    1. // code for setting level colors w/o the lines
    2.     o.Albedo = _Level0Color;
    3.  
    4. // get how much the z changes between each pixel and the one next to it
    5. float zDeriv = fwidth(IN.worldPos.z);
    6.  
    7. // set this to half of how wide you want your lines
    8. float linePixelHalfWidth = 0.5;
    9.  
    10. // creates an anti-aliased black line on white at each level
    11. float lines = saturate(abs(IN.worldPos.z - _Level1) / zDeriv - linePixelHalfWidth)
    12.     * saturate(abs(IN.worldPos.z - _Level2) / zDeriv - linePixelHalfWidth)
    13.     * saturate(abs(IN.worldPos.z - _Level3) / zDeriv - linePixelHalfWidth)
    14.     // etc
    15.  
    16. // multiply albedo by lines
    17. o.Albedo *= lines;
    If the lines are at regular height intervals, this can be made significantly more efficient. The lines can make use of
    frac()
    to repeat essentially for free, and a texture with your height colors will be way, way faster that a long chain of if statements. Even an array of color values instead of a list of properties will be way faster.
     
  3. DeeCeptor

    DeeCeptor

    Joined:
    Aug 4, 2013
    Posts:
    31
    Thanks for the tip - looks like screen space derivative was the right call. Replacing _Slope with the line below did the trick.

    float slope = fwidth (IN.worldPos.z) * _Slope;