KhronosGroup / glTF

glTF – Runtime 3D Asset Delivery
Other
7.14k stars 1.14k forks source link

Spec/Gloss different in Substance Painter, no max statement #1443

Closed emackey closed 1 month ago

emackey commented 6 years ago

The spec/gloss formulas are detailed in this section and were also discussed here. The diffuse light formula is given as:

Cdiff = diffuse.rgb * (1 - max(specular.r, specular.g, specular.b))

Due to the use of max, the contents of one color channel can affect the diffuse light for all.

Doing some testing in Allegorithmic's Substance Painter 2018, with their stock spec/gloss PBR workflow, I'm seeing different results. As a test I made a model with the diffuse channel set to red [1.0, 0.0, 0.0] and the specular to lime green [0.0, 1.0, 0.0]. The resulting model in Substance Painter's preview window has a bright yellow hue to it. Splitting up a screenshot of that rendering into its component color channels reveals that the red channel looks like a diffuse rendering, and the green channel looks like a specular rendering, which seems quite reasonable.

Back in glTF land though, the same model shows up as shiny specular green, without any hint of red or yellow. This is because that max statement has flattened out the red diffuse color on account of the green specular color. Is this really intentional?

Looking at my install of SP, I found a file here: Program Files/Allegorithmic/Substance Painter/resources/shader-doc/pbr-spec-gloss.html. It documents the spec/gloss shader snippet as having essentially the same formula, without the max statement:

Cdiff = diffuse.rgb * (1 - specular)

This change does seem to make the interaction between diffuse and specular capable of a much wider range of appearances. In metal/rough terms this would be akin to allowing the artist to assign different levels of metalness to different color channels.

Would be great to hear feedback from any artists who have experience with this workflow.

I know we don't yet have a standard process for upgrading extension versions, but I'm starting to believe this one is in strong need of an upgrade. I think the max statement should be removed.

bghgary commented 6 years ago

If SP is doing this differently, it would unfortunate. But we can't just use SP as the truth. Unity, for example, does exactly what the formulas say.

Unity built-in shaders can be downloaded here: https://unity3d.com/get-unity/download/archive

// Unity built-in shader source. Copyright (c) 2016 Unity Technologies. MIT license (see license.txt)

CGIncludes\UnityStandardConfig.cginc

// Energy conservation for Specular workflow is Monochrome. For instance: Red metal will make diffuse Black not Cyan
#ifndef UNITY_CONSERVE_ENERGY
#define UNITY_CONSERVE_ENERGY 1
#endif
#ifndef UNITY_CONSERVE_ENERGY_MONOCHROME
#define UNITY_CONSERVE_ENERGY_MONOCHROME 1
#endif

CGIncludes\UnityStandardUtils.cginc

// Helper functions, maybe move into UnityCG.cginc

half SpecularStrength(half3 specular)
{
    #if (SHADER_TARGET < 30)
        // SM2.0: instruction count limitation
        // SM2.0: simplified SpecularStrength
        return specular.r; // Red channel - because most metals are either monocrhome or with redish/yellowish tint
    #else
        return max (max (specular.r, specular.g), specular.b);
    #endif
}

// Diffuse/Spec Energy conservation
inline half3 EnergyConservationBetweenDiffuseAndSpecular (half3 albedo, half3 specColor, out half oneMinusReflectivity)
{
    oneMinusReflectivity = 1 - SpecularStrength(specColor);
    #if !UNITY_CONSERVE_ENERGY
        return albedo;
    #elif UNITY_CONSERVE_ENERGY_MONOCHROME
        return albedo * oneMinusReflectivity;
    #else
        return albedo * (half3(1,1,1) - specColor);
    #endif
}

CGIncludes\UnityStandardCore.cginc

inline FragmentCommonData SpecularSetup (float4 i_tex)
{
    half4 specGloss = SpecularGloss(i_tex.xy);
    half3 specColor = specGloss.rgb;
    half smoothness = specGloss.a;

    half oneMinusReflectivity;
    half3 diffColor = EnergyConservationBetweenDiffuseAndSpecular (Albedo(i_tex), specColor, /*out*/ oneMinusReflectivity);

    FragmentCommonData o = (FragmentCommonData)0;
    o.diffColor = diffColor;
    o.specColor = specColor;
    o.oneMinusReflectivity = oneMinusReflectivity;
    o.smoothness = smoothness;
    return o;
}
emackey commented 6 years ago

I see. Looks like it's configurable with shader defines, I wonder if the SP one is configurable too.

It's a shame there isn't better standardization of spec/gloss PBR.

PatrickRyanMS commented 6 years ago

