Search Unity

Distance Field Alpha Testing: Cheap Smooth Outlines

Discussion in 'Made With Unity' started by runevision, Apr 23, 2008.

  1. runevision

    runevision

    Unity Technologies

    Joined:
    Nov 28, 2007
    Posts:
    1,558
    Using just a regular low resolution image with a specially created alpha channel, textures with alpha testing can have smooth outlines at any level of zoom, as if they were vector graphics. Curves and lines at any angle remain sharp and free of aliasing artifacts.

    :arrow: The big attached picture (see below) was made with the small 64x64 texture below it.

    The method used for this is outlined in Valve's paper below
    http://www.valvesoftware.com/publications/2007/SIGGRAPH2007_AlphaTestedMagnification.pdf

    This method uses the GPU's native bilinear interpolation and doesn't cost anything.
    Imagine this on vegetation and vector like interface.

    Laurent Lavigne (LLavigne in the forum and the IRC channel) and I discussed and tested this technique yesterday after reading the article by Valve. The images above are made by him, using PhotoShop to create the alpha channel.

    Realtime demonstration
    I made a test too, using scripting in Unity to dynamically create a texture with alpha channel at runtime. The test can be seen in a web player here:

    :arrow: http://runevision.com/multimedia/unity/test/Distance Fields.html

    The test demonstrates the technique and compares it to the artifacts obtained using regular alpha channels. The test project is also attached as a package.

    The technique works like this:
    Instead of an anti aliased silhouette, the alpha channel should contain gray scale values representing the distance to the shape. Pixels on the edge of the shape should be mid-gray (0.5 or 128). On the outside of the shape, pixels should go towards black (0.0 or 0) as the distance to the (nearest point on the) edge increases. On the inside of the shape, pixels should go towards white (1.0 or 255) as the distance to the (nearest point on the) edge increases.

    There's one important detail:
    The distances must be based on a high resolution image of the shape. Preferably several times higher than the texture the technique is used for.

    The technique works by making clever use of the bilinear interpolation that is used anyway in alpha cutoff calculations. Straight lines can be represented 100% correct by this technique. Curves can be represented with linear approximation inside the square area of each pixel. The technique does not preserve sharp corners, but cuts off a piece of the corner smaller than the area of one pixel. This is the biggest flaw compared to real vector graphics, but it is still negligible compared to the gross aliasing artifacts obtained with alpha channels not using this technique.

    The technique has many uses
    Foliage springs to mind as one use that could result in much better images up close. Another interesting use is for text. Unity currently burn text to a texture using a standard technique that doesn't work well with scaling. I don't know if real vector based text is planned for a future use of Unity, but the distance fields based alpha technique is a viable alternative that is much closer to the current implementation. Text would then be scalable to any size and look equally good. Sharp corners would still be cut off, but this is negligible compared to current artifacts.

    Special effects
    So far only alpha cutoff has been discussed but the technique makes a lot more possible if used together with custom shaders, as presented in the article by Valve. For example, an outline of a shape can easily be made. The outline can be any thickness, even much thinner than the width of one pixel in the texture. Various possible effects include:

    - Outlines
    - Glows
    - Drop shadows

    All this would be based simply on the interpolated alpha value and is thus very cheap.

    Using the technique
    Given how simple the technique is and how easy is it to implement, it is surprising that it isn't more widespread, even in AAA titles. To make the technique easily accessible in the Unity community, a tool should be implemented that can automatically create the low resolution alpha channel given a high resolution black and white image of the shape. I will try to work on such a tool (or script). (EDIT: I posted the script further down in the thread.) Please make your own experiments as well - I hope we can all make good use of this technique!

    Laurent Lavigne and Rune Skovbo Johansen
     

    Attached Files:

  2. Aras

    Aras

    Unity Technologies

    Joined:
    Nov 7, 2005
    Posts:
    4,534
    That is awesome. I wanted to play around with this for quite some time, but never found the time. Now I don't have to!

    Awesome.
     
  3. bigkahuna

    bigkahuna

    Joined:
    Apr 30, 2006
    Posts:
    5,435
    Very interesting!
     
  4. runevision

    runevision

    Unity Technologies

    Joined:
    Nov 28, 2007
    Posts:
    1,558
    Well, you could lobby for this technique being used for the Text textures in Unity. ;)

    Rune
     
  5. metervara

    metervara

    Joined:
    Jun 15, 2006
    Posts:
    203
    Very cool!
     
  6. Omar Rojo

    Omar Rojo

    Joined:
    Jan 4, 2007
    Posts:
    494
    This is awesome, thanks for sharing the PDF !

    and congratulations for your implementation !

    .org
     
  7. AngryAnt

    AngryAnt

    Keyboard Operator Moderator

    Joined:
    Oct 25, 2005
    Posts:
    3,042
    Very interesting. Gonna watch this thread for updates. Looking forward to the tool / editor integration. Please throw me a line if you need a hand.
     
  8. polytropoi

    polytropoi

    Joined:
    Aug 16, 2006
    Posts:
    671
    Wow. Thanks a lot for sharing this!
     
  9. cblarsen

    cblarsen

    Joined:
    Mar 10, 2007
    Posts:
    267
    Thanks! This can be used for _so_ many things. For example, I am considering how to combine it with heightmaps :D
     
  10. Omar Rojo

    Omar Rojo

    Joined:
    Jan 4, 2007
    Posts:
    494
    Im getting some weird behavior..

    I think this is because of the alpha cutout shader ? that is somehow cutting off in a more intelligent way ? (instead of blending doing some grid density)


    The attached image is a close up of the border..

    .org
     

    Attached Files:

  11. Jessy

    Jessy

    Joined:
    Jun 7, 2007
    Posts:
    7,327
    This technology seems really cool, but your web player crashed both my PC and brand new MacBook Pro.
     
  12. runevision

    runevision

    Unity Technologies

    Joined:
    Nov 28, 2007
    Posts:
    1,558
    It looks odd. I don't know what the reason is. Does it look the same in the web player I linked to?

    I'm sorry to hear that. I don't know what the reason is...

    Rune
     
  13. runevision

    runevision

    Unity Technologies

    Joined:
    Nov 28, 2007
    Posts:
    1,558
    I've been working on an editor script that can create an alpha channel with distance field given a high resolution texture.

    I wouldn't call the script below "done" but it is in a mostly working state now. It's not very well optimized and a feature to downsample the color channels as well is only implemented naively.

    To use the script simply put it in the Editor folder. (If it doesn't exist, create it in the root of the Asset Folder.) Then choose
    Custom -> Distance Field Texture
    from the menu and supply settings as you wish.

    Edit: A few extra notes:
    - The generated texture will be saved in the same location as where the original is located.
    - The saved image will have the same name, but with .lores.png added to the end.
    - You may need to refresh the project view before the saved image is visible.
    - Really big input images are needed to get proper straight lines. In my tests I needed to have the input image be 8 times as big (in both directions) as the generated image to avoid artifacts.
    - In the wizard you need to take focus away from a text field before the changed value takes effect. This may be a bug in Unity (yes, I will report it).

    :arrow: Edit (April 29): The old script had some flaws in it. This version is more brute force but give much better results.

    Code (csharp):
    1. using UnityEngine;
    2. using UnityEditor;
    3. using System.Collections;
    4. using System.Collections.Generic;
    5. using System.IO;
    6.  
    7. public class AlphaFieldGenerator : ScriptableWizard {
    8.    
    9.     public enum InputChannel {
    10.         INPUT_ALPHA,
    11.         INPUT_GRAYSCALE,
    12.         INPUT_RED
    13.     }
    14.    
    15.     public Texture2D hiRes;
    16.     private Texture2D loRes;
    17.     public int outputWidth = 128;
    18.     public int outputHeight = 128;
    19.     public InputChannel inputChannel = InputChannel.INPUT_GRAYSCALE;
    20.     public bool invertField = false;
    21.     public bool includeColorChannels = false;
    22.     public float threshold = 0.5f;
    23.     public float fieldRange = 5.0f;
    24.    
    25.     [MenuItem("Custom/Distance Field Texture")]
    26.     public static void CreateWizard() {
    27.         DisplayWizard("Generate Texture With Alpha Field", typeof(AlphaFieldGenerator), "Create"); 
    28.     }
    29.    
    30.     private float GetValue(Color c) {
    31.         if (inputChannel==InputChannel.INPUT_ALPHA) return c.a;
    32.         if (inputChannel==InputChannel.INPUT_GRAYSCALE) return c.grayscale;
    33.         return c.r;
    34.     }
    35.    
    36.     void OnWizardCreate() {
    37.         loRes = new Texture2D(outputWidth, outputHeight, TextureFormat.ARGB32, true);
    38.         int[,][] nearestPoints = new int[outputWidth, outputHeight][];
    39.        
    40.         float maxRadius = Mathf.Max(
    41.             fieldRange,
    42.             Mathf.Max(
    43.                 hiRes.width *1.0f / loRes.width,
    44.                 hiRes.height *1.0f / loRes.height
    45.             )*1.5f
    46.         );
    47.        
    48.         for (int y=0; y < loRes.height; ++y) {
    49.         for (int x=0; x < loRes.width; ++x) {
    50.            
    51.             // Corresponding pixels in hiRes texture
    52.             int xorig = (int) ((x+0.5f) * hiRes.width / loRes.width -0.5f);
    53.             int yorig = (int) ((y+0.5f) * hiRes.height / loRes.height -0.5f);
    54.             Color origColor = hiRes.GetPixel(xorig, yorig);
    55.            
    56.             // Determine insidedness
    57.             bool side = (GetValue(origColor) > threshold);
    58.            
    59.             // Search for closest distance to other side
    60.             float lowestDist = Mathf.Max(hiRes.width, hiRes.height);
    61.             bool found = false;
    62.             for (int radius=1; radius<maxRadius; radius++) {
    63.                 for (int c=0; c<radius*2; c++) {
    64.                     for (int d=0; d<4; d++) {
    65.                         int X, Y;
    66.                         if      (d==0) { X = xorig-radius+c; Y = yorig-radius; }
    67.                         else if (d==1) { X = xorig+radius; Y = yorig-radius+c; }
    68.                         else if (d==2) { X = xorig+radius-c; Y = yorig+radius; }
    69.                         else           { X = xorig-radius; Y = yorig+radius-c; }
    70.                         if ((GetValue(hiRes.GetPixel(X,Y)) > threshold) != side) {
    71.                             found = true;
    72.                             float dist = new Vector2((X-xorig)*1.0f, (Y-yorig)*1.0f).magnitude;
    73.                             if (dist < lowestDist) {
    74.                                 lowestDist = dist;
    75.                                 nearestPoints[x,y] = new int[] {X,Y};
    76.                             }
    77.                         }
    78.                     }
    79.                 }
    80.                 if (radius > lowestDist) break;
    81.             }
    82.            
    83.             // Calculate value
    84.             float value = 0.0f;
    85.             if (found) {
    86.                 value = Mathf.Clamp01( 0.5f - lowestDist / fieldRange / 2 );
    87.             }
    88.             if (side) value = 1-value;
    89.             if (invertField) value = 1-value;
    90.             Color color = Color.black;
    91.             if (includeColorChannels) color = origColor;
    92.             color.a = value;
    93.            
    94.             loRes.SetPixel(x, y, color);
    95.         }
    96.         EditorUtility.DisplayProgressBar("Calculating Distance field","Calculating close field...", y*1.0f/loRes.height);
    97.         }
    98.        
    99.         loRes.Apply();
    100.        
    101.         //Save to disk
    102.         string outputName = EditorUtility.GetAssetPath(hiRes);
    103.         // FIXME change extension properly
    104.         outputName += ".lores.png";
    105.         File.WriteAllBytes(outputName, loRes.EncodeToPNG());
    106.        
    107.         EditorUtility.ClearProgressBar();
    108.     }
    109. }
    Rune
     
  14. Omar Rojo

    Omar Rojo

    Joined:
    Jan 4, 2007
    Posts:
    494
    In the webplayer the surface looks ok, but in my editor (unity pro 2.0) looks like that and the sharp edges are lost :(

    Im pretty sure the shader or the graphics card have something to do with it.. any advice from the pros ?

    .org
     
  15. runevision

    runevision

    Unity Technologies

    Joined:
    Nov 28, 2007
    Posts:
    1,558
    Here is a test where I have used the distance field technique with a font texture.

    Below you can see 3 lines.

    :arrow: The first line is a 256x256 font texture (generated by Unity) with the Unity default font shader (GUI/Text Shader).

    :arrow: The second line is the same 256x256 texture but with cutoff alpha applied (Transparent/Cutout/Diffuse).

    :arrow: The third line is a 256x256 texture using the distance field technique for the alpha channel. It also use cutoff alpha (Transparent/Cutout/Diffuse). The distance field is generated based on a 2048x2048 texture.

    As can be seen, the distance field technique doesn't give more details, but it does give much smoother lines and curves.

    Note: There are a few artifacts in the bottom line, most visible in the top right part of the "y". This is caused by the fact that the letters are very close to each other in the Unity generated font textures, which make the distance fields interfere. If the textures could be generated with a few more pixels in between the letters this problem would go away.

    Rune
     

    Attached Files:

  16. jjnstn

    jjnstn

    Joined:
    Mar 26, 2008
    Posts:
    53
    This technique has tons of possibilities... thank you...
     
  17. forestjohnson

    forestjohnson

    Joined:
    Oct 1, 2005
    Posts:
    1,370
    Wow, add multisampling or other AA to this and its almost like getting something for nothing. Well done!
     
  18. Cameron860

    Cameron860

    Joined:
    Jun 1, 2009
    Posts:
    764
    This seemed to disappear into the void, was there ever anything more done with this, any polished editor tools ect?
     
  19. Tim-C

    Tim-C

    Unity Technologies

    Joined:
    Feb 6, 2010
    Posts:
    2,087
    Hey cool, I implemented this in unity as well a few months ago. I use it for all my font rendering / some other secret stuff I am working on. It's a really cool technique and has heaps of applications outside of what you would expect. An example is that it's great for rendering shadow maps and things like that :D

    Here is a pic of the tool I made for it:


    Also it looks like you are using straight up distance fields (and only single direction distance fields so you will lose edges). Make them signed and add a second (opposite direction) texture generation pass on generation and they will be much more flexible!
     
    Last edited: Dec 20, 2010
  20. Timmey

    Timmey

    Joined:
    Apr 13, 2010
    Posts:
    35
  21. Ippokratis

    Ippokratis

    Joined:
    Oct 13, 2008
    Posts:
    1,504
    Hi. I implemented a shader for this, feel free to use it. It works on the Iphone too !
    Code (csharp):
    1.  
    2. Shader "Ippo/Font/Plain" {
    3.   Properties {
    4.         _Color ("Main Color", Color) = (1,1,1,1)
    5.         _AlphaMin ("Alpha Min", Range(0.0,1.0)) = 0.49
    6.         _AlphaMax ("Alpha Max", Range(0.0,1.0)) = 0.54
    7.         _MainTex ("Base (RGB)", 2D) = "white" {}
    8.     }
    9. SubShader {
    10. Tags {
    11.             "Queue"="Transparent"
    12.             "IgnoreProjector"="True"
    13.             "RenderType"="Transparent"
    14.         }
    15.         Lighting Off
    16.         Cull Off
    17.         ZTest Always
    18.         ZWrite Off
    19.         Fog {
    20.             Mode Off
    21.         }
    22.         Blend SrcAlpha OneMinusSrcAlpha
    23.     Pass {
    24.  
    25. CGPROGRAM
    26. #pragma vertex vert
    27. #pragma fragment frag
    28. #include "UnityCG.cginc"
    29.  
    30. sampler2D _MainTex;
    31. half4 _MainTex_ST;
    32. half4 _Color;
    33. half _AlphaMin;
    34. half _AlphaMax;
    35.  
    36. struct v2f {
    37.     half4 pos : SV_POSITION;
    38.     half2 uv: TEXCOORD0;
    39.     half3 color : COLOR0;
    40. };
    41.  
    42. v2f vert (appdata_base v)
    43. {
    44.     v2f o;
    45.     o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
    46.     o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
    47.     return o;
    48. }
    49.  
    50. half4 frag(v2f i) : COLOR
    51. {
    52.     half4 base = tex2D(_MainTex, i.uv);
    53.     half alpha = smoothstep(_AlphaMin, _AlphaMax, base.w);
    54.     half3 other = _Color * alpha;    
    55.     return half4 (other, alpha);
    56. }
    57. ENDCG
    58.  
    59.     }
    60. }
    61. Fallback "VertexLit"
    62. }
    63.  
     
  22. herbie

    herbie

    Joined:
    Feb 11, 2012
    Posts:
    237
    I know this is an old thread but I think this is very interesting for me.

    I have Unity pro 3.56 and I have placed the script in the Edit folder. Now Unity is busy with calculating distance field from a .png.
    Is it right that this takes a lot of time (couple of hours)? (Edit: Stupid me, I just forgot to set to ARGB32 format in the inspector)

    This is a thread from 2008. Is this method still up to date or are there other better ways to get smooth outlines at any level of zoom nowadays?
     
    Last edited: Mar 13, 2013
    JoeStrout likes this.
  23. Erind

    Erind

    Joined:
    Jul 23, 2014
    Posts:
    56
  24. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    7,909
    I'm trying to learn about this distance-field technique to produce sharper line art on Oculus Go.
    upload_2019-2-19_13-49-3.png

    You can see the aliasing that a standard shader produces (and it looks much worse in the headset, because of the crawling the aliased edges do with every head movement).

    This distance-field technique sounds amazing, but this is the newest thread I can find about it, and it's so old that most of the images are broken. And most search results have to do with text rendering, which of course I don't worry about since TextMeshPro is built in now. But I do still worry about rendering line art, like the above.

    So. Has anybody worked with this in the last half-decade? Any reason to expect trouble on Android/Go?
     
  25. Ippokratis

    Ippokratis

    Joined:
    Oct 13, 2008
    Posts:
    1,504
    Are you sure that you have set up the mip maps correctly in the flag texture to start with?
    Have you took into consideration the texel density of your texture?
    Or this is some 4k map with no mip maps on a 300x300 pixels wide surface ?
     
  26. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    7,909
    Yes, I'm sure. My question is about the Distance Field technique.

    Have you done any more with it since your 2011 post above?

    I found this project about multi-channel distance fields. It looks quite impressive, but not easy to integrate into Unity. I'm curious whether anyone has tried that?
     
  27. Ippokratis

    Ippokratis

    Joined:
    Oct 13, 2008
    Posts:
    1,504
    I will rephrase my previous answer, hope it makes more sense this way (English is tough for me):
    I believe that the problem you are having is not going to be solved with the approach you re trying.
    The problem seems to be related to wrong texel density settings, as described above.

    "Have you done any more with it since your 2011 post above?"
    Yes, 7 years ago :)

    "My question is about the Distance Field technique."
    It can be used on every platform, with little performance penalties. For instance, TextMesh Pro uses this technique. To use it with line art you must create a distance field texture first from the line art texture. Runevision code from the above posts can be used for this purpose. Then you use the generated texture with a shader, like the one I shared. Other approaches, like distance volumes, or the project you linked can be harder to implement and more expensive, but this technique is quite simple.

    Post here if you still wish to implement this and you are having trouble with the implementation, I will try to help.