Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CgltfImporter: import unrecognized material extensions #117

Closed
wants to merge 35 commits into from

Conversation

pezcode
Copy link
Contributor

@pezcode pezcode commented Dec 14, 2021

Similarly to #116, this adds the ability to import unrecognized material data as custom attributes. Each extension object is imported as a custom layer (with a # prefix because uppercase layer names are reserved, and all glTF extensions start with uppercase), with JSON properties imported as custom attributes.

Attributes can be:

  • bool, Float, Int, UnsignedInt, String
  • arrays with 1-4 Float, Int, UnsignedInt, imported as Vector2/3/4[u][i]
  • textureInfo objects ending in "Texture"

Any other type of attribute is ignored. This is tested fairly extensively, but if you see any corner cases missing, let me know.

Extensions that are parsed by cgltf but don't map to known MaterialLayer/MaterialAttributes get the same treatment, with attributes imported with the original glTF extension spec names.

Since I didn't need it for testing, there's currently no config option to turn off the unrecognized material parsing. Would it make sense to add something like ignoreUnrecognizedMaterialData in AssimpImporter?

Todos

A few remaining issues, but no major changes:

  • Duplicate attribute names are not yet handled, this leads to an assert. It's handled for extension names, but this one is not quite as clean because I'll need to parse the JSON, remember token ranges, sort, remove duplicates, and then import.
  • Tests for the custom material data from extensions handled by cgltf

@pezcode
Copy link
Contributor Author

pezcode commented Dec 14, 2021

Forgot to mention: there are 3 minor AssimpImporter changes in there that I noticed while working on the same code here. Hope that's OK, but I can separate them out too.

The failing linux-sanitizers CI is me not cleaning up after cgltf's parsing function that mallocs random things, already fixed locally.

@codecov
Copy link

codecov bot commented Dec 14, 2021

Codecov Report

Merging #117 (b85ea40) into master (9e30f51) will increase coverage by 0.14%.
The diff coverage is 100.00%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #117      +/-   ##
==========================================
+ Coverage   95.56%   95.71%   +0.14%     
==========================================
  Files         115      115              
  Lines       10830    11158     +328     
