Draw.Line vs Draw.Polyline Linear Colorspace inconsistency

Avatar
  • updated
  • Fixed

Once again I'm comparing Draw.Line and Draw.Polyline in Immediate Mode using the Built in render pipeline.

If the Project's Color space is set to 'Gamma' then Draw.Line and Draw.Polyline appear identical when provided with the same inputs.

However when the Project's color space is set to 'Linear' then Draw.Line appears different from how it did in Gamma space - more desaturated, while Draw.Polyline appears unchanged:

It appears as if Draw.Lines does not properly convert the gamma-space color to linear space.

This is verified by manually converting the color before drawing:

// ...
Draw.Line(fromPoint, toPoint, thickness, FixGammaColor(fromColor), FixGammaColor(toColor));
// ...

private static Color FixGammaColor(Color color) 
    => QualitySettings.activeColorSpace == ColorSpace.Linear ? color.linear : color;

Which results in Draw.Lines behaving like Draw.Polyline does where switching the space does not make any difference.

Reporting a bug? please specify Unity version:
2019.4.16f1
Reporting a bug? please specify Shapes version:
3.0.0
Reporting a bug? please specify Render Pipeline:
Built-in render pipeline
Pinned replies
Avatar
Freya Holmér
  • Answer
  • Fixed

this has now been fixed in 3.2.0

Avatar
Freya Holmér
  • Under Review

How are you generating the colors? I don't get this effect on my end in 2018.4, built in RP when using Color.HSVToRGB( t, 1, 1 )

your endpoints seem a little suspiciously unaffected by this desaturation, but the middle goes all pale


Avatar
JohannesMP

Here is the full source code:

using UnityEngine;
using Shapes;

[ExecuteAlways]
public class GammaTest : ImmediateModeShapeDrawer
{
    public ShapesBlendMode blendMode = ShapesBlendMode.Transparent;
    [Range(0, 1)] public float alpha = 1;
    [Min(0)] public float thickness = 0.75f;
    [Space]
    public bool fixColorSpace= false;
    public bool useGradient;
    public Gradient gradient = new Gradient()
    {
        colorKeys = new[]
        {
            new GradientColorKey(new Color(1.000f, 0.000f, 0.000f), 0.000f),
            new GradientColorKey(new Color(1.000f, 0.631f, 0.000f), 0.182f),
            new GradientColorKey(new Color(1.000f, 0.922f, 0.192f), 0.356f),
            new GradientColorKey(new Color(0.408f, 0.812f, 0.357f), 0.544f),
            new GradientColorKey(new Color(0.208f, 0.463f, 0.831f), 0.771f),
            new GradientColorKey(new Color(0.569f, 0.000f, 0.980f), 1.000f),
        }
    };
    const int points = 32;

    public override void DrawShapes(Camera cam)
    {
        using (Draw.Command(cam))
        {
            Draw.ResetAllDrawStates();
            Draw.Matrix = transform.localToWorldMatrix;
            Draw.LineGeometry = LineGeometry.Volumetric3D;
            Draw.LineThicknessSpace = ThicknessSpace.Meters;
            Draw.LineThickness = 1;
            Draw.BlendMode = blendMode;
            
            using (PolylinePath polyPath = new PolylinePath())
            {
                for (int i = 0; i < points + 1; ++i)
                {
                    float param01 = i / (float) points;
                    
                    Color color = useGradient 
                        ? gradient.Evaluate(param01) 
                        : Color.HSVToRGB(param01, 0.95f, 1);
                    color.a = alpha;
                    
                    polyPath.AddPoint(GetPoint(param01), thickness, color);
                }
                
                Draw.Polyline(polyPath, closed: false);
                
                // Only apply color space fix for Draw.Line
                // Draw.Polyline seems fine without it. ¯\_(ツ)_/¯
                if (fixColorSpace)
                {
                    for (int i = 0; i < polyPath.Count; ++i)
                    {
                        polyPath.SetColor(i, FixGammaColor(polyPath[i].color));
                    }
                }
                
                // Draw Lines further down in local space
                Draw.Matrix *= Matrix4x4.Translate(Vector3.down * 2);
                DrawLines(polyPath);
            }
        }
    }

    private static Vector3 GetPoint(float param01)
    {
        float param = Mathf.Lerp(-Mathf.PI, Mathf.PI, param01);
        return new Vector3(param, Mathf.Sin(param * 2), 0);
    }
    
    private static void DrawLines(PolylinePath p)
    {
        for (int i = 0; i < p.Count-1; ++i)
        {
            Draw.Line(p[i].point, p[i+1].point, p[i].thickness, p[i].color, p[i+1].color);
        }
    }

   private static Color FixGammaColor(Color color) 
        => QualitySettings.activeColorSpace == ColorSpace.Linear ? color.linear : color;
}

Tested in a clean Unity 2019.4.16f1 project.

