Search Unity

Why do my territory borders look awful?

Discussion in 'General Graphics' started by dalanchoo, Oct 11, 2021.

  1. dalanchoo

    dalanchoo

    Joined:
    Jun 6, 2014
    Posts:
    44
    In my game, I have a bunch of territories. These territories have a shader that allows me to draw an outline around the border of the territory. The outline is inside the edge of the territory. I can control many settings, such as the border color, border thickness, etc.

    I do a pathfind from the selected territory to the one I'm pointing at and I hilite each territory that is part of the path by drawing its border in yellow instead of black. (I hilite the last territory in white instead of black).

    So, this works but the hilite looks terrible. Most of the time the border is sort of a crosshatch pattern on black and yellow or black and white. In a few places, the line comes through as a solid yellow, which looks a lot better but is still not the best.

    How do I go about fixing this? I don't know if it makes any difference, but I'm developing this for a Quest 2.



    I'm attaching the shader I use and a video showing the problem.

    Code (CSharp):
    1. Shader "ConquestVR/InternalLine"
    2. {
    3.     Properties
    4.     {
    5.         _FillColor("Fill Color", Color) = (1,0,0,0)
    6.         _BorderColor("Border Color", Color) = (0,0,0,1)
    7.  
    8.         _HilitedFillColorScale("Selected Color Scale", Range(0.5, 1)) = 0.9
    9.         _HilitedBorderColor("Hilited Border Color", Color) = (0,0,0,1)
    10.  
    11.         _SelectedFillColor("Selected Fill Color", Color) = (1,0,0,0)
    12.         _SelectedBorderColor("Selected Border Color", Color) = (1,0,0,0)
    13.         _SelectedPulseSpeed("Selection Pulse Speed", Range(1.0, 10)) = 3.75
    14.  
    15.         _PathHiliteFillColorScale("Path Hilite Color Scale", Range(0.5, 1)) = 0.9
    16.         _PathHiliteBorderColor("Path Hilite Border Color", Color) = (0,0,0,1)
    17.  
    18.         _MainTex("Texture", 2D) = "white" {}
    19.         _BorderWidth("Border width", Range(0, 1)) = 0.1
    20.     }
    21.  
    22.     SubShader
    23.     {
    24.         Tags { "Queue"="Geometry" "RenderType"="Opaque" }
    25.  
    26.         Pass
    27.         {
    28.             CGPROGRAM
    29.             #pragma vertex vert
    30.             #pragma fragment frag
    31.             #pragma multi_compile_instancing
    32.             #pragma shader_feature SELECTED
    33.             #pragma shader_feature HILITED
    34.             #pragma shader_feature PATH_HILITE
    35.  
    36.             #include "UnityCG.cginc"
    37.  
    38.             struct appdata
    39.             {
    40.                 float4 vertex : POSITION;
    41.                 float4 color : COLOR;
    42.             };
    43.  
    44.             struct v2f
    45.             {
    46.                 float4 vertex : SV_POSITION;
    47.                 fixed4 color : COLOR;
    48.             };
    49.  
    50.             v2f vert (appdata v)
    51.             {
    52.                 v2f o;
    53.  
    54.                 o.vertex = UnityObjectToClipPos(v.vertex);
    55.                 o.color = fixed4(v.color.rgb, v.color.a);
    56.  
    57.                 return o;
    58.             }
    59.  
    60.             float4 _FillColor;
    61.             float4 _BorderColor;
    62.  
    63.             float _HilitedFillColorScale;
    64.             float4 _HilitedBorderColor;
    65.  
    66.             float4 _SelectedFillColor;
    67.             float4 _SelectedBorderColor;
    68.             float _SelectedPulseSpeed;
    69.  
    70.             float _PathHiliteFillColorScale;
    71.             float4 _PathHiliteBorderColor;
    72.  
    73.             float _BorderWidth;
    74.  
    75.             fixed4 frag (v2f i) : Color
    76.             {
    77.                 // Inside the if is the internal color
    78.                 if (i.color.g >= _BorderWidth)
    79.                 {
    80. #if SELECTED
    81.                     // When selected, we pulsate between the regular fill color and the selected fill color
    82.                     float t = _Time[1] * _SelectedPulseSpeed;
    83.                     float negOneToPosOne = sin(t);
    84.                     float zeroToOne = (negOneToPosOne + 1.0f) / 2.0f;
    85.                     float base = zeroToOne;
    86.                     float extra = 1.0f - base;
    87.  
    88.                     return (base * _SelectedFillColor) + (extra * _FillColor);
    89. #elif HILITED
    90.                     float4 outColor = float4(_FillColor.rgb * _HilitedFillColorScale, _FillColor.a);
    91.                     return outColor;
    92.  
    93. #elif PATH_HILITE
    94.                     float4 outColor = float4(_FillColor.rgb * _PathHiliteFillColorScale, _FillColor.a);
    95.                     return outColor;
    96. #else
    97.                     return _FillColor;
    98. #endif
    99.                 }
    100.  
    101.                 // Down here is for border
    102. #if SELECTED
    103.                 return _SelectedBorderColor;
    104. #elif HILITED
    105.                 return _HilitedBorderColor;
    106. #elif PATH_HILITE
    107.                 return _PathHiliteBorderColor;
    108. #else
    109.                 return _BorderColor;
    110. #endif
    111.  
    112.                 //float t = _Time[1] * 10.0f;
    113.                 //float negOneToPosOne = sin(t);
    114.                 //float zeroToOne = (negOneToPosOne + 1.0f) / 2.0f;
    115.                 //float base = zeroToOne;
    116.                 //float extra = 1.0f - base;
    117.  
    118.                 //return (base * _BorderColor) + (extra * _HilitedBorderColor);
    119.             }
    120.  
    121.             ENDCG
    122.         }
    123.     }
    124. }
    125.  
    Thanks

    John Lawrie
     

    Attached Files:

    Last edited: Oct 13, 2021
  2. warthos3399

    warthos3399

    Joined:
    May 11, 2019
    Posts:
    1,758
    Your common denominator is Shader, look into shader settings/compatability or modification...
     
  3. dalanchoo

    dalanchoo

    Joined:
    Jun 6, 2014
    Posts:
    44
    Thanks for your reply. Though I don't exactly know what I should be looking for.

    I opened up "Project Settings" | "Graphics" and I see there are some options related to shaders, but they appear to be related to built-in shaders. I don't see a "Compatability" or "Modification" setting. This is what I see.



    Is this where I should be looking?

    (The pane to the right shows the options I see when I have my Territory material selected.)

    Cheers,
    John
     
  4. Shane_Michael

    Shane_Michael

    Joined:
    Jul 8, 2013
    Posts:
    158
    What you're seeing is called "aliasing". You are defining your border with an "if" statement so it is an infinitely sharp edged line, and the value of the pixel depends entirely whether it is on one side or the other. If you have a very thin line some pixels miss it entirely giving you an irregular outline. Even when the line is quite thick the edges will be very jagged.

    You need to add some transitional area to your border.

    Something like this is probably enough, and lets you customize the transition width:
    Code (csharp):
    1.  
    2. fixed4 frag (v2f i) : Color
    3. {          
    4.     fixed borderBlend = smoothstep(_BorderMin, _BorderMax, i.color.g);
    5.     return lerp(_FillColor, _BorderColor, borderBlend);          
    6. }
    7.  
    That gives you a constant border transition, and will probably be fine if you are typically viewing it a fixed distance. You could modify the edge smoothness dynamically using the screen-space partial derivative:

    Code (csharp):
    1.  
    2. fixed4 frag (v2f i) : Color
    3.     {
    4.            
    5.     fixed ddg = max(ddx(i.color.g), ddy(i.color.g)) * _BorderSmoothness;
    6.            
    7.     fixed borderBlend = smoothstep(_BorderMax - ddg, _BorderMax, i.color.g);
    8.     return lerp(_FillColor, _BorderColor, borderBlend);        
    9. }  
    10.  
    That should work better if you are looking at it from different distances, but I wouldn't use that unless you have issues with the simpler version. And this is all off the top of my head so no guarantees on typos, but that is the general idea.

    As an aside, I removed most of the code in your shader because this is really all it should be doing.

    Your pulse value doesn't change per-pixel so doing it in the fragment shader is running millions of redundant calculations per frame. You should be calculating it once and baking it into your fill color. You also don't want to use shader variants for something as trivial as selecting colors. Pre-calculate that and put the colors in a per-territory buffer that so you can more efficiently batch your scene. The SPR batcher will do this automatically with materials that share a shader variant so you can use one material per territory and the batcher will do it for you. You can do the same thing with the built-in pipeline but it will take more manual work. Reading the documentation on writing shaders compatible with the SPR batcher would be useful with this.

    It wouldn't really matter on PC, but on Quest you really need to minimize graphical state changes so you want to render your entire map in one draw call with a single shader variant.
     
  5. dalanchoo

    dalanchoo

    Joined:
    Jun 6, 2014
    Posts:
    44
    Thanks for the info.

    Just to make sure I understand, the border color and fill color would be computed in C# and passed into the shader. This would also include the calculation of the pulse color. Basically, the pulsing is just the fill color. So I would only need to compute it once per frame instead of once per pixel per frame.

    Seems to make sense to me.