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

User defined extensions #732

Merged
merged 1 commit into from
Dec 14, 2019
Merged

Conversation

jjcasmar
Copy link
Contributor

This patch allows to create user defined extensions. This works by loading dynamically python files in the extensions directory. This way, a user can define his own extensions without the need to fork the exporter

@jjcasmar jjcasmar force-pushed the UExtensions branch 2 times, most recently from 13e425b to b6dea71 Compare October 14, 2019 23:20
@jjcasmar
Copy link
Contributor Author

@julienduroure

@jjcasmar jjcasmar force-pushed the UExtensions branch 2 times, most recently from e52e33d to 08215da Compare October 16, 2019 17:35
@julienduroure julienduroure added enhancement New feature or request exporter This involves or affects the export process labels Oct 21, 2019
@julienduroure
Copy link
Collaborator

@donmccurdy @emackey @scurest This PR is quite interesting, and will need lots of testing / code review. Any help is welcome :)

extension = Extension("KHR_texture_transform", texture_transform)
return {"KHR_texture_transform": extension}

Copy link
Contributor

Choose a reason for hiding this comment

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

Weird whitespace.

return None
extension_wrapper = GLTF2ExtensionSkins(blender_object)
extensions = export_user_extensions(extension_wrapper, export_settings)
return extensions


def __gather_extras(blender_object, export_settings):
Copy link
Contributor

Choose a reason for hiding this comment

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

Why does __gather_extras get changed only here?

if extension:
extensions[user_extension_name] = extension

return extensions
Copy link
Contributor

Choose a reason for hiding this comment

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

Should be return extensions or None (not that I think it matters since empty dicts get erased at some point anyway, but for consistency...)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is an error. gether_extras should be unchanged.

self.blender_action = blender_action,
self.blender_object = blender_object

class GLTF2ExtensionCameras:
Copy link
Contributor

Choose a reason for hiding this comment

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