==========================================
+ Hits        10350    10680     +330     
+ Misses        480      478       -2     
Impacted Files Coverage Δ
src/MagnumPlugins/CgltfImporter/CgltfImporter.h 100.00% <ø> (ø)
.../MagnumPlugins/TinyGltfImporter/TinyGltfImporter.h 100.00% <ø> (ø)
...rc/MagnumPlugins/AssimpImporter/AssimpImporter.cpp 90.58% <100.00%> (+0.20%) ⬆️
src/MagnumPlugins/CgltfImporter/CgltfImporter.cpp 98.36% <100.00%> (+0.36%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 9e30f51...b85ea40. Read the comment docs.

- this shouldn't be an error, it doesn't fail the import
- with really large names, the printed size can be small, making this message very confusing
This handles invalid primitives (those are not thoroughly checked by jsmn) the same way as cgltf. Numbers are parsed up until the first invalid characters, or 0 if there are no valid characters.
In preparation for importing custom material texture attributes from unknown extensions. Using raw names is identical to the built-in enum values, no changes to tests needed.
Each extension gets its own layer, extension object properties are imported as custom attributes. To keep our sanity, the following types are not imported, we just print a warning and ignore the attribute:
- arrays with size 0 or > 4
- non-primitive arrays (strings, objects, nested arrays)
- bool arrays, not supported by MaterialAttributeType and awkward to parse when mixed with numbers
- objects that don't end in "Texture" and resolve to a textureInfo (according to the glTF schema) type
Treat them like unknown extensions and import their attributes with the original glTF names. Some of these may turn into a standard MaterialAttribute or MaterialLayer in the future, but for the time being this makes them accessible without changes to magnum. Tests will follow in another commit.
This causes funny sorting issues inside MaterialData, and shouldn't happen in practice anyway
…tributes

Also no need for those extra attributes, all types are checked in MGNM_material_type_zoo
cgltf allocates memory for extensions inside cgltf_parse_json_texture_view(). We don't use that, so we can free it right away.
We treat these as "raw" material layers, so just import whatever is there without divining too much about their meaning
There are no wrappers for these material types/layers, so we don't have to test all these interactions that the clearcoat extension is going through. Just checking the defaults and the texture import.
Previously this only accepted objects with a single "source" attribute. Now it skips all unknown attributes and handles duplicate "source" attributes. This makes it behave similarly to TinyGltfImporter.
Same as vertex attributes and material extensions, duplicate attributes overwrite previous ones. This requires parsing before-hand, then sorting, then importing attributes. It adds some overhead and is incredibly unlikely to matter in practice, but not handling it would lead to asserts in MaterialData, and we don't want that.
We don't need all these if/else blocks anymore, we can continue; out of the attribute loop
@mosra mosra added this to the 2021.0a milestone Dec 18, 2021
Copy link
Owner

@mosra mosra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

The comments regarding number parsing are there because I was doing an investigation for custom scene fields and realized that any attempt to detect numeric type could lead to unnecessary variability and friction in user code. In my case of scene fields it goes even further, where I have things like the following, i.e. can't even assume the tags is a vector because it can have a different number of items in every case and depending on the scene it could be presented as a float, vector2 or whatever:

      "extras": {
        "id": "79098824816e98077c45a760ff59ef946884d09d",
        "name": "Flower Pot, very bigly",
        "objectType": "furniture",
        "tags": [666, 1212, 3, 48]
      },

Fortunately / hopefully in case of materials when there's a numeric array we can assume it's indeed meant to be a fixed-size vector and not an arbitrarily long array. (TODO for me: add an ability to store arbitrarily-sized arrays in MaterialData.)

Related -- materials can have extra fields as well (and I have files with those, containing various string identifiers usually), but I don't see the PR handling those? Those would be present in the base layer and i'd limit the support to just bools, numbers, strings and 2/3/4-component number arrays, i.e., not assuming the extra fields could contain texture objects.

there are 3 minor AssimpImporter changes in there that I noticed while working on the same code here. Hope that's OK

Yep, this is fine. The PR is just a messenger, what matters in the end is the actual patch set, and as long as those are in separate commits it's no problem that they're in the middle of something else.

Would it make sense to add something like ignoreUnrecognizedMaterialData in AssimpImporter?

I don't see a reason right now -- the extra material attributes are not just random noise in this case. It might make sense once there's glTF export, but even then the goal is preserving as much as possible. Cleaning up custom attributes might be better as a standalone utility in SceneTools, for example.

src/MagnumPlugins/CgltfImporter/CgltfImporter.cpp Outdated Show resolved Hide resolved
src/MagnumPlugins/CgltfImporter/CgltfImporter.cpp Outdated Show resolved Hide resolved
src/MagnumPlugins/CgltfImporter/Test/material-raw.gltf Outdated Show resolved Hide resolved
src/MagnumPlugins/CgltfImporter/Test/CgltfImporterTest.cpp Outdated Show resolved Hide resolved
This is the preferred name, in case it gets added as an approved glTF vendor prefix in the future
Because consistently detecting if a number is int, unsigned int or float is impossible. Integer attributes may use exponent notation and decimal points followed by a 0, leaving us at the mercy of the glTF exporter used for the asset. Note that integer attributes in texture objects (texture index, coordinate set) are unaffected because we know the expected type.
For better diagnostics, this prints the string if it's not empty
Ideally, this would become DebugTools::CompareMaterial in the future. Only doing this for custom materials because base material tests aren't as verbose thanks to convenenience accessors.
@pezcode
Copy link
Contributor Author

pezcode commented Dec 22, 2021

materials can have extra fields as well

I kind of forgot about that 🙊 Importing those turns out to be not quite as straightforward, because they can be any token type, including objects. Blender, for example, uses it to export custom attributes which then looks like this:

"extras" : {
    "prop1" : 2.6,
    "PROP2" : 6.9
}

This can be imported as another layer "Extras". But if, instead, it's just a single string or primitive, do we import that as "Extras" in the base layer? Or as "Extras" in the "Extras" layer for consistency? Opinions?

There's also the issue of how the concept of opaque metadata can be extended to other resource types but that might be a discussion for another day.

@mosra
Copy link
Owner

mosra commented Jan 9, 2022

Sorry for the review delay, it definitely wasn't as time-consuming as I anticipated 🙈

because they can be any token type, including objects

Err, not sure I understand what you mean? Yes, the assumption is that "extras" are always an object -- sorry if that wasn't clear. The glTF spec suggest them to be an object, and a (censored) example of a real-world use was in my comment above:

      "extras": {
        "id": "79098824816e98077c45a760ff59ef946884d09d",
        "name": "Flower Pot, very bigly",
        "objectType": "furniture",
        "tags": [666, 1212, 3, 48]
      },

If the "extras" are not an object, print a warning; ignore nested objects with a warning as well.

This can be imported as another layer "Extras".

Nope, in your case it'd be the base layer getting new float attribute prop1 and pROP2, in case of my snippet, if it wasn't actually in the material and not elsewhere, it'd be a new string attributes id, name and objectType and a new Vector4 attribute tags. In other words, basically the same treatment as you do with extension objects, except that it gets put directly into the base layer and no nested objects (not even anything resembling a texture) get handled.

Sorry for the confusion, hope it's clearer now :)

@pezcode
Copy link
Contributor Author

pezcode commented Jan 9, 2022

Ah, unfortunate misunderstanding on my part. I didn't even realize your snippet had "extras" in it 🥲

The existing tests only tested one extensions that had ALL the warnings, but not that parsing still works
The extras type can be any valid JSON token, but we ignore anything that's not an object. Type support and conversion is the same as unrecognized material extensions, except we ignore ALL nested objects, including textureInfo. Attributes are written directly into the material base layer.
@mosra
Copy link
Owner

mosra commented Jan 30, 2022

Excellent work as always, thank you 👍 Merged as e5eb856...ad289da.

I squashed some commits to have code and corresponding docs/tests together as that makes bisects easier, but that was basically the only "cleanup" needed. Additionally in f9e10f4 I made a behavioral change where empty extension objects are preserved as well -- this is to support use cases similar to KHR_materials_unlit, where just the presence of the extension alone affects the material behavior.

Tested on a random extension-heavy dataset I have here, and it's just ✨ perfect ✨ :

$ magnum-sceneconverter -I CgltfImporter --info-materials 102817200.glb
…
Material 357: Shader1
  Type: Trade::MaterialType::Phong|Trade::MaterialType::PbrMetallicRoughness|Trade::MaterialType::PbrClearCoat
  BaseColor @ Trade::MaterialAttributeType::Vector4: Vector(0.534067, 0.534067, 0.534067, 1)
  DiffuseColor @ Trade::MaterialAttributeType::Vector4: Vector(0.534067, 0.534067, 0.534067, 1)
  Metalness @ Trade::MaterialAttributeType::Float: 0.973018
  Roughness @ Trade::MaterialAttributeType::Float: 0.0507658
  Layer 1: ClearCoat
    LayerFactor @ Trade::MaterialAttributeType::Float: 0
    Roughness @ Trade::MaterialAttributeType::Float: 0
  Layer 2: #KHR_materials_ior
    ior @ Trade::MaterialAttributeType::Float: 1.2
  Layer 3: #KHR_materials_specular
    specularColorFactor @ Trade::MaterialAttributeType::Vector3: Vector(1, 1, 1)
    specularFactor @ Trade::MaterialAttributeType::Float: 0.633728
  Layer 4: #KHR_materials_transmission
    transmissionFactor @ Trade::MaterialAttributeType::Float: 0
  Layer 5: #KHR_materials_volume
    attenuationColor @ Trade::MaterialAttributeType::Vector3: Vector(1, 1, 1)
    attenuationDistance @ Trade::MaterialAttributeType::Float: 1
    thicknessFactor @ Trade::MaterialAttributeType::Float: 0
  Layer 6: #KHR_materials_anisotropy
    anisotropy @ Trade::MaterialAttributeType::Float: 0
    anisotropyDirection @ Trade::MaterialAttributeType::Float: -0
…
Material 419: Shader1
  Type: Trade::MaterialType::Phong|Trade::MaterialType::PbrMetallicRoughness|Trade::MaterialType::PbrClearCoat
  BaseColorTexture @ Trade::MaterialAttributeType::UnsignedInt: 395
  DiffuseTexture @ Trade::MaterialAttributeType::UnsignedInt: 395
  NoneRoughnessMetallicTexture @ Trade::MaterialAttributeType::UnsignedInt: 394
  Layer 1: ClearCoat
    LayerFactor @ Trade::MaterialAttributeType::Float: 0
    Roughness @ Trade::MaterialAttributeType::Float: 0
  Layer 2: #KHR_materials_ior
    ior @ Trade::MaterialAttributeType::Float: 1.5
  Layer 3: #KHR_materials_specular
    specularColorFactor @ Trade::MaterialAttributeType::Vector3: Vector(1, 1, 1)
    specularColorTexture @ Trade::MaterialAttributeType::UnsignedInt: 396
    specularFactor @ Trade::MaterialAttributeType::Float: 1
    specularTexture @ Trade::MaterialAttributeType::UnsignedInt: 396
    specularTextureSwizzle @ Trade::MaterialAttributeType::TextureSwizzle: Trade::MaterialTextureSwizzle::A
  Layer 4: #KHR_materials_transmission
    transmissionFactor @ Trade::MaterialAttributeType::Float: 0
  Layer 5: #KHR_materials_volume
    attenuationColor @ Trade::MaterialAttributeType::Vector3: Vector(1, 1, 1)
    attenuationDistance @ Trade::MaterialAttributeType::Float: 1
    thicknessFactor @ Trade::MaterialAttributeType::Float: 0
  Layer 6: #KHR_materials_anisotropy
    anisotropy @ Trade::MaterialAttributeType::Float: 0
    anisotropyDirection @ Trade::MaterialAttributeType::Float: 0

@mosra mosra closed this Jan 30, 2022
@mosra mosra mentioned this pull request Dec 31, 2022
28 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging this pull request may close these issues.

2 participants