@emackey, I looked into what you were seeing with the viewport renderer in Substance Painter, and it is due to a lack of shadows in the viewport renderer. There is an option in display settings to enable shadows on the viewport renderer which brings the render in line with the iRay version which we have used for some of our ground truth testing. The reason that you can disable shadows in the viewport render is that most artists won't want to be texturing an asset that has shadows on it as it can make you think your colors are darker than they are and cause your textures to be off as you are compensating for the shadows.

In this case, with your settings for spec-gloss, it appears that the renderer is trying to render an overloaded color over 1 so the green is going yellow because it's reading from an HDR environment and applying specular color. I obviously don't see this issue when working with metal-rough and spend most of my time there.

Here are a couple of comparison screens of the viewport renderer (with the shadows enabled in Display Settings) and iRay:

painterviewport

painter_iray

In this second set, I rotated the hottest light to reflect on the face and you can see it still goes a little more yellow in the viewport render, but that can be reduced a bit by increasing the quality under Shader Parameters as seen in the screen shot.

painterviewport2

painter_iray2

Lastly, I reduced the glossiness on the shader ball so we can see what more spread on the specular renders like. Again, the viewport has more yellow, but more of note is the diffuse color showing through more in the viewport than in the iRay render. As an artist, I will always look to the ray tracer as my ground truth

painterviewport3

painter_iray3

PatrickRyanMS commented 6 years ago

@emackey here is another thing you can do if you would like to have Painter's Viewport look like glTF, and that's use this version of the spec-gloss shader in Painter:

pbr-spec-gloss-glTF.zip

Place it in the folder at: C:\Program Files\Allegorithmic\Substance Painter\resources\shelf\allegorithmic\shaders

And then choose that shader in Shader Settings and after turning off shadows in Display Settings you will get:

painterviewport4

And in Babylon:

babylonjs

emackey commented 5 years ago

I took a long break from this issue, but I do have some updated notes here. I've researched which platforms (of the ones I have access to) use which formulas for combining PBR diffuse with PBR specular. There are four different formulas so far:

  1. Cdiff = diffuse.rgb * (1 - max(specular.r, specular.g, specular.b))

This is the formula specified by glTF. It is used in Windows 3D Viewer, BabylonJS, and CesiumJS. It is unusual (in my experience) that any (non-special-effect) formula for 3D rendering allows different color channels to mingle and influence each other, as the max statement is doing here. It's being done in the name of overall light energy conservation. But it is not per-frequency energy conservation, and after experimentation I definitely feel like there are situations where this becomes noticeable.

The formula is also used by Unity's shader code posted by @bghgary above, but only when UNITY_CONSERVE_ENERGY and UNITY_CONSERVE_ENERGY_MONOCHROME are both defined.

  1. Cdiff = diffuse.rgb * (1 - specular.r)

This one also comes from Unity (above), but it appears to be doing this only as a fallback behavior for Shader Model 2.0 (obsolete). So I think this one can be disregarded.

  1. Cdiff = diffuse.rgb * (1 - specular)

Substance Painter uses this formula in its "PBR - Specular Glossiness (allegorithmic)" profile. It is energy-conserving per-channel, so does not allow different color channels to influence one another.

The above Unity shader will use this formula if UNITY_CONSERVE_ENERGY is defined and UNITY_CONSERVE_ENERGY_MONOCHROME is undefined.

  1. Cdiff = diffuse.rgb

This simplistic approach doesn't attempt any energy conservation, or makes the assumption that conservation has been baked into the diffuse map from the start. Blender's Eevee "Specular BSDF" (realtime-only) node takes this approach, as does ThreeJS, and Sketchfab.

The Unity shader will do this one too, when UNITY_CONSERVE_ENERGY is undefined.


I can't really argue that glTF picked the "wrong" one, as each one has its own merits. But I spent a while experimenting with these formulas today, and the one we selected is the least flexible, in the sense that it can barely do anything that Metal/Rough can't do. By comparison, formula 3 above can do some weirdly interesting things while still claiming per-color-channel energy conservation. But it's formula 4, the non-energy-conserving one, that feels most intuitive to an artist messing around with color selectors, as the artist's selected colors are not tampered with by formula 4.

My larger concern is that various platforms are all over the map as to which one is used. An engine can easily claim compatibility with the SpecGloss extension, without picking the glTF standard formula. I've been working on a test model that will visually highlight which formula is in use, if we think that could help the situation.

bghgary commented 5 years ago

FWIW, one can argue that having different diffuse/specular colors is no longer physically based since AFAIK it doesn't occur in real life and thus the asset should be authored with that in mind. The only scenario I've heard where it is appropriate to use different diffuse/specular color is to fake multiple layers being blended due to translucency.

emackey commented 1 month ago

This relates to an extension that has been deprecated / archived.