Search Unity

Fade to Black in Linear color space

Discussion in 'General Graphics' started by Aaron-Meyers, May 6, 2019.

  1. Aaron-Meyers

    Aaron-Meyers

    Joined:
    Dec 8, 2009
    Posts:
    305
    So whenever I've needed to fade to black in Unity, I've always used the old black UI Image with a tweened alpha and its great.

    However, I'm working on a project that is in Linear rather than Gamma and I noticed that the dark end of my fading curve was very short and the light part a much longer tale. Its an entirely different curve due to Linear space. I knew my eyes were seeing it, but to confirm it, I tried putting the black UI Image in front of a white one, adjusting the alpha and checking the white level with the eye dropper tool.

    So when the alpha is at 50%, the color showing through is at 73 in the V of HSV (in Gamma color space, its always the inverse of the alpha of the black image). When the alpha is at 90%, its already 35 V!!!

    I tried writing a custom sprite shader to see if converting from Linear to Gamma
    Code (CSharp):
    1. return pow( IN.color, 1.0 / 2.2 );
    This brings it a bit closer, but its still not really the right curve... the dark end of the fade is still too short and when fading to black, there is an extremely noticeable jump from full brightness to a darker color. Not subtle at all.

    Wondering if anyone has any ideas about how to get a nice proper black fade in Linear space.
     
  2. Gkqt

    Gkqt

    Joined:
    Dec 1, 2018
    Posts:
    11
    Isn't it easier to just adjust your tweening curve?
     
  3. Aaron-Meyers

    Aaron-Meyers

    Joined:
    Dec 8, 2009
    Posts:
    305
    that's essentially what the code in my shader is doing... exponentially reshaping the curve...

    but if you know a way to get a nice linear transition like you automatically get in Gamma color space, please share...
     
  4. Gkqt

    Gkqt

    Joined:
    Dec 1, 2018
    Posts:
    11
    I couldn't tell you an algorithm for exact translation of gamma to linear. What I would do is link the tweening to an animation curve with curve.Evaluate(t), and then simply tune the curve until I get the result I want.

    Something like this: (warning - untested psuedocode)

    Code (CSharp):
    1. public float tweenDuration = 5;
    2. public AnimationCurve curve;
    3. float tween = 0;
    4. float tweenScale;
    5. bool active = true;
    6.  
    7. void Start()
    8. {
    9.    tweenScale = 1/tweenDuration;
    10. }
    11.  
    12. void Update()
    13. {
    14.     if(tween < 1 && active)
    15.    {
    16.         color.alpha = curve.Evaluate(tweenDuration) //pseudo
    17.         tween += time.deltaTime * tweenScale; //scales the tween duration to between 0 and 1
    18.    }
    19.    if(tweenDuration>1 && active)active = false;
    20. }
    This way you can choose how long the tweening takes, and have exact, visual control over how the tweening interpolates.
     
    Aaron-Meyers likes this.
  5. Aaron-Meyers

    Aaron-Meyers

    Joined:
    Dec 8, 2009
    Posts:
    305
    Yea that could work... I was just hoping there would be a magic bullet kind of solution... seems like something we’ll all need if the overall trend is a move towards Linear color space.
     
  6. Aaron-Meyers

    Aaron-Meyers

    Joined:
    Dec 8, 2009
    Posts:
    305
    upload_2019-5-6_9-7-25.png
    plotted a bunch of values taken by hand with the eye dropper tool. hopefully this will do the trick!
     
    JohngUK likes this.
  7. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,336
    No need to make your own curve, or otherwise hack something in. Unity provides the gamma (actually sRGB) to linear conversion functions.
    https://docs.unity3d.com/ScriptReference/Mathf.GammaToLinearSpace.html
    https://docs.unity3d.com/ScriptReference/Mathf.LinearToGammaSpace.html

    However, one bit of warning. Doing this inverse modification to the alpha only solves the very specific case of a fully white background and a fade to black using alpha. It'll still be wrong for all other color combinations (ie: grey background fading to black!). There simply is no way to do a fade from one color to another with alpha blend when using linear space color and have it match the apparently linear blend you get when using gamma space.

    The correct solution is to use a post process to do the fade.
    Code (csharp):
    1. // properties
    2. _ColorVec ("Color", Vector) = (0,0,0,0) // should _not_ be a Color, and do not set it using SetColor!
    3.  
    4. half4 frag (v2f_img i) : SV_Target
    5. {
    6.   // sample frame buffer image
    7.   half4 col = tex2D(_MainTex, i.uv);
    8.  
    9. #ifndef UNITY_COLORSPACE_GAMMA
    10.   // if in linear space, convert color to sRGB "gamma" space
    11.   col = LinearToGammaSpace (col);
    12. #endif
    13.  
    14.   // lerp between colors (always done in sRGB space)
    15.   col = lerp(col, _ColorVec, _ColorVec.a);
    16.  
    17. #ifndef UNITY_COLORSPACE_GAMMA
    18.   // convert back to linear space in needed
    19.   col = GammaToLinearSpace (col);
    20. #endif
    21.  
    22.   return col;
    23. }
    When setting the color value from script, for any color not white or black you'll want to make sure it's being passed to the shader in sRGB space. When you use SetColor on a Color property, Unity will automatically convert the color from the usual sRGB representation you're used to setting colors in to the color space of the project. Since you want the sRGB version of the color, make sure you use:
    mat.SetVector("_ColorVec", myColor);

    Unity will implicitly cast colors to a Vector4 for you, without any color space conversions.
     
    Last edited: May 6, 2019
    Aaron-Meyers likes this.
  8. Aaron-Meyers

    Aaron-Meyers

    Joined:
    Dec 8, 2009
    Posts:
    305
    I'm assuming Mathf.LinearToGammaSpace is roughly the same as what I was doing in the shader: pow(x, 1/2.2)
    I tried swapping it but it still didn't give good results. My hand-plotted curve is a big improvement though!

    The post-process method makes a lot of sense though, but I'm on mobile so is it possible that adding a blit could incur a bigger cost?
     
  9. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,336
    It is not. It's the real sRGB conversions, like shown here:
    http://chilliant.blogspot.com/2012/08/srgb-approximations-for-hlsl.html
    The approximations on that page are what the in-shader functions shown above use.

    The fact it didn't look significantly better confuses me a bit.
     
  10. Aaron-Meyers

    Aaron-Meyers

    Joined:
    Dec 8, 2009
    Posts:
    305
    Here is the class I'm using with my AnimationCurve:
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.UI;
    3.  
    4. [ExecuteInEditMode]
    5. public class LinearFadeToBlack : MonoBehaviour
    6. {
    7.     [SerializeField]
    8.     AnimationCurve _curve;
    9.  
    10.     [SerializeField][Range( 0, 1 )]
    11.     float _alpha;
    12.  
    13.     [SerializeField]
    14.     Image _image;
    15.  
    16.     void Update()
    17.     {
    18.         var col = _image.color;
    19.         col.a = _curve.Evaluate( _alpha );
    20.         _image.color = col;
    21.     }
    22. }
    23.  
    To try out the built-in conversion, I changed the line in the Update function to be

    Code (CSharp):
    1. col.a = Mathf.GammaToLinearSpace( _alpha );
    I suspect this might not be the right way to implement it...
     
  11. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,336
    I think that should be:
    col.a = Mathf.LinearToGammaSpace(_alpha);

    edit: Nope, try this:
    col.a = 1f - Mathf.GammaToLinearSpace(1f - _alpha);
     
    Last edited: May 6, 2019
  12. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,336
    So, here's an example of the above working.

    There are 4 bars below, each is using the i.uv.y to interpolate from one color to another.
    • The first bar is what you get with just lerping the two colors values with i.uv.y. This is what you're trying to avoid.
    • The second bar is converting the colors into gamma space, doing the lerp with i.uv.y, and converting back into linear space, which properly recreates the curve you see when using gamma color space rendering. i.e.: This is what you're trying to reproduce.
    • The third bar is a white background with an alpha blended black alpha blended in as a second pass. The alpha is using 1 - GammaToLinear(1 - i.uv.y) like what I suggested in the above post. Note this matches the overall appearance of the second bar, but there is some slight banding. This was done using linear color space w/o HDR enabled. With HDR enabled, it's a perfect match to the second bar.
    • The fourth bar is doing a lerp in the shader using the converted i.uv.y like in the third bar. It does not have any banding issues, and also perfectly matches the second bar.
    upload_2019-5-6_13-12-0.png


    So, we're all good, right? Well, as an example, lets flip the colors so we have effectively a black background and a white foreground we're blending to.
    upload_2019-5-6_13-24-35.png

    As you can see, the first and second bars look exactly like before, just upside down. But the third and fourth bars now look worse than even the first bar! This is why I said the hack of modifying the alpha curve only works in the specific case of a white background and black overlay. However, it might be good enough if you always have a black overlay you're fading to. Here's what it looks like with a middle grey background:

    upload_2019-5-6_13-27-40.png

    If you look, you'll notice the second and third bars do not match the second, and again, there's some additional banding in the third bar (the one using alpha blending). But, overall, it's passable. For my own shaders I use a bit of screen space noise to hide that kind of banding when it shows up. On the default white background of this site it's not super obvious, but it's much more obvious when on a dark background or otherwise filling your screen.
     
  13. Gkqt

    Gkqt

    Joined:
    Dec 1, 2018
    Posts:
    11
    This is an amazingly detailed response, bgolus. Great write-up!
     
  14. Aaron-Meyers

    Aaron-Meyers

    Joined:
    Dec 8, 2009
    Posts:
    305
    Yes thank you! Super comprehensive analysis of the options!