diff --git a/openpype/hosts/houdini/plugins/load/load_alembic.py b/openpype/hosts/houdini/plugins/load/load_alembic.py index 96e666b255..c6f0ebf2f9 100644 --- a/openpype/hosts/houdini/plugins/load/load_alembic.py +++ b/openpype/hosts/houdini/plugins/load/load_alembic.py @@ -104,3 +104,6 @@ class AbcLoader(load.LoaderPlugin): node = container["node"] node.destroy() + + def switch(self, container, representation): + self.update(container, representation) diff --git a/openpype/hosts/houdini/plugins/load/load_alembic_archive.py b/openpype/hosts/houdini/plugins/load/load_alembic_archive.py index b960073e12..47d2e1b896 100644 --- a/openpype/hosts/houdini/plugins/load/load_alembic_archive.py +++ b/openpype/hosts/houdini/plugins/load/load_alembic_archive.py @@ -73,3 +73,6 @@ class AbcArchiveLoader(load.LoaderPlugin): node = container["node"] node.destroy() + + def switch(self, container, representation): + self.update(container, representation) diff --git a/openpype/hosts/houdini/plugins/load/load_bgeo.py b/openpype/hosts/houdini/plugins/load/load_bgeo.py index b298d423bc..86e8675c02 100644 --- a/openpype/hosts/houdini/plugins/load/load_bgeo.py +++ b/openpype/hosts/houdini/plugins/load/load_bgeo.py @@ -106,3 +106,6 @@ class BgeoLoader(load.LoaderPlugin): node = container["node"] node.destroy() + + def switch(self, container, representation): + self.update(container, representation) diff --git a/openpype/hosts/houdini/plugins/load/load_camera.py b/openpype/hosts/houdini/plugins/load/load_camera.py index 059ad11a76..6365508f4e 100644 --- a/openpype/hosts/houdini/plugins/load/load_camera.py +++ b/openpype/hosts/houdini/plugins/load/load_camera.py @@ -192,3 +192,6 @@ class CameraLoader(load.LoaderPlugin): new_node.moveToGoodPosition() return new_node + + def switch(self, container, representation): + self.update(container, representation) diff --git a/openpype/hosts/houdini/plugins/load/load_image.py b/openpype/hosts/houdini/plugins/load/load_image.py index c78798e58a..26bc569c53 100644 --- a/openpype/hosts/houdini/plugins/load/load_image.py +++ b/openpype/hosts/houdini/plugins/load/load_image.py @@ -125,3 +125,6 @@ class ImageLoader(load.LoaderPlugin): prefix, padding, suffix = first_fname.rsplit(".", 2) fname = ".".join([prefix, "$F{}".format(len(padding)), suffix]) return os.path.join(root, fname).replace("\\", "/") + + def switch(self, container, representation): + self.update(container, representation) diff --git a/openpype/hosts/houdini/plugins/load/load_usd_layer.py b/openpype/hosts/houdini/plugins/load/load_usd_layer.py index 2e5079925b..1f0ec25128 100644 --- a/openpype/hosts/houdini/plugins/load/load_usd_layer.py +++ b/openpype/hosts/houdini/plugins/load/load_usd_layer.py @@ -79,3 +79,6 @@ class USDSublayerLoader(load.LoaderPlugin): node = container["node"] node.destroy() + + def switch(self, container, representation): + self.update(container, representation) diff --git a/openpype/hosts/houdini/plugins/load/load_usd_reference.py b/openpype/hosts/houdini/plugins/load/load_usd_reference.py index c4371db39b..f66d05395e 100644 --- a/openpype/hosts/houdini/plugins/load/load_usd_reference.py +++ b/openpype/hosts/houdini/plugins/load/load_usd_reference.py @@ -79,3 +79,6 @@ class USDReferenceLoader(load.LoaderPlugin): node = container["node"] node.destroy() + + def switch(self, container, representation): + self.update(container, representation) diff --git a/openpype/hosts/houdini/plugins/load/load_vdb.py b/openpype/hosts/houdini/plugins/load/load_vdb.py index c558a7a0e7..87900502c5 100644 --- a/openpype/hosts/houdini/plugins/load/load_vdb.py +++ b/openpype/hosts/houdini/plugins/load/load_vdb.py @@ -102,3 +102,6 @@ class VdbLoader(load.LoaderPlugin): node = container["node"] node.destroy() + + def switch(self, container, representation): + self.update(container, representation) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index c3de2c327f..f814187cc1 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -32,6 +32,10 @@ from openpype.pipeline import ( load_container, registered_host, ) +from openpype.pipeline.create import ( + legacy_create, + get_legacy_creator_by_name, +) from openpype.pipeline.context_tools import ( get_current_asset_name, get_current_project_asset, @@ -3931,3 +3935,53 @@ def get_capture_preset(task_name, task_type, subset, project_settings, log): capture_preset = plugin_settings["capture_preset"] return capture_preset or {} + + +def create_rig_animation_instance(nodes, context, namespace, log=None): + """Create an animation publish instance for loaded rigs. + + See the RecreateRigAnimationInstance inventory action on how to use this + for loaded rig containers. + + Arguments: + nodes (list): Member nodes of the rig instance. + context (dict): Representation context of the rig container + namespace (str): Namespace of the rig container + log (logging.Logger, optional): Logger to log to if provided + + Returns: + None + + """ + output = next((node for node in nodes if + node.endswith("out_SET")), None) + controls = next((node for node in nodes if + node.endswith("controls_SET")), None) + + assert output, "No out_SET in rig, this is a bug." + assert controls, "No controls_SET in rig, this is a bug." + + # Find the roots amongst the loaded nodes + roots = ( + cmds.ls(nodes, assemblies=True, long=True) or + get_highest_in_hierarchy(nodes) + ) + assert roots, "No root nodes in rig, this is a bug." + + asset = legacy_io.Session["AVALON_ASSET"] + dependency = str(context["representation"]["_id"]) + + if log: + log.info("Creating subset: {}".format(namespace)) + + # Create the animation instance + creator_plugin = get_legacy_creator_by_name("CreateAnimation") + with maintained_selection(): + cmds.select([output, controls] + roots, noExpand=True) + legacy_create( + creator_plugin, + name=namespace, + asset=asset, + options={"useSelection": True}, + data={"dependencies": dependency} + ) diff --git a/openpype/hosts/maya/plugins/create/create_animation.py b/openpype/hosts/maya/plugins/create/create_animation.py index f992ff2c1a..095cbcdd64 100644 --- a/openpype/hosts/maya/plugins/create/create_animation.py +++ b/openpype/hosts/maya/plugins/create/create_animation.py @@ -7,6 +7,12 @@ from openpype.hosts.maya.api import ( class CreateAnimation(plugin.Creator): """Animation output for character rigs""" + # We hide the animation creator from the UI since the creation of it + # is automated upon loading a rig. There's an inventory action to recreate + # it for loaded rigs if by chance someone deleted the animation instance. + # Note: This setting is actually applied from project settings + enabled = False + name = "animationDefault" label = "Animation" family = "animation" diff --git a/openpype/hosts/maya/plugins/inventory/rig_recreate_animation_instance.py b/openpype/hosts/maya/plugins/inventory/rig_recreate_animation_instance.py new file mode 100644 index 0000000000..39bc59fbbf --- /dev/null +++ b/openpype/hosts/maya/plugins/inventory/rig_recreate_animation_instance.py @@ -0,0 +1,35 @@ +from openpype.pipeline import ( + InventoryAction, + get_representation_context +) +from openpype.hosts.maya.api.lib import ( + create_rig_animation_instance, + get_container_members, +) + + +class RecreateRigAnimationInstance(InventoryAction): + """Recreate animation publish instance for loaded rigs""" + + label = "Recreate rig animation instance" + icon = "wrench" + color = "#888888" + + @staticmethod + def is_compatible(container): + return ( + container.get("loader") == "ReferenceLoader" + and container.get("name", "").startswith("rig") + ) + + def process(self, containers): + + for container in containers: + # todo: delete an existing entry if it exist or skip creation + + namespace = container["namespace"] + representation_id = container["representation"] + context = get_representation_context(representation_id) + nodes = get_container_members(container) + + create_rig_animation_instance(nodes, context, namespace) diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index c2b321b789..0dbdb03bb7 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -4,16 +4,12 @@ import contextlib from maya import cmds from openpype.settings import get_project_settings -from openpype.pipeline import legacy_io -from openpype.pipeline.create import ( - legacy_create, - get_legacy_creator_by_name, -) import openpype.hosts.maya.api.plugin from openpype.hosts.maya.api.lib import ( maintained_selection, get_container_members, - parent_nodes + parent_nodes, + create_rig_animation_instance ) @@ -114,9 +110,6 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): icon = "code-fork" color = "orange" - # Name of creator class that will be used to create animation instance - animation_creator_name = "CreateAnimation" - def process_reference(self, context, name, namespace, options): import maya.cmds as cmds @@ -220,37 +213,10 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): self._lock_camera_transforms(members) def _post_process_rig(self, name, namespace, context, options): - - output = next((node for node in self if - node.endswith("out_SET")), None) - controls = next((node for node in self if - node.endswith("controls_SET")), None) - - assert output, "No out_SET in rig, this is a bug." - assert controls, "No controls_SET in rig, this is a bug." - - # Find the roots amongst the loaded nodes - roots = cmds.ls(self[:], assemblies=True, long=True) - assert roots, "No root nodes in rig, this is a bug." - - asset = legacy_io.Session["AVALON_ASSET"] - dependency = str(context["representation"]["_id"]) - - self.log.info("Creating subset: {}".format(namespace)) - - # Create the animation instance - creator_plugin = get_legacy_creator_by_name( - self.animation_creator_name + nodes = self[:] + create_rig_animation_instance( + nodes, context, namespace, log=self.log ) - with maintained_selection(): - cmds.select([output, controls] + roots, noExpand=True) - legacy_create( - creator_plugin, - name=namespace, - asset=asset, - options={"useSelection": True}, - data={"dependencies": dependency} - ) def _lock_camera_transforms(self, nodes): cameras = cmds.ls(nodes, type="camera") diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index a535f8d4c9..12223216cd 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -554,7 +554,7 @@ "publish_mip_map": true }, "CreateAnimation": { - "enabled": true, + "enabled": false, "write_color_sets": false, "write_face_sets": false, "include_parent_hierarchy": false, diff --git a/website/docs/artist_hosts_maya.md b/website/docs/artist_hosts_maya.md index 0a551f0213..e36ccb77d2 100644 --- a/website/docs/artist_hosts_maya.md +++ b/website/docs/artist_hosts_maya.md @@ -238,12 +238,12 @@ For resolution and frame range, use **OpenPype → Set Frame Range** and Creating and publishing rigs with OpenPype follows similar workflow as with other data types. Create your rig and mark parts of your hierarchy in sets to -help OpenPype validators and extractors to check it and publish it. +help OpenPype validators and extractors to check and publish it. ### Preparing rig for publish When creating rigs, it is recommended (and it is in fact enforced by validators) -to separate bones or driving objects, their controllers and geometry so they are +to separate bones or driven objects, their controllers and geometry so they are easily managed. Currently OpenPype doesn't allow to publish model at the same time as its rig so for demonstration purposes, I'll first create simple model for robotic arm, just made out of simple boxes and I'll publish it. @@ -252,41 +252,48 @@ arm, just made out of simple boxes and I'll publish it. For more information about publishing models, see [Publishing models](artist_hosts_maya.md#publishing-models). -Now lets start with empty scene. Load your model - **OpenPype → Load...**, right +Now let's start with empty scene. Load your model - **OpenPype → Load...**, right click on it and select **Reference (abc)**. -I've created few bones and their controllers in two separate -groups - `rig_GRP` and `controls_GRP`. Naming is not important - just adhere to -your naming conventions. +I've created a few bones in `rig_GRP`, their controllers in `controls_GRP` and +placed the rig's output geometry in `geometry_GRP`. Naming of the groups is not important - just adhere to +your naming conventions. Then I parented everything into a single top group named `arm_rig`. -Then I've put everything into `arm_rig` group. - -When you've prepared your hierarchy, it's time to create *Rig instance* in OpenPype. -Select your whole rig hierarchy and go **OpenPype → Create...**. Select **Rig**. -Set is created in your scene to mark rig parts for export. Notice that it has -two subsets - `controls_SET` and `out_SET`. Put your controls into `controls_SET` +With the prepared hierarchy it is time to create a *Rig instance* in OpenPype. +Select the top group of your rig and go to **OpenPype → Create...**. Select **Rig**. +A publish set for your rig is created in your scene to mark rig parts for export. +Notice that it has two subsets - `controls_SET` and `out_SET`. Put your controls into `controls_SET` and geometry to `out_SET`. You should end up with something like this: ![Maya - Rig Hierarchy Example](assets/maya-rig_hierarchy_example.jpg) +:::note controls_SET and out_SET contents +It is totally allowed to put the `geometry_GRP` in the `out_SET` as opposed to +the individual meshes - it's even **recommended**. However, the `controls_SET` +requires the individual controls in it that the artist is supposed to animate +and manipulate so the publish validators can accurately check the rig's +controls. +::: + ### Publishing rigs -Publishing rig is done in same way as publishing everything else. Save your scene -and go **OpenPype → Publish**. When you run validation you'll mostly run at first into -few issues. Although number of them will seem to be intimidating at first, you'll -find out they are mostly minor things easily fixed. +Publishing rigs is done in a same way as publishing everything else. Save your scene +and go **OpenPype → Publish**. When you run validation you'll most likely run into +a few issues at first. Although a number of them will seem to be intimidating you +will find out they are mostly minor things, easily fixed and are there to optimize +your rig for consistency and safe usage by the artist. -* **Non Duplicate Instance Members (ID)** - This will most likely fail because when +- **Non Duplicate Instance Members (ID)** - This will most likely fail because when creating rigs, we usually duplicate few parts of it to reuse them. But duplication will duplicate also ID of original object and OpenPype needs every object to have unique ID. This is easily fixed by **Repair** action next to validator name. click on little up arrow on right side of validator name and select **Repair** form menu. -* **Joints Hidden** - This is enforcing joints (bones) to be hidden for user as +- **Joints Hidden** - This is enforcing joints (bones) to be hidden for user as animator usually doesn't need to see them and they clutter his viewports. So well behaving rig should have them hidden. **Repair** action will help here also. -* **Rig Controllers** will check if there are no transforms on unlocked attributes +- **Rig Controllers** will check if there are no transforms on unlocked attributes of controllers. This is needed because animator should have ease way to reset rig to it's default position. It also check that those attributes doesn't have any incoming connections from other parts of scene to ensure that published rig doesn't @@ -297,6 +304,19 @@ have any missing dependencies. You can load rig with [Loader](artist_tools_loader). Go **OpenPype → Load...**, select your rig, right click on it and **Reference** it. +### Animation instances + +Whenever you load a rig an animation publish instance is automatically created +for it. This means that if you load a rig you don't need to create a pointcache +instance yourself to publish the geometry. This is all cleanly prepared for you +when loading a published rig. + +:::tip Missing animation instance for your loaded rig? +Did you accidentally delete the animation instance for a loaded rig? You can +recreate it using the [**Recreate rig animation instance**](artist_hosts_maya.md#recreate-rig-animation-instance) +inventory action. +::: + ## Point caches OpenPype is using Alembic format for point caches. Workflow is very similar as other data types. @@ -646,3 +666,15 @@ Select 1 container of type `animation` or `pointcache`, then 1+ container of any The action searches the selected containers for 1 animation container of type `animation` or `pointcache`. This animation container will be connected to the rest of the selected containers. Matching geometries between containers is done by comparing the attribute `cbId`. The connection between geometries is done with a live blendshape. + +### Recreate rig animation instance + +This action can regenerate an animation instance for a loaded rig, for example +for when it was accidentally deleted by the user. + +![Maya - Inventory Action Recreate Rig Animation Instance](assets/maya-inventory_action_recreate_animation_instance.png) + +#### Usage + +Select 1 or more container of type `rig` for which you want to recreate the +animation instance. diff --git a/website/docs/assets/maya-inventory_action_recreate_animation_instance.png b/website/docs/assets/maya-inventory_action_recreate_animation_instance.png new file mode 100644 index 0000000000..42a6f26964 Binary files /dev/null and b/website/docs/assets/maya-inventory_action_recreate_animation_instance.png differ