-
Notifications
You must be signed in to change notification settings - Fork 323
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
Conversation
13e425b
to
b6dea71
Compare
e52e33d
to
08215da
Compare
@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} | ||
|
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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...)
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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. |
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. |
Well, what's the use case for this? It's already pretty limited because you can only add data to the I'm wondering if we could design something more like how |
I dont understand what you mean when you say you can only add data to the |
For example, you wouldn't be able to set the fallback behavior on a That's what I mean about only being able to add data to |
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. 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? |
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
A correct KHR_materials_unlit implementation should give something like this
That requires more that just adding (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.) |
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:
Maybe we can keep the first step as it is right now, returning an What do you think? |
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 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? |
Probably the user extension developers would need to keep track on exporter changes but I think there are some benefits: 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 As use cases, at KDAB we have developed an internal extension to add tags to nodes. Basically its a new glTF array |
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.
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.
Tagging sounds like something you could basically use custom properties for though. |
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?
They are not just tags. The layers have some properties and can be shared between different nodes. They represent the layers in Qt3D. |
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 |
Right now, if the user wants to enable/disable one extension, he just need to check a checkbox in the 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 |
I have tried what you suggested, but there is an issue. The gltf2 addon is imported before the user extensions addons, so in |
I have added the call to the hooks for the rest of gltf objects that have a |
So ... Being quite far from this development. Can you please confirm that this dev is stable and can be merged? |
@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 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 I wonder if it would be better to just get rid of the Great work on this. |
@emackey thanks for your comments. I will review the UI stuff and submit any needed changes. |
59dc4b2
to
aad9eb6
Compare
@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() |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
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... |
There was a problem hiding this 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?
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) |
misc/ExampleExtension.py
Outdated
) | ||
float_property: bpy.props.FloatProperty( | ||
name='ExampleExtension_enabled', | ||
description='This is an example of a BoolProperty used by a UserExtension.', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FloatProperty
My question was (sorry, was not really clear): |
@julienduroure Yes, good question, I just tested this and it works. in
That might be a good thing to add to this example, now that the bug is fixed in master. The default is |
Thanks for your explanation. |
I will add that flag in the ExampleExtension. @julienduroure where/how should I write the documentation? |
@jjcasmar The documentation is here: docs/blender_docs/io_scene_gltf2.rst |
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.
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. |
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
aad9eb6
to
2195ce9
Compare
Hello, |
Thanks again @jjcasmar, this is an important one. |
Thanks @emackey . This patch is very useful for KDAB. |
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