Originally I only sampled a gradient, but the code now also includes HSV generation as yours does, except using 0.95 for saturation. 
Since gamma and linear space match on a given channel at values 0.0 and 1.0, differences between a color in linear and a color in gamma space are far more visible when color is not 100% saturated, which is why I think you may not be noticing the issue.

As an aside: is calling Draw.ResetAllDrawStates(); necessary when using Draw.Command, or does it revert the changes that were made during the command when it is disposed?

Avatar
Freya Holmér
Quote from JohannesMP

Here is the full source code:

using UnityEngine;
using Shapes;

[ExecuteAlways]
public class GammaTest : ImmediateModeShapeDrawer
{
    public ShapesBlendMode blendMode = ShapesBlendMode.Transparent;
    [Range(0, 1)] public float alpha = 1;
    [Min(0)] public float thickness = 0.75f;
    [Space]
    public bool fixColorSpace= false;
    public bool useGradient;
    public Gradient gradient = new Gradient()
    {
        colorKeys = new[]
        {
            new GradientColorKey(new Color(1.000f, 0.000f, 0.000f), 0.000f),
            new GradientColorKey(new Color(1.000f, 0.631f, 0.000f), 0.182f),
            new GradientColorKey(new Color(1.000f, 0.922f, 0.192f), 0.356f),
            new GradientColorKey(new Color(0.408f, 0.812f, 0.357f), 0.544f),
            new GradientColorKey(new Color(0.208f, 0.463f, 0.831f), 0.771f),
            new GradientColorKey(new Color(0.569f, 0.000f, 0.980f), 1.000f),
        }
    };
    const int points = 32;

    public override void DrawShapes(Camera cam)
    {
        using (Draw.Command(cam))
        {
            Draw.ResetAllDrawStates();
            Draw.Matrix = transform.localToWorldMatrix;
            Draw.LineGeometry = LineGeometry.Volumetric3D;
            Draw.LineThicknessSpace = ThicknessSpace.Meters;
            Draw.LineThickness = 1;
            Draw.BlendMode = blendMode;
            
            using (PolylinePath polyPath = new PolylinePath())
            {
                for (int i = 0; i < points + 1; ++i)
                {
                    float param01 = i / (float) points;
                    
                    Color color = useGradient 
                        ? gradient.Evaluate(param01) 
                        : Color.HSVToRGB(param01, 0.95f, 1);
                    color.a = alpha;
                    
                    polyPath.AddPoint(GetPoint(param01), thickness, color);
                }
                
                Draw.Polyline(polyPath, closed: false);
                
                // Only apply color space fix for Draw.Line
                // Draw.Polyline seems fine without it. ¯\_(ツ)_/¯
                if (fixColorSpace)
                {
                    for (int i = 0; i < polyPath.Count; ++i)
                    {
                        polyPath.SetColor(i, FixGammaColor(polyPath[i].color));
                    }
                }
                
                // Draw Lines further down in local space
                Draw.Matrix *= Matrix4x4.Translate(Vector3.down * 2);
                DrawLines(polyPath);
            }
        }
    }

    private static Vector3 GetPoint(float param01)
    {
        float param = Mathf.Lerp(-Mathf.PI, Mathf.PI, param01);
        return new Vector3(param, Mathf.Sin(param * 2), 0);
    }
    
    private static void DrawLines(PolylinePath p)
    {
        for (int i = 0; i < p.Count-1; ++i)
        {
            Draw.Line(p[i].point, p[i+1].point, p[i].thickness, p[i].color, p[i+1].color);
        }
    }

   private static Color FixGammaColor(Color color) 
        => QualitySettings.activeColorSpace == ColorSpace.Linear ? color.linear : color;
}

Tested in a clean Unity 2019.4.16f1 project.

Originally I only sampled a gradient, but the code now also includes HSV generation as yours does, except using 0.95 for saturation. 
Since gamma and linear space match on a given channel at values 0.0 and 1.0, differences between a color in linear and a color in gamma space are far more visible when color is not 100% saturated, which is why I think you may not be noticing the issue.

As an aside: is calling Draw.ResetAllDrawStates(); necessary when using Draw.Command, or does it revert the changes that were made during the command when it is disposed?

Draw.ResetAllDrawStates() is necessary yeah, currently Draw.Command doesn't touch the static state at all (though I'm considering making it reset it at the start of every Draw.Command by default, with an option to turn that off, or make it push/pop state, though that would require more work)

Avatar
JohannesMP

Ignoring my aside for a moment, any response on the actual issue? ;)

Avatar
Freya Holmér

I've yet to look into it! It'll take some time because I'm already working on other things, plus for this one I don't know what the proper solution is, if I should convert on the CPU or the GPU, and where unity does convert automatically vs not. I usually don't go for the easiest solution because it usually comes around in the form of other bugs on other platforms with other render pipelines

Avatar
Freya Holmér
  • Answer
  • Fixed

this has now been fixed in 3.2.0