Grammatical number seems to be inconsistent here. Some are singular (GLTF2ExtensionMesh, GLTF2ExtensionNode, etc.) and some are plural (GLTF2ExtensionCameras, GLTF2ExtensionMaterials, etc.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I using the same notation as the gltf2_blender_gather_.py files. There are some that are cameras and others that are mesh.

Copy link
Contributor

Choose a reason for hiding this comment

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

Node is the only difference then (GLTF2ExtensionNode but gltf2_blender_gather_nodes.py). I still think they should still have a consistent number though.

self.bake_channel = bake_channel

class GLTF2ExtensionAnimations:
def __init__(self, blender_action, blender_object):
Copy link
Contributor

Choose a reason for hiding this comment

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

How does this work? What action/object is it getting? Because generally an animation will correspond to a collection of Blender actions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It works exactly the same way as in other exported element. The __gather_extensions in gltf2_blender_gather_animations receives a blender_action and a blender_object. I simply redirect those to the extension exporters.

Copy link
Contributor

@scurest scurest Nov 4, 2019

Choose a reason for hiding this comment

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

Yeah, I see that you just use whatever the __gather_extensions function takes, but is that okay? Most of these are just returning None right now anyway so there's no guarantee their arguments are actually useful or sensible enough to become a public API.

For example, I just tested this and this gets called once for each (action,blender_object) pair in the animation, but only the value returned by the first call for each animation actually makes it into the extensions dict; the rest get dropped on the floor. I don't think that's how it should work.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well, if the __gather_extensions should receive another thing, the it's obvious it will be incorrect, but I'm not sure if its on the scope of this PR to fix what the __gather_extensions function should receive.
If you think its on the scope, we can try to fix it, but probably we will have to review all the other __gather_extensions methods.

@scurest
Copy link
Contributor

scurest commented Oct 29, 2019

The big thing I guess is backwards compatibility. This creates a public API the exporter would presumably be beholden to maintain, which seems like a big commitment.

@jjcasmar
Copy link
Contributor Author

jjcasmar commented Nov 4, 2019

What can we do about the backward compatibility? Maybe use the Blender version number or the exporter version as some kind of extension API version? Not sure about this, but if there are no commitments at all about API stability, is hard (imposible) to expose this to the public.

@scurest
Copy link
Contributor

scurest commented Nov 4, 2019

Well, what's the use case for this? It's already pretty limited because you can only add data to the extensions object without changing the rest of the glTF.

I'm wondering if we could design something more like how extras already works. Ziflin showed something in #730 where they used a regular Blender add-on to "extend" the exporter with new custom properties. Could we do something like that?

@jjcasmar
Copy link
Contributor Author

jjcasmar commented Nov 5, 2019

I dont understand what you mean when you say you can only add data to the extensions object. I have been able to reimplement KHR_materials_unlit and KHR_punctual_lights with this mechanism. You can add new extensions to the root extensions object and refer to them from other places in the glTF file, as proved by the KHR_punctual_lights reimplementation. Ill push it to this PR so you can see how it works

@scurest
Copy link
Contributor

scurest commented Nov 5, 2019

For example, you wouldn't be able to set the fallback behavior on a KHR_materials_unlit material with this would you? And doesn't knowledge about KHR_lights_punctual need to be integrated into the exporter anyway so it can emit the correction node? There'd be no way to do that from a user extension.

That's what I mean about only being able to add data to extensions dicts.

@jjcasmar
Copy link
Contributor Author

jjcasmar commented Nov 5, 2019

The default fallback material is already exported. The current code exports the PBR information if the shader graph has a Principled BSDF node, no matter what the extension does. Then the extension add the extension data if the shader graph has a Background node.

For the lights you are right, its not possible to add the correction node from the extension exporter, as we can only add data to the extensions dictionaries.
How do you suggest to improve this? Im not sure if an extension should have impact outside of the extension dictionaries anyway. That means that an importer that doesn't support the extension may be importing something that is not useful and could potentially break the scene

A bit offopic, I'm looking at the current code for the exporter and wouldn't be better to add a global correction node at the beggining of the hierarchy instead of per camera/light?

@scurest
Copy link
Contributor

scurest commented Nov 5, 2019

The current code exports the PBR information if the shader graph has a Principled BSDF node, no matter what the extension does

But the PBR information depends on whether it should be unlit. For example, unlit materials should have a metallicFactor of 0 for fallback purposes, but the default metallicFactor is 1.

Right now if I import a KHR_materials_unlit material (in 2.8 where an Emission node is used for this because there is no Background node in 2.8) and export it, I get a material like this

{
    "emissiveFactor": [1, 1, 1],
    "emissiveTexture": { "index": xxx },
    "pbrMetallicRoughness": {}
}

A correct KHR_materials_unlit implementation should give something like this

{
    "pbrMetallicRoughness": {
        "baseColorTexture": { "index": xxx },
        "roughnessFactor": 0.9,
        "metallicFactor": 0.0
    },
    "extensions": { "KHR_materials_unlit": {} }
}

That requires more that just adding extensions data.


(The correction nodes are there to rotate the camera/light when you have a situation where glTF says it should point along one axis but in Blender it points along another axis. You can't do that with one rotation at the root.)

@jjcasmar
Copy link
Contributor Author

jjcasmar commented Nov 6, 2019

I understand what you mean. Maybe we can do a two step mechanism? From, what I have seen, the gltf2 object is created using something like this:

    node = gltf2_io.Node(
        camera=__gather_camera(blender_object, export_settings),
        children=__gather_children(blender_object, blender_scene, export_settings),
        extensions=__gather_extensions(blender_object, export_settings),
        extras=__gather_extras(blender_object, export_settings),
        matrix=__gather_matrix(blender_object, export_settings),
        mesh=__gather_mesh(blender_object, export_settings),
        name=__gather_name(blender_object, export_settings),
        rotation=None,
        scale=None,
        skin=__gather_skin(blender_object, export_settings),
        translation=None,
        weights=__gather_weights(blender_object, export_settings)
    )

Maybe we can keep the first step as it is right now, returning an Extension on the __gather_extensions and then do a second step in which we pass the glTF2 object to the extension plugin to modify something if its necessary.

What do you think?

@scurest
Copy link
Contributor

scurest commented Nov 6, 2019

The first step seems unnecessary. If you're going to be given the glTF object to manipulate as you please you can just add anything you want to the extensions property then. At that point this would no longer be limited to glTF extensions since you could also do manipulations that don't touch the extensions prop. You'd basically have a bunch of hooks to modify the output of the __gather_XXX functions just before they finish?

It's difficult to see how well this would work in practice. I think any user extension would have to track changes to the exporter pretty heavily so that in practice they would end up highly coupled anyway. The two examples we just had show that it's not so easy to write a correct user extension.

Do you have an example of a user extension you actually want to use this for?

@jjcasmar
Copy link
Contributor Author

jjcasmar commented Nov 7, 2019

Probably the user extension developers would need to keep track on exporter changes but I think there are some benefits:
First of all, if the developer needs to patch the exporter to add extensions, he is going to need to keep track on the changes anyway.
Second, patching the exporter and deploying a new exporter forces the users to disable/uninstall the official exporter and install the patched exporter. Not very user friendly in my opinion.

You are right that passing around the glTF object to the extension exporter opens the door to modifying the whole node. Patching the exporter doesn't fix this, a developer can add whatever he wants. Also, if to properly implement an extension you need to modify other properties than the extensions property, then its clear the developer needs access to the whole glTF object.

As use cases, at KDAB we have developed an internal extension to add tags to nodes. Basically its a new glTF array layers and a layer property on the node. I'm algo trying to draft something for supporting animating any floating point value property using fcurves. Of course its possible to implement these extensions, or any extension, just patching the exporter, but I think this has some inconvenients as already mentioned at the beginning.

@scurest
Copy link
Contributor

scurest commented Nov 7, 2019

Second, patching the exporter and deploying a new exporter forces the users to disable/uninstall the official exporter and install the patched exporter. Not very user friendly in my opinion.

Yes, and it would also let the user mix and match the user-extensions they want (if there are n user-extensions, you'd need 2^n - 1 patched exporters to represent every combination of installed user-extensions). Like I said above, I think it would be even better if we could find a way to use regular Blender addons for this. Then Blender would handle managing their lifecycles and their installation would be even more user friendly.

You are right that passing around the glTF object to the extension exporter opens the door to modifying the whole node

I wasn't saying this was bad, just that user-extensions aren't about glTF extensions anymore; they become general plugins for modifying bits of the glTF.

add tags to nodes

Tagging sounds like something you could basically use custom properties for though.

@jjcasmar
Copy link
Contributor Author

jjcasmar commented Nov 7, 2019

How do you propose to let Blender managed this? If we add the user extensions as a new Blender addon, we wont be able to call them as hooks from the exporter, no?
Something that was in my roadmap is to let the user specify the folder where the user extensions python files are, but they will be still managed by the exporter itself, as they are with the current approach.

Tagging sounds like something you could basically use custom properties for though.

They are not just tags. The layers have some properties and can be shared between different nodes. They represent the layers in Qt3D.

@scurest
Copy link
Contributor

scurest commented Nov 7, 2019

I dunno, does something like this work?

# At beginning of each export
import sys
user_exts = {}
for addon_name in bpy.context.preferences.addons.keys():
    try:
        # Every user-extension would have a top-level class called Gltf2UserExt
        user_exts[addon_name] = sys.modules[addon_name].Gltf2UserExt()
    except Exception:
        pass

# Example hook
for user_ext in user_exts.values():
    try:
        user_ext.gather_node_hook(gltf_node, blender_object, export_settings)
    except Exception:
        pass

The user-extensions would need to make sure they wait until the Gltf2UserExt class is called into to import io_scene_gltf2 I think.

@jjcasmar
Copy link
Contributor Author

jjcasmar commented Nov 7, 2019

Right now, if the user wants to enable/disable one extension, he just need to check a checkbox in the Extensions section of the exporter panel. Probably easy to do anyway with the approach you suggested.
You approach will allow to simplify code also, as I dont need all the code to load for the py files.

If I understand correctly, at each place where a gltf2 object is created, I will need to call the user hooks, similar to what Im doing now, right?

Thanks for the suggestion

@jjcasmar
Copy link
Contributor Author

I have tried what you suggested, but there is an issue. The gltf2 addon is imported before the user extensions addons, so in sys.modules[addon_name], the module for the user_extension doesn't exist yet. Do you know how can I force the extensions to be imported before the gltf2 addon?

@jjcasmar
Copy link
Contributor Author

jjcasmar commented Dec 3, 2019

I have added the call to the hooks for the rest of gltf objects that have a __gather_extension function

@julienduroure
Copy link
Collaborator

So ... Being quite far from this development. Can you please confirm that this dev is stable and can be merged?

@emackey
Copy link
Member

emackey commented Dec 10, 2019

@jjcasmar Thanks so much for this contribution. I've wanted this feature for some time, but I haven't really been able to sit and review this thoroughly until this morning.

My initial impression is this appears very stable and works well. I do have a small concern on the UI. When there are no custom extensions enabled, Blender still puts an extra Extensions dropdown on the glTF export file dialog. Most users will see this as empty, so expanding the dropdown doesn't do anything but spin a triangle. Is there any way to remove that? (I'm testing with Blender 2.81a. No need for compatibility with 2.80).

I also tried going into user preferences and toggling the glTF addon itself off, and after a moment, back on. This ensured that the glTF addon "woke up" with a custom extension already previously active. When I did this, the custom settings appeared below (outside of) the Extensions dropdown on the UI. This also resulted in an expandable dropdown being empty.

I wonder if it would be better to just get rid of the Extensions dropdown on the UI, and always have custom extension options simply appear underneath, separately. It seems like that could save some concern over which addon is activated first. Just a suggestion.

Great work on this.

@jjcasmar
Copy link
Contributor Author

@emackey thanks for your comments.

I will review the UI stuff and submit any needed changes.

@jjcasmar jjcasmar force-pushed the UExtensions branch 2 times, most recently from 59dc4b2 to aad9eb6 Compare December 11, 2019 23:35
@jjcasmar
Copy link
Contributor Author

jjcasmar commented Dec 11, 2019

@emackey The latest version should fix the issues you have found. Now I only show the extension panel if there is some extension to be shown and I unregister the extension panels when the exporter is disabled. That way, when the exporter is enabled again, there are no panels preregistered.

CI is failing though. Apparently, is not being able to download blender?

@@ -940,6 +986,9 @@ def register():
def unregister():
for c in classes:
bpy.utils.unregister_class(c)
for f in extension_panel_unregister_functors:
f()
Copy link
Member

Choose a reason for hiding this comment

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

Should the extension_panel_unregister_functors list be cleared after this? Or Blender always makes a fresh copy of the whole addon when re-registering?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can clear the list, but anyway, since the function runs in a try/catch scope, there is no problem is there is a dangling function trying to remove an already removed panel.

@emackey
Copy link
Member

emackey commented Dec 12, 2019

I tried to manually re-run the CI but I think it only ran 1 of the 4 containers. Can you push a new commit to trigger the whole thing? I think it should be OK now...

Copy link
Member

@emackey emackey left a comment

Choose a reason for hiding this comment

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

This feels solid, and I like the UI fixes. Let's merge this once the tests are green.

@donmccurdy Did you want us to wait for your review?

@julienduroure
Copy link
Collaborator

These is in #829 a discussion about required / used extensions: how this PR manage it? (Sorry, don't have the time to test for now)

@emackey
Copy link
Member

emackey commented Dec 12, 2019

These is in #829 a discussion about required / used extensions: how this PR manage it? (Sorry, don't have the time to test for now)

This PR doesn't need to do anything about #829. That one is a bugfix for a flag that's not being respected.

)
float_property: bpy.props.FloatProperty(
name='ExampleExtension_enabled',
description='This is an example of a BoolProperty used by a UserExtension.',
Copy link
Member

Choose a reason for hiding this comment

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

FloatProperty

@julienduroure
Copy link
Collaborator

My question was (sorry, was not really clear):
With this new way of extension creation at export, is there a way to define these new extensions as required or used?
Can't see any info about that in misc/ExampleExtension.py file

@emackey
Copy link
Member

emackey commented Dec 12, 2019

@julienduroure Yes, good question, I just tested this and it works.

in misc/ExampleExtension.py around line 101, set the required parameter:

            gltf2_object.extensions[bl_info['name']] = self.Extension(
                name= bl_info['name'], 
                extension={"float": self.properties.float_property},
                required=False
            )

That might be a good thing to add to this example, now that the bug is fixed in master.

The default is True (required), and the flag is only respected if #829 or master is merged into this branch.

@julienduroure
Copy link
Collaborator

Thanks for your explanation.
Agree that this should be added to the example. This will not replace the documentation (that still need to be written ?), but it can lead to understand/find this parameter easier.

@jjcasmar
Copy link
Contributor Author

I will add that flag in the ExampleExtension.

@julienduroure where/how should I write the documentation?

@julienduroure
Copy link
Collaborator

@jjcasmar The documentation is here: docs/blender_docs/io_scene_gltf2.rst
This file is committed in official blender documentation here : https://docs.blender.org/manual/en/latest/

@donmccurdy
Copy link
Contributor

It would be a good to see an example or two of real vendor extensions implemented on this API before we consider the API stable — I expect that if we merge it now, there could be breaking changes before we get to that point. If that's OK and matches everyone's expectations, I'm fine with merging the current PR any time.

The documentation is here: docs/blender_docs/io_scene_gltf2.rst
This file is committed in official blender documentation here : https://docs.blender.org/manual/en/latest/ ...

Perhaps just put a brief section mentioning the existence of this API in the public Blender documentation, and link to a more detailed markdown API doc for the feature in this repository? Both can be a separate PR. I don't think we'd want to spend a lot of words on Python APIs in the otherwise UI-focused official addon documentation.

@jjcasmar
Copy link
Contributor Author

I can provide a KDAB extension if necesary. I will do this last changes ASAP

The extension plugin should receive a dictionary with the properties
@julienduroure
Copy link
Collaborator

Hello,
I am going to merge it, because bcon2 will start on Monday, so this is the last days to merge new big feature into Blender repository. (see #801 for more info about that).
Polishing can be done on another PR

@julienduroure julienduroure merged commit 6030b24 into KhronosGroup:master Dec 14, 2019
@emackey
Copy link
Member

emackey commented Dec 14, 2019

Thanks again @jjcasmar, this is an important one.

@jjcasmar
Copy link
Contributor Author

Thanks @emackey . This patch is very useful for KDAB.

@jjcasmar jjcasmar deleted the UExtensions branch December 15, 2019 18:51
@scurest scurest mentioned this pull request Feb 10, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request exporter This involves or affects the export process
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants