jkuhlmann / cgltf

:diamond_shape_with_a_dot_inside: Single-file glTF 2.0 loader and writer written in C99
MIT License
1.42k stars 135 forks source link

Possible to have individual flags for metallic and roughness? #225

Closed chaoticbob closed 1 year ago

chaoticbob commented 1 year ago

Hi, I posted issue#1956 over on the glTF-Blender-IO repo with regards to handling of metallic and roughness properties. TL;DR; when a material doesn't have metallic, the exporter is writing 1 into the exported texture as opposed to the value set by the node.

This got me wondering, currently in cgltf there is a combined flag that checks to see if a material has_metallic_roughness. I know that the texture source is one texture for both metallic and roughness, but was wondering if there are cases where one is present but not the other? For instance if a wood material only has roughess (along with baseColor and normal) but not metallic.

My inclination (which may be wong in terms of the spec) is that if the material only has roughness, there should be a texture for roughness and metallicFactor of 0. Which is what I'm seeing in the export:

            "pbrMetallicRoughness":{
                "baseColorTexture":{
                    "index":1
                },
                "metallicFactor":0,
                "metallicRoughnessTexture":{
                    "index":2
                }
            }

I don't see a flag in the cgltf API to check for metallic or roughness individually. Am I missing or misundersatnding something here?

zeux commented 1 year ago

The way you would check the condition you're asking about is material->has_pbr_metallic_roughness && material->pbr_metallic_roughness.metallic_factor > 0 and material->has_pbr_metallic_roughness && material->pbr_metallic_roughness.roughness_factor > 0, respectively.

has_pbr_metallic_roughness specifically refers to the presence of pbrMetallicRoughness node in glTF material file; cgltf mirrors the structure of glTF files pretty much one to one, so since glTF files don't have metallic and roughness separated, other than through factors, that's how cgltf represents this as well.

chaoticbob commented 1 year ago

Hi Arseny, I apologize in advanced that I'm responding in the early hours of the morning. Hopefully my response is still comprehensible.

Here's some clarification to what I was trying to ask originally: What I'm trying to do is figure out from cgltf if I should use the values from metallicFactor/roughnessFactor or from the texture. Based on my understanding, I think there are some ambiguous cases using the checks that have been suggested.

I beg your patience, this is a long example. Below I have 4 materials. Each material name has a suffix for metallic or roughness that indicates what/where the value for each respective field should come from: _F - means the value for metallic and/or roughness should come from the *Factor variable(s). _T - means the value for metallic and/or roughness should come from the texture. *Factor variables are ignored. _Z - means the value for metallic and/or roughness is zero but should also come from the *Factor variable(s).

