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

Metallic map linear vs gamma

Discussion in 'Shaders' started by SunnySunshine, Aug 14, 2016.

  1. SunnySunshine

    SunnySunshine

    Joined:
    May 18, 2009
    Posts:
    974
    Does unity expect metallic texture to be in linear or gamma space? What are the consequences of disabling sRGB for the texture?

    I wrote a surface shader than has two different smoothness maps that I lerp between. These two maps reside in the same texture, in the alpha and blue channels. Because of the sRGB gamma correction, these two maps are interpreted differently though. Disabling sRGB fixes that issue, but that also affects the metallic map (in the red channel). So I'm wondering if disabling sRGB is OK, or if I should leave it on and simply do a GammaToLinear on the blue channel (other smoothness).

    It seems like unity doesn't do any gamma correction or anything at all for the metalness:
    Code (CSharp):
    1. inline half3 DiffuseAndSpecularFromMetallic (half3 albedo, half metallic, out half3 specColor, out half oneMinusReflectivity)
    2. {
    3.     specColor = lerp (unity_ColorSpaceDielectricSpec.rgb, albedo, metallic);
    4.     oneMinusReflectivity = OneMinusReflectivityFromMetallic(metallic);
    5.     return albedo * oneMinusReflectivity;
    6. }
    I thought that was kind of weird, since that means the gamma is present by default in the metalness (since sRGB is enabled unless explicitly disabled in the advanced settings of the texture).
     
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    Unity expects metallic to be an sRGB texture.

    The slider on the standard shader has a [Gamma] property drawer prefix so the value you set there will match the correlating sRGB texture value (metallic slider value * 255). Annoyingly the default standard surface shader doesn't have [Gamma] on the slider which causes all sorts of weirdness for people switching back and forth.

    It might seem odd that it's sRGB and not linear, and it's fine if you use a linear texture instead, it's just that the color space doesn't really matter too much. The "correct" way of using it is to have the texture be either 0 or 1 and nothing in between, though no one uses it that way. Since once you to get to something between the two you're getting into arbitrary "whatever looks good" land rather than anything physically based. Choosing linear or sRGB does change the bilinear filtering behavior on hard edges, but there's no "correct" way to do those either as neither linear or sRGB are accurate, though sRGB tends to hide some of the problems better.
     
    SunnySunshine likes this.
  3. SunnySunshine

    SunnySunshine

    Joined:
    May 18, 2009
    Posts:
    974
    Excellent reply as always, @bgolus. I'm keeping the Metallic in sRGB and doing the space conversion instead. Also adding [Gamma] to metallic slider so it's consitent with Standard shader behavior.

    I just noticed that I needed to add LinearToGammaSpace for the blue smoothness channel though and not GammaToLinearSpace. This confuses me. If the texture is in sRGB, surely I would need to convert it to linear, right? But it's only if I do LinearToGammaSpace that alpha and blue will match up.
     
  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    The alpha component of textures are always linear regardless of the settings on the texture, sRGB only applies to the RGB channels. The smoothness channel is expected to be linear btw, though again this kind of comes down to personal preference for what you want. I think most engines use linear, but a handful use sRGB or pow(smoothness, 2).
     
  5. SunnySunshine

    SunnySunshine

    Joined:
    May 18, 2009
    Posts:
    974
    Yes, I know alpha is in linear. That's why I want to convert the blue channel to linear too, for the lerp. So why does GammaToLinearSpace not work (for the blue channel), but LinearToGammaSpace does? That seems inversed of what I want to do.
     
  6. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    What color space are you rendering with? If you're using gamma all textures are linear all of the time. If you're using linear only then does sRGB kick in. Pretty sure switching back and forth between gamma and linear on half-metallic objects using a metallic / smoothness texture would change drastically in the past, don't know if they fixed that or not.

    Otherwise they still might not match perfectly due to GammaToLinearSpace using an approximate (but surprisingly good) version of sRGB, where LinearToGammaSpace also uses approximation that is almost perfect. But it should be a very small difference.

    There's a helper function, for checking if you're in gamma or linear in UnityCG.cginc
    bool gammaSpace = IsGammaSpace();
     
  7. SunnySunshine

    SunnySunshine

    Joined:
    May 18, 2009
    Posts:
    974
    I'm using deferred. This is what I do:

    Code (CSharp):
    1. fixed4 m = tex2D(_MetallicGlossMap, IN.uv_MainTex);
    2. o.Metallic = m.r * _Metallic;
    3. // Since _MetallicGlossMap.rgb is in sRGB, I thought I'd need to convert blue channel to linear, but it's the other way around?
    4. // If using the same data in alpha and blue channels, then they will be interpreted differently unless LinearToGammaSpace is used.
    5. o.Smoothness = lerp(m.a, LinearToGammaSpace(m.b), _Lerp) * _Glossiness;
     
  8. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    Deferred is the rendering path, of which Unity has options for Forward and Deferred. Gamma is the color space, of which there is Gamma (really sRGB) and Linear.

    However I think I missed something in what you're trying to do. Are you packing custom metallic gloss textures with metal in red channel, and gloss / smoothness in the blue channel? What's in the green channel (which is the highest quality of the RGB components)? What's in the alpha channel and why are you lerping between it and the blue channel?
     
  9. SunnySunshine

    SunnySunshine

    Joined:
    May 18, 2009
    Posts:
    974
    Right, sorry, I was high when writing that. I'm using linear color space with deferred renderer.

    What, why would green have higher quality then the rest? Aren't they all 8 bit? I'm pretty sure unity uses red channel for the metallic too.

    Both alpha and blue are smoothness, just two different versions that I need to be able to transition in between. Hence the lerp.
     
  10. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    Unity does indeed get Metallic from the red channel, so the other two channels are "free", but the alpha component is significantly higher quality, and the green is the second best, at least on PC with compressed textures.

    DXT1 textures compress 8:1 over an uncompressed texture, and part of how they do this is they actually only store two colors for each block of 4x4 pixels, and those colors are stored with only 16 bits using RGB 5:6:5. They can represent 4 colors per block with two more interpolated colors "between" the two stored colors (1/3rd of one and 2/3rd of the other). When the textures are read the interpolated colors are full 24 bit color / 8 bit per channel.

    DXT5 textures are the same DXT1 RGB with an additional alpha stored similarly to the RGB, but the alpha uses a full 8 bits for the alpha value for the two values, and 6 additional values between those two.

    A side effect of this is RGB color channels in DXT1/5 aren't really separate so you can see a "shadow" of other colors when trying to do multiple individual channels.

    It also means a couple of us were confused that metallic uses the red and not the green by default, but it's not really a big deal.


    Back to the original topic, if you're having to do LinearToGamma on the B channel to get it to match my first expectation is the way you're authoring the blue channel is doing something wrong. That or somehow Unity's naming scheme is backwards.
     
  11. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    Argh... Unity is correct. I think I've been getting this wrong the whole time too.

    Here's how I've always thought about it: an sRGB texture automatically converts the data that's stored in a linear data format and converts it to display gamma corrected sRGB values. Also, the very direct "I have a linear texture and want to make it look like a sRGB texture". In that way LinearToGammaSpace seems like the function you should use to convert a linear channel to match a sRGB channel.

    Apparently that's the wrong way to think about it, and likely the same way you're thinking about it.

    It's a stored sRGB space value converted to display a linear space value. So that linear texture channel is actually a sRGB / Gamma value that needs to be converted to linear, hence GammaToLinearSpace is correct.


    If you think about it in terms of "object" and "world" space it might make more sense. A sRGB value of 0.5 is displayed as a linear value of ~0.2, and to convert it you're transforming from gamma to linear space. Where that value 0.5 came from (a linear texture value of 127/255 or sRGB of 187/255) isn't important, just that you're treating that 0.5 as a gamma value.
     
  12. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    dxt5_perchannelerror_lineargradient.png

    Made a little graphic to show off the relative error of DXT5 channels against uncompressed, red is the real value is greater, green is the real value is less than that in the compressed channel. This is a linear texture being compared against an uncompressed texture. The gradient is a perfect linear gradient going from 255 to 0 in all 4 channels with steps of 1. The alpha channel is almost perfect; curiously the tiny amount of error at the top and bottom of the alpha are products of Unity's DXT5 compressor, not the format itself. The "crunched" compressor, while worse for the RGB channels, ends up producing a perfect alpha.

    If the gradient is stored in a single RGB color channel at a time with the other two 0 (or 255) the error is reduced and becomes more regular. Below you can see the green channel gets very, very close.
    dxt5_singlechannelgradients.png

    Here are the two images used for comparison. (Somewhat hard to look at the all 4 channel version since the alpha fades it out on web pages).
    gradient_linear_allchannels.png gradient_linear_rgbseparate.png
     
    Last edited: Aug 17, 2016
  13. SunnySunshine

    SunnySunshine

    Joined:
    May 18, 2009
    Posts:
    974
    No, that's what I thought - that it's stored as sRGB. That's why I want to use GammaToLinearSpace.

    So why doesn't that work? :)
    Using GammaToLinearSpace makes it worse.

    Here, I prepared a sample project.
    https://dl.dropboxusercontent.com/u/608462/test.unitypackage

    Here, the blue and alpha channels are the same (just stored differently due to sRGB). So the result we want to see here is that _Lerp shouldn't affect anything.

    As you can see, only using LinearToGammaSpace makes it work (in linear color space for the project).
     
  14. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    Okay. Ignore everything I wrote before. I think I confused both you and me more than I needed to. I was converting the alpha channel to match the sRGB blue channel, which is not what you're doing.

    Lets say you make texture that has a blue channel of 128/255, a value about ~0.5. That is it's Gamma space value.
    That texture imported into Unity as an (default) sRGB texture.
    If your project is setup to use Linear color space when that texture is sampled by the shader the value it will see is ~0.2, that is the Linear space value.
    So now you have your Linear blue value of ~0.2 and you want it to be the value you would have gotten from a linear texture or alpha channel, so you need to convert your Linear space ~0.2 back to the Gamma space ~0.5.
    If that texture was set to Bypass sRGB, or you had that same 128/255 in the alpha channel, the shader would get the ~0.5 value straight and unmodified.
    Hence using LinearToGammaSpace() when converting an sRGB texture's blue channel linear space output into the same as what a linear texture would produce.
     
    SunnySunshine likes this.
  15. SunnySunshine

    SunnySunshine

    Joined:
    May 18, 2009
    Posts:
    974
    Thanks for clearing that up!