Search Unity

How to convert metallic workflow maps to specular via code?

Discussion in 'General Graphics' started by hungrybelome, Aug 23, 2020.

  1. hungrybelome

    hungrybelome

    Joined:
    Dec 31, 2014
    Posts:
    336
    I'm trying to automate converting metallic workflow maps to specular, and then use the converted maps in a specular surface shader. This is because I have a texture atlasing workflow, and half of my assets are metallic, and other half are specular. I want to convert the metallic assets to specular, so that I can atlas all of them in a single specular texture atlas.

    Also, I've seen benchmarks that show that the specular standard shader performs better than the metallic standard shader, since metallic data has to be converted to specular data, I believe. Even if the difference is just a few instructions, I'm trying to squeeze out as much performance as possible for my mobile VR game.

    I'm currently digging through the Standard Shader files trying to find the exact shader code that converts metallic data into specular data, as once I have that, transcribing to C# is easy.

    Does anyone know to convert metallic maps to specular via code? Even just written pseudo code would be extremely helpful.

    Thanks!

    *EDIT*
    Think I got it:

    https://github.com/TwoTailsGames/Un...3c437/CGIncludes/UnityStandardUtils.cginc#L46

    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. }
     
    Last edited: Aug 23, 2020
  2. neoshaman

    neoshaman

    Joined:
    Feb 11, 2011
    Posts:
    6,493
    Benchmark on unity? Can you link those benchmarks? what is the target hardware?

    Specular use more memory, and metallic just assume a global dielectric specular per material, it shouldn't be slower in any way. Metalics store albedo for dielectric and specular for metal in a single color texture (because most dielectric have a narrow range of specularity, and metalics have no albedo) then use the metalic texture map to switch between the two. That switch is possibly the extra line of code as it still has to compute specs and diffuse from albedo. But I'm surprise that it would slow that much since memory access is more costly generally and you have less memory access.
     
  3. hungrybelome

    hungrybelome

    Joined:
    Dec 31, 2014
    Posts:
    336
    I actually didn't make the benchmarks myself; there was a guy that had a really comprehensive benchmark set for all of the built-in shaders (for the built-in RP). Unforunately, I actually lost the link to his article (I think it was posted on the Unity subreddit), and I was actually planning on making a post here asking if anyone had it. His benchmarks included ALU, texture samples, a generic performance metric, etc. I don't remember what hardware he used, but I think it was mobile and not desktop.

    As for memory, I'm actually only using 1 channel for specular, since my assets only have grayscale specular data, so I'm not using any more memory than a metallic map. I'm packing all of my PBR texture data into only 2 textures (only storing normal map .xy). So the reduced ALU for a specular shader is worth it for me, since it comes with no trade-offs, in my specific case.
     
  4. neoshaman

    neoshaman

    Joined:
    Feb 11, 2011
    Posts:
    6,493
    SO your shader is already custom? what's the main target hardware (they have different profile)?


    JUST A RANDOM THOUGHT:
    Also something I haven't done but want to experiment with, make roughness a constant per shader if there is no variation it significantly decrease instruction. I also plan to see if I can have visual variation by attenuating it linearly over small range so it doesn't break light too much (possibly over vertex data instead of texture, technically would lose energy from conservation, which is the lesser concern over gaining energy) by tweaking visual by eyes. A lot of surface don't have the grim, so just constant roughness, no texture make sense, breaking the mesh into more vertices would cost less, but beware of drawcalls if done thoughtlessly.
     
  5. hungrybelome

    hungrybelome

    Joined:
    Dec 31, 2014
    Posts:
    336
    Yes, I'm using custom surface shaders and targeting mobile VR (Oculus Quest).

    My surface shaders are very simple and something like this:

    Code (CSharp):
    1. Shader "Production/PBR/Specular (1 Channel)"
    2. {
    3.     Properties
    4.     {
    5.         [NoScaleOffset] _AlbedoSpecular1("Albedo (RGB) Specular (A)", 2D) = "white" {}
    6.         [NoScaleOffset] _NormalOcclusionSmoothness1("Normal (GA) Occlusion (B) Smoothness (R)", 2D) = "white" {}
    7.     }
    8.  
    9.     SubShader
    10.     {
    11.         Tags{ "RenderType" = "Opaque"  "Queue" = "Geometry+0" }
    12.         Cull Back
    13.         CGPROGRAM
    14.         #include "UnityStandardUtils.cginc"
    15.         #include "../../ProductionShadersUtils.cginc"
    16.      
    17.         // Force BRDF3:
    18.         // https://github.com/TwoTailsGames/Unity-Built-in-Shaders/blob/master/CGIncludes/UnityPBSLighting.cginc
    19.         #define UNITY_PBS_USE_BRDF3 1
    20.         #pragma target 3.0
    21.      
    22.         #pragma surface surf StandardSpecular addshadow fullforwardshadows vertex:vertexDataFunc
    23.      
    24.         struct Input
    25.         {
    26.             half2 texcoord_0;
    27.         };
    28.  
    29.         uniform sampler2D _AlbedoSpecular1;
    30.         uniform sampler2D _NormalOcclusionSmoothness1;
    31.  
    32.         void vertexDataFunc(inout appdata_full v, out Input o)
    33.         {
    34.             UNITY_INITIALIZE_OUTPUT(Input, o);
    35.          
    36.             o.texcoord_0.xy = v.texcoord.xy;
    37.         }
    38.  
    39.         void surf(Input i, inout SurfaceOutputStandardSpecular o)
    40.         {
    41.             half4 albedoSpecular1 = tex2D(_AlbedoSpecular1, i.texcoord_0);
    42.             half4 normalOcclusionSmoothness1 = tex2D(_NormalOcclusionSmoothness1, i.texcoord_0);
    43.          
    44.             o.Albedo = albedoSpecular1.rgb;
    45.             o.Occlusion = normalOcclusionSmoothness1.b;
    46.             o.Normal = decodePackedNormal(normalOcclusionSmoothness1.ga);
    47.             o.Specular = float3(albedoSpecular1.a, albedoSpecular1.a, albedoSpecular1.a);
    48.             o.Smoothness = normalOcclusionSmoothness1.r;
    49.          
    50.             o.Alpha = 1;
    51.         }
    52.  
    53.         ENDCG
    54.     }
    55.     Fallback "Diffuse"
    56. }
    57.  


    For your second point, I personally do use a metallic constant (usually 0.02, a value I saw recommended somewhere) for dielectric shaders, when I want to pack height data instead of metallic data. I think a lot of terrain shaders probably do this also.
     
  6. neoshaman

    neoshaman

    Joined:
    Feb 11, 2011
    Posts:
    6,493
    What? dielectric are the opposite of metallic (in shader methodology), how they have a constant? o_O
    Must be a typo right? I mean I guess you mean specs, I heard the standard is 0.04, .02 seems low to me as it's the advise limit. Anyway if it work for you.

    I was talking about roughness though (or smoothness depend on the implementation) roughness is what we use, usually, to vary the surface with artistic grit (as specs and albedo must be measured) and it's quite costly due to the whole energy conservation and other stuff needed to create the curve, by making roughness a constant, you basically collapse (part of) the complex curve equation (beckmann or ggx) to a single value (no more exp and pow) and you also bypass the need for a LUT texture AND the roughness texture. I haven't seen anyone do that, which makes sense since you lose flexibility and need to multiply shader and material (duplicate for each roughness variant since it's no longer a variable). That's an extreme optimization.

    Now you are using surface, so all of that is hidden to you, so the code is small, but that's the complexity that is hidden (it's no so simple), I thought you were going more custom (ie custom lighting equation).
     
  7. hungrybelome

    hungrybelome

    Joined:
    Dec 31, 2014
    Posts:
    336
    Oh sorry, I glossed over your post and misread it. I was talking about a constant for the metallic input of a surface shader for a dielectric material, like o.metallic = 0.02. I thought we were talking in the context of unity's built-in lighting functions since that was what my first post was about.
     
  8. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,343
    If you're using Unity's built in shaders, Specular and Metallic workflows are identical in terms of memory, because both use a DXT5 with roughness in the A and either specular color in the RGB or the metallic in R. Though it is true you can more cheaply pack metallic since you only need a single channel instead of three if you're doing custom packing / atlasing, but if you have a mix, you kind of have to use specular as there's no perfect way to convert from a specular setup to a metallic setup, especially in cases where the specular color is significantly different from the albedo color.

    Those benchmarks were wrong. Or at least not accurate enough to be useful.

    I do actually know the benchmarks you looked at. The author used a mid range laptop GPU from 7 years ago (was 3 years old when they did the benchmarks). They showed a <2% difference between the Standard and Standard Specular shaders. That's well within the margin of error for the kind of benchmark he did. I could sometimes see deltas of 15% between runs on a desktop GPU with a locked clock speed using proper profiling tools. And they were just looking at the frame times in Unity ... which don't even reflect actual GPU time. Basically, their benchmarking methodology won't give you even remotely accurate timings.

    Similarly, the page showed the "Skybox 6 Sided" shader as being slightly more expensive than the Standard Specular shader. That's one of the cheapest shaders in Unity. I rather wonder if the order in the graph more accurately shows the order in which he tested the shaders, and it's showing the GPU getting thermally throttled over time, rather than their actual cost.

    The built in metallic and specular shaders are effectively identical in cost. The specular workflow shader is a single instruction less, but when the Standard shaders are potentially >200 instructions a single additional instruction is going to probably have less of an effect on performance than from the fluctuations in the your room's ambient temp. If you're doing custom packing and can remove a texture, you'd potentially be saving way more from that than anything else. Especially on Quest.

    Basically the cost of calculating energy conservation between the specular and albedo when using the specular workflow ends up being almost exactly the same cost as calculating the specular color from the metallic. So it's a wash. The only case where this isn't true is on very old OpenGLES 2.0 devices where Unity takes a shortcut when calculating energy conservation for the specular workflow ... to make it 3 instructions cheaper instead of just 1. Again, basically nothing.



    Lastly, the Quest's Adreno 540 is a totally different beast than the (now ancient) desktop GPU being used in those benchmarks. In my experience, the Standard shading was too expensive, period. At least for what we were trying to do. I swapped to Lambert diffuse only shaders on everything for the Quest, with specular highlights confined to only the most important places. You can certainly use the Standard shading model if you want. You may have better luck with the content you have, but it is very expensive for mobile. That said, the most expensive thing you can do on the Quest is any kind of transparent overdraw. That can be from a single dynamic point light, or a basic unlit transparent texture, these will absolutely murder your frame rate if they cover a large portion of your screen. The shader complexity almost didn't matter here. You're significantly bottle necked by the Quest's memory bandwidth and ROPs (the part of GPU hardware that combines the output of a shader with the frame buffer). Lots of layers of opaque geometry don't matter a ton, as the tiled deferred rendering of the Adreno GPUs does a very good job of removing any kind of opaque overdraw (this is unrelated to forward vs. deferred rendering in Unity, and works for both rendering paths). But three layers of transparent effects that take up 50% of the screen and it'll be nearly impossible to hit 72 hz.
     
    neoshaman, hungrybelome and AcidArrow like this.
  9. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,343
    Back to the original question, yes, the function you found is correct. The value for
    unity_ColorSpaceDielectricSpec
    in that function used to come from Unity at runtime, but it's now hard coded in the shader includes, and switches between two values based on if you're using linear or gamma color space for your project. Really it's the "same value", just pre-calculated for gamma an linear space.
    https://github.com/TwoTailsGames/Un...bed144a74fd9d0f4/CGIncludes/UnityCG.cginc#L34

    It is totally valid to convert your textures from metallic to specular in this way to improve atlasing & baking.
     
    neoshaman and hungrybelome like this.