I created a scene in Blender with the 4 materials and exported:

    "materials":[
        // We know there is a metallicRoughness texture.
        // cgltf will have 0 for metallicFactor and 1.0 for roughnessFactor.
        // The intent of this material is to have metallic be 0 and 
        //   use the roughness from the texture.
        // From the cgltf perspective, it's ambiguous if roughness
        //   should come from the the factor or the texture.
        // We know from looking at the GLTF file that there isn't roughnessFactor
        //   so this must mean that roughness must come from the texture. 
        {
            "doubleSided":true,
            "name":"metallic_Z_and_roughness_T",
            "pbrMetallicRoughness":{
                "baseColorFactor":[
                    0.800000011920929,
                    0.800000011920929,
                    0.800000011920929,
                    1
                ],
                "metallicFactor":0,
                "metallicRoughnessTexture":{
                    "index":0
                }
            }
        },
        // We know 100% that both metallic and roughness are factors.
        // cgltf will have 0.25 for metallicFactor and 0.5 for roughnessFactor.
        // The intent of this material is to use both metallic and roughness from the factors.
        // The absence of a metallicRoughness texture means that this
        //   material will not have texture reads.
        {
            "doubleSided":true,
            "name":"mettalic_F_and_roughness_F",
            "pbrMetallicRoughness":{
                "baseColorFactor":[
                    0.800000011920929,
                    0.800000011920929,
                    0.800000011920929,
                    1
                ],
                "metallicFactor":0.25,
                "roughnessFactor":0.5
            }
        },
        // We know there is a metallicRoughness texture.
        // cgltf will have 1.0 for both metallicFactor and roughnessFactor.
        // The intent of this material is to use both metallic and roughness from the texture.
        // From the cgltf perspective, it's ambiguous if metallica nd roughness
        //   should come from the factor or the texture. 
        // We know by looking at the GLTF file that isn't metallicFactor 
        //   or roughnessFactor, so this must mean that both metallic and roughness
        //   must come from the texture.
        {
            "doubleSided":true,
            "name":"metallic_T_and_roughness_T",
            "pbrMetallicRoughness":{
                "baseColorFactor":[
                    0.800000011920929,
                    0.800000011920929,
                    0.800000011920929,
                    1
                ],
                "metallicRoughnessTexture":{
                    "index":1
                }
            }
        },
        // We know there is a metallicRoughness texture.
        // cgltf will have 1.0 for metallicFactor and 0 for roughness.
        // The intent of this material is to have metallic come from the texture
        //   and roughness be 0.
        // From the cgltf perspective, it's ambiguous if metallic
        //   should come from the the factor or the texture.
        // We know from looking at the GLTF file that there isn't metallicFactor
        //   so this must mean that metallic must come from the texture.        {
            "doubleSided":true,
            "name":"metallic_T_and_roughness_Z",
            "pbrMetallicRoughness":{
                "baseColorFactor":[
                    0.800000011920929,
                    0.800000011920929,
                    0.800000011920929,
                    1
                ],
                "metallicRoughnessTexture":{
                    "index":2
                },
                "roughnessFactor":0
            }
        }
    ],

From what I understand, a check like material->has_pbr_metallic_roughness && material->pbr_metallic_roughness.metallic_factor > 0 even with an additional check for material->pbr_metallic_roughness.metallic_roughness_texture.texture != NULL can't indicate with certainty whether the value for metallic should come from the factor or thet exture. Also want to note that both metallic_factor = 0 and 'roughness_factor = 0are both legit use cases for a material. By looking at the GLTF JSON we can know with certainty that ifmetallicFactorand/orroughnessFactoris not present then the values for metallic/roughness must from the texture - assuming the JSON is well formed andmetallicRoughnessTexture` exists.

Based on what I've said above (fully admit my understanding might be flawed), it seems like if there existed something like:

cgltf_bool has_metallic_factor; // starts out as false and becomes true IFF metallicFactor is in JSON
cgltf_bool has_roughness_factor; // starts out as false and becomes true IFF roughnessFactor is in JSON

then could be certainty on which source the values metallic/roughness could come from.

zeux commented 1 year ago

Let's ignore cgltf for now, as I said it tries to give a 1-1 semantic mirror of the glTF file.

The way glTF metallic roughness node works is that for every pixel, the metalness value is computed as metallicFactor * textureSample(metallicRoughnessTexture, uv).b, and roughness value is computed as roughnessFactor * textureSample(metallicRoughnessTexture, uv).g. When texture is absent, the texture sample is assumed to return (1, 1, 1, 1). When either factor is absent, it is assumed to be 1. See https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#metallic-roughness-material.

A factor is not ignored when a texture is present. A factor that is absent is indistinguishable from a factor that is specified and equal to 1.

As such, you can determine the definite absence of metalness (if the goal is to skip some computations) by checking if metalnessFactor is exactly 0 - even if texture is present, that will nullify whatever values the texture has.

If the factor is not 0 or absent (which means it's 1), and the texture is present, you can't definitively say whether any pixels of the mesh must be rendered as a metallic material - this requires analyzing the blue channel of the texture and seeing if all pixels are 0, which glTF file doesn't provide anywhere.

Hopefully that clarifies this? Getting back to cgltf, since absence of metallicFactor is equivalent to metallicFactor being specified as 1, cgltf doesn't store the presence of the factor in source document anywhere because from the spec perspective it doesn't matter if it exists.

chaoticbob commented 1 year ago

That's a helpful explanation. Thanks!

I had forgotten that the factors acts as multiplier in the cases I stated above. In thinking about it, the ambiguous case(s) will need to be addressed with workflow requirements.