diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index c01ab5122cf..2f253fea825 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -35,6 +35,18 @@ body:
label: Version
description: What version are you running? Look to OpenPype Tray
options:
+ - 3.18.9-nightly.10
+ - 3.18.9-nightly.9
+ - 3.18.9-nightly.8
+ - 3.18.9-nightly.7
+ - 3.18.9-nightly.6
+ - 3.18.9-nightly.5
+ - 3.18.9-nightly.4
+ - 3.18.9-nightly.3
+ - 3.18.9-nightly.2
+ - 3.18.9-nightly.1
+ - 3.18.8
+ - 3.18.8-nightly.2
- 3.18.8-nightly.1
- 3.18.7
- 3.18.7-nightly.5
@@ -123,18 +135,6 @@ body:
- 3.16.1
- 3.16.0
- 3.16.0-nightly.2
- - 3.16.0-nightly.1
- - 3.15.12
- - 3.15.12-nightly.4
- - 3.15.12-nightly.3
- - 3.15.12-nightly.2
- - 3.15.12-nightly.1
- - 3.15.11
- - 3.15.11-nightly.5
- - 3.15.11-nightly.4
- - 3.15.11-nightly.3
- - 3.15.11-nightly.2
- - 3.15.11-nightly.1
validations:
required: true
- type: dropdown
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4ec3448570a..de9ac77bdd0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,226 @@
# Changelog
+## [3.18.8](https://github.com/ynput/OpenPype/tree/3.18.8)
+
+
+[Full Changelog](https://github.com/ynput/OpenPype/compare/3.18.7...3.18.8)
+
+### **🚀 Enhancements**
+
+
+
+Max: Implementation of Camera Attributes Validator #6110
+
+Implement Validate Camera Attributes in camera family in Max host
+
+
+___
+
+
+
+
+
+Max: Add missing workfile creator #6203
+
+Add the missing workfile creator in 3dsMax.
+
+
+___
+
+
+
+
+
+Deadline: Expose families transfer setting - OP-8268 #6217
+
+This PR exposes the `families_transfer` attribute on the `ProcessSubmittedJobOnFarm` plugin.The use case is to remove `ftrack` from the list if a studio does not want all render passes from Maya to become asset versions in Ftrack.
+
+
+___
+
+
+
+### **🐛 Bug fixes**
+
+
+
+Deadline: Add AVALON_DB to Deadline submissions - OP-8270 #6218
+
+Because testing uses a different database name https://github.com/ynput/OpenPype/blob/develop/tests/lib/testing_classes.py#L46 we need to add `AVALON_DB` to the environment for Deadline submissions.
+
+
+___
+
+
+
+
+
+Houdini: fix default render product name in Vray #6083
+
+This is fixing key name for default render products in VRay. Original name `RGB Color` caused issues during job submission.
+
+
+___
+
+
+
+
+
+Resolve Clip Load - Slate support #6126
+
+Loaded clip should ignore the slate, and be trimmed the same regardless of slate presence.closes: https://github.com/ynput/OpenPype/issues/6124#AY-1684
+
+
+___
+
+
+
+
+
+Use duration from streams as its more precise #6171
+
+When dealing with 30 fps mov of 2 frames, the duration was reduce to 3 decimal places (0.067) which meant that the flag for ffmpeg `-ss` ended up with a time that was not precise enough for ffmpeg to pick a frame; `0.0335`. Should be `0.0333`.Using the duration from the streams is more precise; `0.066667`.
+
+
+___
+
+
+
+
+
+Core: Headless publish failing without GL lib #6205
+
+Trying to run a headless publish in the farm I hit another blocker:
+```
+2024-02-07 20:42:45: 0: STDOUT: !!! AYON crashed:
+2024-02-07 20:42:45: 0: STDOUT: Traceback (most recent call last):
+2024-02-07 20:42:45: 0: STDOUT: File "start.py", line 740, in main_cli
+2024-02-07 20:42:45: 0: STDOUT: ))
+2024-02-07 20:42:45: 0: STDOUT: File "/usr/ayon-launcher/1.0.0+ax/dependencies/click/core.py", line 1157, in __call__
+2024-02-07 20:42:45: 0: STDOUT: return self.main(*args, **kwargs)
+2024-02-07 20:42:45: 0: STDOUT: File "/usr/ayon-launcher/1.0.0+ax/dependencies/click/core.py", line 1078, in main
+2024-02-07 20:42:45: 0: STDOUT: rv = self.invoke(ctx)
+2024-02-07 20:42:45: 0: STDOUT: File "/usr/ayon-launcher/1.0.0+ax/dependencies/click/core.py", line 1688, in invoke
+2024-02-07 20:42:45: 0: STDOUT: return _process_result(sub_ctx.command.invoke(sub_ctx))
+2024-02-07 20:42:45: 0: STDOUT: File "/usr/ayon-launcher/1.0.0+ax/dependencies/click/core.py", line 1434, in invoke
+2024-02-07 20:42:45: 0: STDOUT: return ctx.invoke(self.callback, **ctx.params)
+2024-02-07 20:42:45: 0: STDOUT: File "/usr/ayon-launcher/1.0.0+ax/dependencies/click/core.py", line 783, in invoke
+2024-02-07 20:42:45: 0: STDOUT: return __callback(*args, **kwargs)
+2024-02-07 20:42:45: 0: STDOUT: File "/pipe/dev/farrizabalaga/OpenPype/openpype/cli.py", line 197, in publish
+2024-02-07 20:42:45: 0: STDOUT: PypeCommands.publish(list(paths), targets, gui)
+2024-02-07 20:42:45: 0: STDOUT: File "/pipe/dev/farrizabalaga/OpenPype/openpype/pype_commands.py", line 100, in publish
+2024-02-07 20:42:45: 0: STDOUT: from openpype.tools.utils.host_tools import show_publish
+2024-02-07 20:42:45: 0: STDOUT: File "/pipe/dev/farrizabalaga/OpenPype/openpype/tools/utils/__init__.py", line 1, in
+2024-02-07 20:42:45: 0: STDOUT: from .layouts import FlowLayout
+2024-02-07 20:42:45: 0: STDOUT: File "/pipe/dev/farrizabalaga/OpenPype/openpype/tools/utils/layouts.py", line 1, in
+2024-02-07 20:42:45: 0: STDOUT: from qtpy import QtWidgets, QtCore
+2024-02-07 20:42:45: 0: STDOUT: File "/usr/ayon-launcher/1.0.0+ax/dependencies/qtpy/QtWidgets.py", line 111, in
+2024-02-07 20:42:45: 0: STDOUT: from PySide2.QtWidgets import *
+2024-02-07 20:42:45: 0: STDOUT: File "/usr/ayon-launcher/1.0.0+ax/vendor/python/shiboken2/files.dir/shibokensupport/__feature__.py", line 142, in _import
+2024-02-07 20:42:45: 0: STDOUT: return original_import(name, *args, **kwargs)
+2024-02-07 20:42:45: 0: STDOUT: ImportError: libGL.so.1: cannot open shared object file: No such file or directory
+```
+The imports of `openpype.tools.utils.host_tools.__init__.py` were throwing an error due to trying to import QtWidgets unnecessarily.
+
+
+___
+
+
+
+
+
+Nuke: LoadClip colorspace override - OP-6591 #6215
+
+Setting the colorspace from the representation data was not supported.
+
+
+___
+
+
+
+
+
+Hiero: Add OP settings and convert in plugin - OP-8338 #6232
+
+Missing settings for https://github.com/ynput/OpenPype/pull/6143.
+
+
+___
+
+
+
+
+
+Unreal: Fix Render Instance Collector to use folderPath #6233
+
+Fix Render Instance Collector to use folderPath instead of just the asset name.
+
+
+___
+
+
+
+
+
+Bugfix - Fix "Action Failed" window not showing #6236
+
+This PR targets to fix issue #6234.
+
+
+___
+
+
+
+
+
+Nuke: render use existing frames with slate offsets the published render - AY-1433 #6239
+
+Due to `frameStart` data member on representation for existing frames, the frame indexes would be re-numbered when integrating due to this; https://github.com/ynput/OpenPype/blob/develop/openpype/plugins/publish/integrate.py#L712-L726Removing `frameStart` had no effect on publishing workflows, local or farm.Also fixed an issues with slate collection which could misbehave if the instance node had "slate" in the name.Resolves #5883
+
+
+___
+
+
+
+
+
+AYON Workfiles tool: Copy and open of published workfile works #6241
+
+Fix copy and open published workfiles.
+
+
+___
+
+
+
+
+
+Chore: OCIO and python2 compatibility fixes #6242
+
+Nuke 12 is now fully supported with our OCIO wrapping functionalities.
+
+
+___
+
+
+
+### **Merged pull requests**
+
+
+
+Tests: Fix failing maya automatic test #6235
+
+Improvement on https://github.com/ynput/OpenPype/pull/6231
+
+
+___
+
+
+
+
+
+
## [3.18.7](https://github.com/ynput/OpenPype/tree/3.18.7)
diff --git a/openpype/hosts/hiero/api/events.py b/openpype/hosts/hiero/api/events.py
index 862a2607c1d..015f023083f 100644
--- a/openpype/hosts/hiero/api/events.py
+++ b/openpype/hosts/hiero/api/events.py
@@ -4,8 +4,8 @@
from .lib import (
sync_avalon_data_to_workfile,
launch_workfiles_app,
- selection_changed_timeline,
before_project_save,
+ apply_colorspace_project
)
from .tags import add_tags_to_workfile
from .menu import update_menu_task_label
@@ -42,6 +42,8 @@ def afterNewProjectCreated(event):
# reset workfiles startup not to open any more in session
os.environ["WORKFILES_STARTUP"] = "0"
+ apply_colorspace_project()
+
def beforeProjectLoad(event):
log.info("before project load event...")
diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py
index bf719160d11..af02889ed33 100644
--- a/openpype/hosts/hiero/api/lib.py
+++ b/openpype/hosts/hiero/api/lib.py
@@ -100,9 +100,9 @@ def flatten(list_):
def get_current_project(remove_untitled=False):
- projects = flatten(hiero.core.projects())
+ projects = hiero.core.projects()
if not remove_untitled:
- return next(iter(projects))
+ return projects[0]
# if remove_untitled
for proj in projects:
@@ -243,8 +243,13 @@ def get_track_items(
# collect all available active sequence track items
if not return_list:
sequence = get_current_sequence(name=sequence_name)
- # get all available tracks from sequence
- tracks = list(sequence.audioTracks()) + list(sequence.videoTracks())
+
+ tracks = []
+ if sequence is not None:
+ # get all available tracks from sequence
+ tracks = list(sequence.audioTracks())
+ tracks += list(sequence.videoTracks())
+
# loop all tracks
for track in tracks:
if check_locked and track.isLocked():
@@ -1039,18 +1044,68 @@ def _set_hrox_project_knobs(doc, **knobs):
def apply_colorspace_project():
- project_name = get_current_project_name()
- # get path the the active projects
- project = get_current_project(remove_untitled=True)
- current_file = project.path()
-
- # close the active project
- project.close()
-
+ """Apply colorspaces from settings.
+
+ Due to not being able to set the project settings through the Python API,
+ we need to do use some dubious code to find the widgets and set them. It is
+ possible to set the project settings without traversing through the widgets
+ but it involves reading the hrox files from disk with XML, so no in-memory
+ support. See https://community.foundry.com/discuss/topic/137771/change-a-project-s-default-color-transform-with-python # noqa
+ for more details.
+ """
# get presets for hiero
+ project_name = get_current_project_name()
imageio = get_project_settings(project_name)["hiero"]["imageio"]
presets = imageio.get("workfile")
+ # Open Project Settings UI.
+ for act in hiero.ui.registeredActions():
+ if act.objectName() == "foundry.project.settings":
+ act.trigger()
+
+ # Find widgets from their sibling label.
+ labels = {
+ "Working Space:": "workingSpace",
+ "Viewer:": "viewerLut",
+ "Thumbnails:": "thumbnailLut",
+ "Monitor Out:": "monitorOutLut",
+ "8 Bit Files:": "eightBitLut",
+ "16 Bit Files:": "sixteenBitLut",
+ "Log Files:": "logLut",
+ "Floating Point Files:": "floatLut"
+ }
+ widgets = {x: None for x in labels.values()}
+
+ def _recursive_children(widget, labels, widgets):
+ children = widget.children()
+ for count, child in enumerate(children):
+ if isinstance(child, QtWidgets.QLabel):
+ if child.text() in labels.keys():
+ widgets[labels[child.text()]] = children[count + 1]
+ _recursive_children(child, labels, widgets)
+
+ app = QtWidgets.QApplication.instance()
+ title = "Project Settings"
+ for widget in app.topLevelWidgets():
+ if isinstance(widget, QtWidgets.QMainWindow):
+ if widget.windowTitle() != title:
+ continue
+ _recursive_children(widget, labels, widgets)
+ widget.close()
+
+ msg = "Setting value \"{}\" is not a valid option for \"{}\""
+ for key, widget in widgets.items():
+ options = [widget.itemText(i) for i in range(widget.count())]
+ setting_value = presets[key]
+ assert setting_value in options, msg.format(setting_value, key)
+ widget.setCurrentText(presets[key])
+
+ # This code block is for setting up project colorspaces for files on disk.
+ # Due to not having Python API access to set the project settings, the
+ # Foundry recommended way is to modify the hrox files on disk with XML. See
+ # this forum thread for more details;
+ # https://community.foundry.com/discuss/topic/137771/change-a-project-s-default-color-transform-with-python # noqa
+ '''
# backward compatibility layer
# TODO: remove this after some time
config_data = get_imageio_config(
@@ -1063,6 +1118,13 @@ def apply_colorspace_project():
"ocioConfigName": "custom"
})
+ # get path the the active projects
+ project = get_current_project()
+ current_file = project.path()
+
+ msg = "The project needs to be saved to disk to apply colorspace settings."
+ assert current_file, msg
+
# save the workfile as subversion "comment:_colorspaceChange"
split_current_file = os.path.splitext(current_file)
copy_current_file = current_file
@@ -1105,6 +1167,7 @@ def apply_colorspace_project():
# open the file as current project
hiero.core.openProject(copy_current_file)
+ '''
def apply_colorspace_clips():
@@ -1114,10 +1177,8 @@ def apply_colorspace_clips():
# get presets for hiero
imageio = get_project_settings(project_name)["hiero"]["imageio"]
- from pprint import pprint
presets = imageio.get("regexInputs", {}).get("inputs", {})
- pprint(presets)
for clip in clips:
clip_media_source_path = clip.mediaSource().firstpath()
clip_name = clip.name()
diff --git a/openpype/hosts/hiero/plugins/publish/collect_clip_effects.py b/openpype/hosts/hiero/plugins/publish/collect_clip_effects.py
index d7f646ebc9f..44767e458ac 100644
--- a/openpype/hosts/hiero/plugins/publish/collect_clip_effects.py
+++ b/openpype/hosts/hiero/plugins/publish/collect_clip_effects.py
@@ -72,9 +72,13 @@ def process(self, instance):
subset_split.insert(0, "effect")
- effect_categories = {
- x["name"]: x["effect_classes"] for x in self.effect_categories
- }
+ # Need to convert to dict for AYON settings. This isinstance check can
+ # be removed in the future when OpenPype is no longer.
+ effect_categories = self.effect_categories
+ if isinstance(self.effect_categories, list):
+ effect_categories = {
+ x["name"]: x["effect_classes"] for x in self.effect_categories
+ }
category_by_effect = {"": ""}
for key, values in effect_categories.items():
diff --git a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py
index ad4fdb0da51..1b55e787b3f 100644
--- a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py
+++ b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py
@@ -67,7 +67,7 @@ def process(self, instance):
beauty_product = self.get_render_product_name(default_prefix)
render_products.append(beauty_product)
files_by_aov = {
- "RGB Color": self.generate_expected_files(instance,
+ "": self.generate_expected_files(instance,
beauty_product)}
if instance.data.get("RenderElement", True):
@@ -75,7 +75,9 @@ def process(self, instance):
if render_element:
for aov, renderpass in render_element.items():
render_products.append(renderpass)
- files_by_aov[aov] = self.generate_expected_files(instance, renderpass) # noqa
+ files_by_aov[aov] = self.generate_expected_files(
+ instance, renderpass)
+
for product in render_products:
self.log.debug("Found render product: %s" % product)
diff --git a/openpype/hosts/max/api/action.py b/openpype/hosts/max/api/action.py
new file mode 100644
index 00000000000..c3c1957af10
--- /dev/null
+++ b/openpype/hosts/max/api/action.py
@@ -0,0 +1,42 @@
+from pymxs import runtime as rt
+
+import pyblish.api
+
+from openpype.pipeline.publish import get_errored_instances_from_context
+
+
+class SelectInvalidAction(pyblish.api.Action):
+ """Select invalid objects in Blender when a publish plug-in failed."""
+ label = "Select Invalid"
+ on = "failed"
+ icon = "search"
+
+ def process(self, context, plugin):
+ errored_instances = get_errored_instances_from_context(context,
+ plugin=plugin)
+
+ # Get the invalid nodes for the plug-ins
+ self.log.info("Finding invalid nodes...")
+ invalid = list()
+ for instance in errored_instances:
+ invalid_nodes = plugin.get_invalid(instance)
+ if invalid_nodes:
+ if isinstance(invalid_nodes, (list, tuple)):
+ invalid.extend(invalid_nodes)
+ else:
+ self.log.warning(
+ "Failed plug-in doesn't have any selectable objects."
+ )
+
+ if not invalid:
+ self.log.info("No invalid nodes found.")
+ return
+ invalid_names = [obj.name for obj in invalid if isinstance(obj, str)]
+ if not invalid_names:
+ invalid_names = [obj.name for obj, _ in invalid]
+ invalid = [obj for obj, _ in invalid]
+ self.log.info(
+ "Selecting invalid objects: %s", ", ".join(invalid_names)
+ )
+
+ rt.Select(invalid)
diff --git a/openpype/hosts/max/plugins/create/create_workfile.py b/openpype/hosts/max/plugins/create/create_workfile.py
new file mode 100644
index 00000000000..30692ccd064
--- /dev/null
+++ b/openpype/hosts/max/plugins/create/create_workfile.py
@@ -0,0 +1,123 @@
+# -*- coding: utf-8 -*-
+"""Creator plugin for creating workfiles."""
+from openpype import AYON_SERVER_ENABLED
+from openpype.pipeline import CreatedInstance, AutoCreator
+from openpype.client import get_asset_by_name, get_asset_name_identifier
+from openpype.hosts.max.api import plugin
+from openpype.hosts.max.api.lib import read, imprint
+from pymxs import runtime as rt
+
+
+class CreateWorkfile(plugin.MaxCreatorBase, AutoCreator):
+ """Workfile auto-creator."""
+ identifier = "io.openpype.creators.max.workfile"
+ label = "Workfile"
+ family = "workfile"
+ icon = "fa5.file"
+
+ default_variant = "Main"
+
+ def create(self):
+ variant = self.default_variant
+ current_instance = next(
+ (
+ instance for instance in self.create_context.instances
+ if instance.creator_identifier == self.identifier
+ ), None)
+ project_name = self.project_name
+ asset_name = self.create_context.get_current_asset_name()
+ task_name = self.create_context.get_current_task_name()
+ host_name = self.create_context.host_name
+
+ if current_instance is None:
+ current_instance_asset = None
+ elif AYON_SERVER_ENABLED:
+ current_instance_asset = current_instance["folderPath"]
+ else:
+ current_instance_asset = current_instance["asset"]
+
+ if current_instance is None:
+ asset_doc = get_asset_by_name(project_name, asset_name)
+ subset_name = self.get_subset_name(
+ variant, task_name, asset_doc, project_name, host_name
+ )
+ data = {
+ "task": task_name,
+ "variant": variant
+ }
+ if AYON_SERVER_ENABLED:
+ data["folderPath"] = asset_name
+ else:
+ data["asset"] = asset_name
+
+ data.update(
+ self.get_dynamic_data(
+ variant, task_name, asset_doc,
+ project_name, host_name, current_instance)
+ )
+ self.log.info("Auto-creating workfile instance...")
+ instance_node = self.create_node(subset_name)
+ data["instance_node"] = instance_node.name
+ current_instance = CreatedInstance(
+ self.family, subset_name, data, self
+ )
+ self._add_instance_to_context(current_instance)
+ imprint(instance_node.name, current_instance.data)
+ elif (
+ current_instance_asset != asset_name
+ or current_instance["task"] != task_name
+ ):
+ # Update instance context if is not the same
+ asset_doc = get_asset_by_name(project_name, asset_name)
+ subset_name = self.get_subset_name(
+ variant, task_name, asset_doc, project_name, host_name
+ )
+ asset_name = get_asset_name_identifier(asset_doc)
+
+ if AYON_SERVER_ENABLED:
+ current_instance["folderPath"] = asset_name
+ else:
+ current_instance["asset"] = asset_name
+ current_instance["task"] = task_name
+ current_instance["subset"] = subset_name
+
+ def collect_instances(self):
+ self.cache_subsets(self.collection_shared_data)
+ for instance in self.collection_shared_data["max_cached_subsets"].get(self.identifier, []): # noqa
+ if not rt.getNodeByName(instance):
+ continue
+ created_instance = CreatedInstance.from_existing(
+ read(rt.GetNodeByName(instance)), self
+ )
+ self._add_instance_to_context(created_instance)
+
+ def update_instances(self, update_list):
+ for created_inst, _ in update_list:
+ instance_node = created_inst.get("instance_node")
+ imprint(
+ instance_node,
+ created_inst.data_to_store()
+ )
+
+ def remove_instances(self, instances):
+ """Remove specified instance from the scene.
+
+ This is only removing `id` parameter so instance is no longer
+ instance, because it might contain valuable data for artist.
+
+ """
+ for instance in instances:
+ instance_node = rt.GetNodeByName(
+ instance.data.get("instance_node"))
+ if instance_node:
+ rt.Delete(instance_node)
+
+ self._remove_instance_from_context(instance)
+
+ def create_node(self, subset_name):
+ if rt.getNodeByName(subset_name):
+ node = rt.getNodeByName(subset_name)
+ return node
+ node = rt.Container(name=subset_name)
+ node.isHidden = True
+ return node
diff --git a/openpype/hosts/max/plugins/publish/collect_members.py b/openpype/hosts/max/plugins/publish/collect_members.py
index 2970cf0e247..7cd646e0e72 100644
--- a/openpype/hosts/max/plugins/publish/collect_members.py
+++ b/openpype/hosts/max/plugins/publish/collect_members.py
@@ -12,7 +12,10 @@ class CollectMembers(pyblish.api.InstancePlugin):
hosts = ['max']
def process(self, instance):
-
+ if instance.data["family"] == "workfile":
+ self.log.debug("Skipping Actions for workfile family.")
+ self.log.debug("{}".format(instance.data["subset"]))
+ return
if instance.data.get("instance_node"):
container = rt.GetNodeByName(instance.data["instance_node"])
instance.data["members"] = [
diff --git a/openpype/hosts/max/plugins/publish/collect_workfile.py b/openpype/hosts/max/plugins/publish/collect_workfile.py
index 0eb4bb731e1..446175c0ed0 100644
--- a/openpype/hosts/max/plugins/publish/collect_workfile.py
+++ b/openpype/hosts/max/plugins/publish/collect_workfile.py
@@ -6,15 +6,16 @@
from pymxs import runtime as rt
-class CollectWorkfile(pyblish.api.ContextPlugin):
+class CollectWorkfile(pyblish.api.InstancePlugin):
"""Inject the current working file into context"""
order = pyblish.api.CollectorOrder - 0.01
label = "Collect 3dsmax Workfile"
hosts = ['max']
- def process(self, context):
+ def process(self, instance):
"""Inject the current working file."""
+ context = instance.context
folder = rt.maxFilePath
file = rt.maxFileName
if not folder or not file:
@@ -23,15 +24,12 @@ def process(self, context):
context.data['currentFile'] = current_file
- filename, ext = os.path.splitext(file)
-
- task = context.data["task"]
+ ext = os.path.splitext(file)[-1].lstrip(".")
data = {}
# create instance
- instance = context.create_instance(name=filename)
- subset = 'workfile' + task.capitalize()
+ subset = instance.data["subset"]
data.update({
"subset": subset,
@@ -55,7 +53,7 @@ def process(self, context):
}]
instance.data.update(data)
-
+ self.log.info('Collected data: {}'.format(data))
self.log.info('Collected instance: {}'.format(file))
self.log.info('Scene path: {}'.format(current_file))
self.log.info('staging Dir: {}'.format(folder))
diff --git a/openpype/hosts/max/plugins/publish/validate_camera_attributes.py b/openpype/hosts/max/plugins/publish/validate_camera_attributes.py
new file mode 100644
index 00000000000..4eec1951e5d
--- /dev/null
+++ b/openpype/hosts/max/plugins/publish/validate_camera_attributes.py
@@ -0,0 +1,88 @@
+import pyblish.api
+from pymxs import runtime as rt
+
+from openpype.pipeline.publish import (
+ RepairAction,
+ OptionalPyblishPluginMixin,
+ PublishValidationError
+)
+from openpype.hosts.max.api.action import SelectInvalidAction
+
+
+class ValidateCameraAttributes(OptionalPyblishPluginMixin,
+ pyblish.api.InstancePlugin):
+ """Validates Camera has no invalid attribute properties
+ or values.(For 3dsMax Cameras only)
+
+ """
+
+ order = pyblish.api.ValidatorOrder
+ families = ['camera']
+ hosts = ['max']
+ label = 'Validate Camera Attributes'
+ actions = [SelectInvalidAction, RepairAction]
+ optional = True
+
+ DEFAULTS = ["fov", "nearrange", "farrange",
+ "nearclip", "farclip"]
+ CAM_TYPE = ["Freecamera", "Targetcamera",
+ "Physical"]
+
+ @classmethod
+ def get_invalid(cls, instance):
+ invalid = []
+ if rt.units.DisplayType != rt.Name("Generic"):
+ cls.log.warning(
+ "Generic Type is not used as a scene unit\n\n"
+ "sure you tweak the settings with your own values\n\n"
+ "before validation.")
+ cameras = instance.data["members"]
+ project_settings = instance.context.data["project_settings"].get("max")
+ cam_attr_settings = (
+ project_settings["publish"]["ValidateCameraAttributes"]
+ )
+ for camera in cameras:
+ if str(rt.ClassOf(camera)) not in cls.CAM_TYPE:
+ cls.log.debug(
+ "Skipping camera created from external plugin..")
+ continue
+ for attr in cls.DEFAULTS:
+ default_value = cam_attr_settings.get(attr)
+ if default_value == float(0):
+ cls.log.debug(
+ f"the value of {attr} in setting set to"
+ " zero. Skipping the check.")
+ continue
+ if round(rt.getProperty(camera, attr), 1) != default_value:
+ cls.log.error(
+ f"Invalid attribute value for {camera.name}:{attr} "
+ f"(should be: {default_value}))")
+ invalid.append(camera)
+
+ return invalid
+
+ def process(self, instance):
+ if not self.is_active(instance.data):
+ self.log.debug("Skipping Validate Camera Attributes.")
+ return
+ invalid = self.get_invalid(instance)
+
+ if invalid:
+ raise PublishValidationError(
+ "Invalid camera attributes found. See log.")
+
+ @classmethod
+ def repair(cls, instance):
+ invalid_cameras = cls.get_invalid(instance)
+ project_settings = instance.context.data["project_settings"].get("max")
+ cam_attr_settings = (
+ project_settings["publish"]["ValidateCameraAttributes"]
+ )
+ for camera in invalid_cameras:
+ for attr in cls.DEFAULTS:
+ expected_value = cam_attr_settings.get(attr)
+ if expected_value == float(0):
+ cls.log.debug(
+ f"the value of {attr} in setting set to zero.")
+ continue
+ rt.setProperty(camera, attr, expected_value)
diff --git a/openpype/hosts/maya/api/alembic.py b/openpype/hosts/maya/api/alembic.py
new file mode 100644
index 00000000000..b657262b4db
--- /dev/null
+++ b/openpype/hosts/maya/api/alembic.py
@@ -0,0 +1,306 @@
+import json
+import logging
+import os
+
+from maya import cmds # noqa
+
+from openpype.hosts.maya.api.lib import evaluation
+
+log = logging.getLogger(__name__)
+
+# The maya alembic export types
+ALEMBIC_ARGS = {
+ "attr": (list, tuple),
+ "attrPrefix": (list, tuple),
+ "autoSubd": bool,
+ "dataFormat": str,
+ "dontSkipUnwrittenFrames": bool,
+ "endFrame": float,
+ "eulerFilter": bool,
+ "frameRange": str, # "start end"; overrides startFrame & endFrame
+ "frameRelativeSample": float,
+ "melPerFrameCallback": str,
+ "melPostJobCallback": str,
+ "noNormals": bool,
+ "preRoll": bool,
+ "preRollStartFrame": int,
+ "pythonPerFrameCallback": str,
+ "pythonPostJobCallback": str,
+ "renderableOnly": bool,
+ "root": (list, tuple),
+ "selection": bool,
+ "startFrame": float,
+ "step": float,
+ "stripNamespaces": bool,
+ "userAttr": (list, tuple),
+ "userAttrPrefix": (list, tuple),
+ "uvWrite": bool,
+ "uvsOnly": bool,
+ "verbose": bool,
+ "wholeFrameGeo": bool,
+ "worldSpace": bool,
+ "writeColorSets": bool,
+ "writeCreases": bool, # Maya 2015 Ext1+
+ "writeFaceSets": bool,
+ "writeUVSets": bool, # Maya 2017+
+ "writeVisibility": bool,
+}
+
+
+def extract_alembic(
+ file,
+ attr=None,
+ attrPrefix=None,
+ dataFormat="ogawa",
+ endFrame=None,
+ eulerFilter=True,
+ frameRange="",
+ noNormals=False,
+ preRoll=False,
+ preRollStartFrame=0,
+ renderableOnly=False,
+ root=None,
+ selection=True,
+ startFrame=None,
+ step=1.0,
+ stripNamespaces=True,
+ uvWrite=True,
+ verbose=False,
+ wholeFrameGeo=False,
+ worldSpace=False,
+ writeColorSets=False,
+ writeCreases=False,
+ writeNormals=False,
+ writeFaceSets=False,
+ writeUVSets=False,
+ writeVisibility=False
+):
+ """Extract a single Alembic Cache.
+
+ This extracts an Alembic cache using the `-selection` flag to minimize
+ the extracted content to solely what was Collected into the instance.
+
+ Arguments:
+
+ startFrame (float): Start frame of output. Ignored if `frameRange`
+ provided.
+
+ endFrame (float): End frame of output. Ignored if `frameRange`
+ provided.
+
+ frameRange (tuple or str): Two-tuple with start and end frame or a
+ string formatted as: "startFrame endFrame". This argument
+ overrides `startFrame` and `endFrame` arguments.
+
+ eulerFilter (bool): When on, X, Y, and Z rotation data is filtered with
+ an Euler filter. Euler filtering helps resolve irregularities in
+ rotations especially if X, Y, and Z rotations exceed 360 degrees.
+ Defaults to True.
+
+ noNormals (bool): When on, normal data from the original polygon
+ objects is not included in the exported Alembic cache file.
+
+ preRoll (bool): This frame range will not be sampled.
+ Defaults to False.
+
+ renderableOnly (bool): When on, any non-renderable nodes or hierarchy,
+ such as hidden objects, are not included in the Alembic file.
+ Defaults to False.
+
+ selection (bool): Write out all all selected nodes from the
+ active selection list that are descendents of the roots specified
+ with -root. Defaults to False.
+
+ uvWrite (bool): When on, UV data from polygon meshes and subdivision
+ objects are written to the Alembic file. Only the current UV map is
+ included.
+
+ writeColorSets (bool): Write all color sets on MFnMeshes as
+ color 3 or color 4 indexed geometry parameters with face varying
+ scope. Defaults to False.
+
+ writeFaceSets (bool): Write all Face sets on MFnMeshes.
+ Defaults to False.
+
+ wholeFrameGeo (bool): Data for geometry will only be written
+ out on whole frames. Defaults to False.
+
+ worldSpace (bool): When on, the top node in the node hierarchy is
+ stored as world space. By default, these nodes are stored as local
+ space. Defaults to False.
+
+ writeVisibility (bool): Visibility state will be stored in
+ the Alembic file. Otherwise everything written out is treated as
+ visible. Defaults to False.
+
+ writeUVSets (bool): Write all uv sets on MFnMeshes as vector
+ 2 indexed geometry parameters with face varying scope. Defaults to
+ False.
+
+ writeCreases (bool): If the mesh has crease edges or crease
+ vertices, the mesh (OPolyMesh) would now be written out as an OSubD
+ and crease info will be stored in the Alembic file. Otherwise,
+ creases info won't be preserved in Alembic file unless a custom
+ Boolean attribute SubDivisionMesh has been added to mesh node and
+ its value is true. Defaults to False.
+
+ dataFormat (str): The data format to use for the cache,
+ defaults to "ogawa"
+
+ step (float): The time interval (expressed in frames) at
+ which the frame range is sampled. Additional samples around each
+ frame can be specified with -frs. Defaults to 1.0.
+
+ attr (list of str, optional): A specific geometric attribute to write
+ out. Defaults to [].
+
+ attrPrefix (list of str, optional): Prefix filter for determining which
+ geometric attributes to write out. Defaults to ["ABC_"].
+
+ root (list of str): Maya dag path which will be parented to
+ the root of the Alembic file. Defaults to [], which means the
+ entire scene will be written out.
+
+ stripNamespaces (bool): When on, any namespaces associated with the
+ exported objects are removed from the Alembic file. For example, an
+ object with the namespace taco:foo:bar appears as bar in the
+ Alembic file.
+
+ verbose (bool): When on, outputs frame number information to the
+ Script Editor or output window during extraction.
+
+ preRollStartFrame (float): The frame to start scene
+ evaluation at. This is used to set the starting frame for time
+ dependent translations and can be used to evaluate run-up that
+ isn't actually translated. Defaults to 0.
+ """
+
+ # Ensure alembic exporter is loaded
+ cmds.loadPlugin('AbcExport', quiet=True)
+
+ # Alembic Exporter requires forward slashes
+ file = file.replace('\\', '/')
+
+ # Ensure list arguments are valid.
+ attr = attr or []
+ attrPrefix = attrPrefix or []
+ root = root or []
+
+ # Pass the start and end frame on as `frameRange` so that it
+ # never conflicts with that argument
+ if not frameRange:
+ # Fallback to maya timeline if no start or end frame provided.
+ if startFrame is None:
+ startFrame = cmds.playbackOptions(query=True,
+ animationStartTime=True)
+ if endFrame is None:
+ endFrame = cmds.playbackOptions(query=True,
+ animationEndTime=True)
+
+ # Ensure valid types are converted to frame range
+ assert isinstance(startFrame, ALEMBIC_ARGS["startFrame"])
+ assert isinstance(endFrame, ALEMBIC_ARGS["endFrame"])
+ frameRange = "{0} {1}".format(startFrame, endFrame)
+ else:
+ # Allow conversion from tuple for `frameRange`
+ if isinstance(frameRange, (list, tuple)):
+ assert len(frameRange) == 2
+ frameRange = "{0} {1}".format(frameRange[0], frameRange[1])
+
+ # Assemble options
+ options = {
+ "selection": selection,
+ "frameRange": frameRange,
+ "eulerFilter": eulerFilter,
+ "noNormals": noNormals,
+ "preRoll": preRoll,
+ "renderableOnly": renderableOnly,
+ "uvWrite": uvWrite,
+ "writeColorSets": writeColorSets,
+ "writeFaceSets": writeFaceSets,
+ "wholeFrameGeo": wholeFrameGeo,
+ "worldSpace": worldSpace,
+ "writeVisibility": writeVisibility,
+ "writeUVSets": writeUVSets,
+ "writeCreases": writeCreases,
+ "dataFormat": dataFormat,
+ "step": step,
+ "attr": attr,
+ "attrPrefix": attrPrefix,
+ "stripNamespaces": stripNamespaces,
+ "verbose": verbose,
+ "preRollStartFrame": preRollStartFrame
+ }
+
+ # Validate options
+ for key, value in options.copy().items():
+
+ # Discard unknown options
+ if key not in ALEMBIC_ARGS:
+ log.warning("extract_alembic() does not support option '%s'. "
+ "Flag will be ignored..", key)
+ options.pop(key)
+ continue
+
+ # Validate value type
+ valid_types = ALEMBIC_ARGS[key]
+ if not isinstance(value, valid_types):
+ raise TypeError("Alembic option unsupported type: "
+ "{0} (expected {1})".format(value, valid_types))
+
+ # Ignore empty values, like an empty string, since they mess up how
+ # job arguments are built
+ if isinstance(value, (list, tuple)):
+ value = [x for x in value if x.strip()]
+
+ # Ignore option completely if no values remaining
+ if not value:
+ options.pop(key)
+ continue
+
+ options[key] = value
+
+ # The `writeCreases` argument was changed to `autoSubd` in Maya 2018+
+ maya_version = int(cmds.about(version=True))
+ if maya_version >= 2018:
+ options['autoSubd'] = options.pop('writeCreases', False)
+
+ # Format the job string from options
+ job_args = list()
+ for key, value in options.items():
+ if isinstance(value, (list, tuple)):
+ for entry in value:
+ job_args.append("-{} {}".format(key, entry))
+ elif isinstance(value, bool):
+ # Add only when state is set to True
+ if value:
+ job_args.append("-{0}".format(key))
+ else:
+ job_args.append("-{0} {1}".format(key, value))
+
+ job_str = " ".join(job_args)
+ job_str += ' -file "%s"' % file
+
+ # Ensure output directory exists
+ parent_dir = os.path.dirname(file)
+ if not os.path.exists(parent_dir):
+ os.makedirs(parent_dir)
+
+ if verbose:
+ log.debug("Preparing Alembic export with options: %s",
+ json.dumps(options, indent=4))
+ log.debug("Extracting Alembic with job arguments: %s", job_str)
+
+ # Perform extraction
+ print("Alembic Job Arguments : {}".format(job_str))
+
+ # Disable the parallel evaluation temporarily to ensure no buggy
+ # exports are made. (PLN-31)
+ # TODO: Make sure this actually fixes the issues
+ with evaluation("off"):
+ cmds.AbcExport(j=job_str, verbose=verbose)
+
+ if verbose:
+ log.debug("Extracted Alembic to: %s", file)
+
+ return file
diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py
index 3a5c6fa10ff..25532504824 100644
--- a/openpype/hosts/maya/api/lib.py
+++ b/openpype/hosts/maya/api/lib.py
@@ -71,37 +71,6 @@
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0]
-# The maya alembic export types
-_alembic_options = {
- "startFrame": float,
- "endFrame": float,
- "frameRange": str, # "start end"; overrides startFrame & endFrame
- "eulerFilter": bool,
- "frameRelativeSample": float,
- "noNormals": bool,
- "renderableOnly": bool,
- "step": float,
- "stripNamespaces": bool,
- "uvWrite": bool,
- "wholeFrameGeo": bool,
- "worldSpace": bool,
- "writeVisibility": bool,
- "writeColorSets": bool,
- "writeFaceSets": bool,
- "writeCreases": bool, # Maya 2015 Ext1+
- "writeUVSets": bool, # Maya 2017+
- "dataFormat": str,
- "root": (list, tuple),
- "attr": (list, tuple),
- "attrPrefix": (list, tuple),
- "userAttr": (list, tuple),
- "melPerFrameCallback": str,
- "melPostJobCallback": str,
- "pythonPerFrameCallback": str,
- "pythonPostJobCallback": str,
- "selection": bool
-}
-
INT_FPS = {15, 24, 25, 30, 48, 50, 60, 44100, 48000}
FLOAT_FPS = {23.98, 23.976, 29.97, 47.952, 59.94}
@@ -1347,178 +1316,6 @@ def is_visible(node,
return True
-
-def extract_alembic(file,
- startFrame=None,
- endFrame=None,
- selection=True,
- uvWrite=True,
- eulerFilter=True,
- dataFormat="ogawa",
- verbose=False,
- **kwargs):
- """Extract a single Alembic Cache.
-
- This extracts an Alembic cache using the `-selection` flag to minimize
- the extracted content to solely what was Collected into the instance.
-
- Arguments:
-
- startFrame (float): Start frame of output. Ignored if `frameRange`
- provided.
-
- endFrame (float): End frame of output. Ignored if `frameRange`
- provided.
-
- frameRange (tuple or str): Two-tuple with start and end frame or a
- string formatted as: "startFrame endFrame". This argument
- overrides `startFrame` and `endFrame` arguments.
-
- dataFormat (str): The data format to use for the cache,
- defaults to "ogawa"
-
- verbose (bool): When on, outputs frame number information to the
- Script Editor or output window during extraction.
-
- noNormals (bool): When on, normal data from the original polygon
- objects is not included in the exported Alembic cache file.
-
- renderableOnly (bool): When on, any non-renderable nodes or hierarchy,
- such as hidden objects, are not included in the Alembic file.
- Defaults to False.
-
- stripNamespaces (bool): When on, any namespaces associated with the
- exported objects are removed from the Alembic file. For example, an
- object with the namespace taco:foo:bar appears as bar in the
- Alembic file.
-
- uvWrite (bool): When on, UV data from polygon meshes and subdivision
- objects are written to the Alembic file. Only the current UV map is
- included.
-
- worldSpace (bool): When on, the top node in the node hierarchy is
- stored as world space. By default, these nodes are stored as local
- space. Defaults to False.
-
- eulerFilter (bool): When on, X, Y, and Z rotation data is filtered with
- an Euler filter. Euler filtering helps resolve irregularities in
- rotations especially if X, Y, and Z rotations exceed 360 degrees.
- Defaults to True.
-
- """
-
- # Ensure alembic exporter is loaded
- cmds.loadPlugin('AbcExport', quiet=True)
-
- # Alembic Exporter requires forward slashes
- file = file.replace('\\', '/')
-
- # Pass the start and end frame on as `frameRange` so that it
- # never conflicts with that argument
- if "frameRange" not in kwargs:
- # Fallback to maya timeline if no start or end frame provided.
- if startFrame is None:
- startFrame = cmds.playbackOptions(query=True,
- animationStartTime=True)
- if endFrame is None:
- endFrame = cmds.playbackOptions(query=True,
- animationEndTime=True)
-
- # Ensure valid types are converted to frame range
- assert isinstance(startFrame, _alembic_options["startFrame"])
- assert isinstance(endFrame, _alembic_options["endFrame"])
- kwargs["frameRange"] = "{0} {1}".format(startFrame, endFrame)
- else:
- # Allow conversion from tuple for `frameRange`
- frame_range = kwargs["frameRange"]
- if isinstance(frame_range, (list, tuple)):
- assert len(frame_range) == 2
- kwargs["frameRange"] = "{0} {1}".format(frame_range[0],
- frame_range[1])
-
- # Assemble options
- options = {
- "selection": selection,
- "uvWrite": uvWrite,
- "eulerFilter": eulerFilter,
- "dataFormat": dataFormat
- }
- options.update(kwargs)
-
- # Validate options
- for key, value in options.copy().items():
-
- # Discard unknown options
- if key not in _alembic_options:
- log.warning("extract_alembic() does not support option '%s'. "
- "Flag will be ignored..", key)
- options.pop(key)
- continue
-
- # Validate value type
- valid_types = _alembic_options[key]
- if not isinstance(value, valid_types):
- raise TypeError("Alembic option unsupported type: "
- "{0} (expected {1})".format(value, valid_types))
-
- # Ignore empty values, like an empty string, since they mess up how
- # job arguments are built
- if isinstance(value, (list, tuple)):
- value = [x for x in value if x.strip()]
-
- # Ignore option completely if no values remaining
- if not value:
- options.pop(key)
- continue
-
- options[key] = value
-
- # The `writeCreases` argument was changed to `autoSubd` in Maya 2018+
- maya_version = int(cmds.about(version=True))
- if maya_version >= 2018:
- options['autoSubd'] = options.pop('writeCreases', False)
-
- # Format the job string from options
- job_args = list()
- for key, value in options.items():
- if isinstance(value, (list, tuple)):
- for entry in value:
- job_args.append("-{} {}".format(key, entry))
- elif isinstance(value, bool):
- # Add only when state is set to True
- if value:
- job_args.append("-{0}".format(key))
- else:
- job_args.append("-{0} {1}".format(key, value))
-
- job_str = " ".join(job_args)
- job_str += ' -file "%s"' % file
-
- # Ensure output directory exists
- parent_dir = os.path.dirname(file)
- if not os.path.exists(parent_dir):
- os.makedirs(parent_dir)
-
- if verbose:
- log.debug("Preparing Alembic export with options: %s",
- json.dumps(options, indent=4))
- log.debug("Extracting Alembic with job arguments: %s", job_str)
-
- # Perform extraction
- print("Alembic Job Arguments : {}".format(job_str))
-
- # Disable the parallel evaluation temporarily to ensure no buggy
- # exports are made. (PLN-31)
- # TODO: Make sure this actually fixes the issues
- with evaluation("off"):
- cmds.AbcExport(j=job_str, verbose=verbose)
-
- if verbose:
- log.debug("Extracted Alembic to: %s", file)
-
- return file
-
-
# region ID
def get_id_required_nodes(referenced_nodes=False, nodes=None):
"""Filter out any node which are locked (reference) or readOnly
diff --git a/openpype/hosts/maya/plugins/create/create_animation.py b/openpype/hosts/maya/plugins/create/create_animation.py
deleted file mode 100644
index 115c73c0d33..00000000000
--- a/openpype/hosts/maya/plugins/create/create_animation.py
+++ /dev/null
@@ -1,89 +0,0 @@
-from openpype.hosts.maya.api import (
- lib,
- plugin
-)
-from openpype.lib import (
- BoolDef,
- TextDef
-)
-
-
-class CreateAnimation(plugin.MayaHiddenCreator):
- """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.
- """
- identifier = "io.openpype.creators.maya.animation"
- name = "animationDefault"
- label = "Animation"
- family = "animation"
- icon = "male"
-
- write_color_sets = False
- write_face_sets = False
- include_parent_hierarchy = False
- include_user_defined_attributes = False
-
- def get_instance_attr_defs(self):
-
- defs = lib.collect_animation_defs()
-
- defs.extend([
- BoolDef("writeColorSets",
- label="Write vertex colors",
- tooltip="Write vertex colors with the geometry",
- default=self.write_color_sets),
- BoolDef("writeFaceSets",
- label="Write face sets",
- tooltip="Write face sets with the geometry",
- default=self.write_face_sets),
- BoolDef("writeNormals",
- label="Write normals",
- tooltip="Write normals with the deforming geometry",
- default=True),
- BoolDef("renderableOnly",
- label="Renderable Only",
- tooltip="Only export renderable visible shapes",
- default=False),
- BoolDef("visibleOnly",
- label="Visible Only",
- tooltip="Only export dag objects visible during "
- "frame range",
- default=False),
- BoolDef("includeParentHierarchy",
- label="Include Parent Hierarchy",
- tooltip="Whether to include parent hierarchy of nodes in "
- "the publish instance",
- default=self.include_parent_hierarchy),
- BoolDef("worldSpace",
- label="World-Space Export",
- default=True),
- BoolDef("includeUserDefinedAttributes",
- label="Include User Defined Attributes",
- default=self.include_user_defined_attributes),
- TextDef("attr",
- label="Custom Attributes",
- default="",
- placeholder="attr1, attr2"),
- TextDef("attrPrefix",
- label="Custom Attributes Prefix",
- placeholder="prefix1, prefix2")
- ])
-
- # TODO: Implement these on a Deadline plug-in instead?
- """
- # Default to not send to farm.
- self.data["farm"] = False
- self.data["priority"] = 50
- """
-
- return defs
-
- def apply_settings(self, project_settings):
- super(CreateAnimation, self).apply_settings(project_settings)
- # Hardcoding creator to be enabled due to existing settings would
- # disable the creator causing the creator plugin to not be
- # discoverable.
- self.enabled = True
diff --git a/openpype/hosts/maya/plugins/create/create_animation_pointcache.py b/openpype/hosts/maya/plugins/create/create_animation_pointcache.py
new file mode 100644
index 00000000000..e44b0c7b27d
--- /dev/null
+++ b/openpype/hosts/maya/plugins/create/create_animation_pointcache.py
@@ -0,0 +1,155 @@
+from maya import cmds
+
+from openpype.hosts.maya.api import lib, plugin
+
+from openpype.lib import (
+ BoolDef,
+ NumberDef,
+)
+from openpype.pipeline import CreatedInstance
+
+
+def _get_animation_attr_defs(cls):
+ """Get Animation generic definitions."""
+ defs = lib.collect_animation_defs()
+ defs.extend(
+ [
+ BoolDef("farm", label="Submit to Farm"),
+ NumberDef("priority", label="Farm job Priority", default=50),
+ BoolDef("refresh", label="Refresh viewport during export"),
+ BoolDef(
+ "includeParentHierarchy", label="Include Parent Hierarchy"
+ ),
+ BoolDef(
+ "includeUserDefinedAttributes",
+ label="Include User Defined Attributes"
+ ),
+ ]
+ )
+
+ return defs
+
+
+def extract_alembic_attributes(node_data, class_name):
+ """This is a legacy transfer of creator attributes to publish attributes
+ for ExtractAlembic/ExtractAnimation plugin.
+ """
+ publish_attributes = node_data["publish_attributes"]
+
+ if class_name in publish_attributes:
+ return node_data
+
+ extract_alembic_flags = [
+ "writeColorSets",
+ "writeFaceSets",
+ "writeNormals",
+ "renderableOnly",
+ "visibleOnly",
+ "worldSpace",
+ "renderableOnly"
+ ]
+ extract_alembic_attributes = [
+ "attr",
+ "attrPrefix",
+ "visibleOnly"
+ ]
+ attributes = extract_alembic_flags + extract_alembic_attributes
+ plugin_attributes = {"flags": []}
+ for attr in attributes:
+ if attr not in node_data["creator_attributes"].keys():
+ continue
+ value = node_data["creator_attributes"].pop(attr)
+
+ if value and attr in extract_alembic_flags:
+ plugin_attributes["flags"].append(attr)
+
+ if attr in extract_alembic_attributes:
+ plugin_attributes[attr] = value
+
+ publish_attributes[class_name] = plugin_attributes
+
+ return node_data
+
+
+class CreateAnimation(plugin.MayaHiddenCreator):
+ """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.
+ """
+
+ identifier = "io.openpype.creators.maya.animation"
+ name = "animationDefault"
+ label = "Animation"
+ family = "animation"
+ icon = "male"
+
+ write_color_sets = False
+ write_face_sets = False
+ include_parent_hierarchy = False
+ include_user_defined_attributes = False
+
+ def collect_instances(self):
+ try:
+ cached_subsets = self.collection_shared_data["maya_cached_subsets"]
+ except KeyError:
+ self.cache_subsets(self.collection_shared_data)
+ cached_subsets = self.collection_shared_data["maya_cached_subsets"]
+
+ for node in cached_subsets.get(self.identifier, []):
+ node_data = self.read_instance_node(node)
+
+ node_data = extract_alembic_attributes(
+ node_data, "ExtractAnimation"
+ )
+
+ created_instance = CreatedInstance.from_existing(node_data, self)
+ self._add_instance_to_context(created_instance)
+
+ def get_instance_attr_defs(self):
+ super(CreateAnimation, self).get_instance_attr_defs()
+ defs = _get_animation_attr_defs(self)
+ return defs
+
+
+class CreatePointCache(plugin.MayaCreator):
+ """Alembic pointcache for animated data"""
+
+ identifier = "io.openpype.creators.maya.pointcache"
+ label = "Pointcache"
+ family = "pointcache"
+ icon = "gears"
+ write_color_sets = False
+ write_face_sets = False
+ include_user_defined_attributes = False
+
+ def collect_instances(self):
+ try:
+ cached_subsets = self.collection_shared_data["maya_cached_subsets"]
+ except KeyError:
+ self.cache_subsets(self.collection_shared_data)
+ cached_subsets = self.collection_shared_data["maya_cached_subsets"]
+
+ for node in cached_subsets.get(self.identifier, []):
+ node_data = self.read_instance_node(node)
+
+ node_data = extract_alembic_attributes(node_data, "ExtractAlembic")
+
+ created_instance = CreatedInstance.from_existing(node_data, self)
+ self._add_instance_to_context(created_instance)
+
+ def get_instance_attr_defs(self):
+ super(CreatePointCache, self).get_instance_attr_defs()
+ defs = _get_animation_attr_defs(self)
+ return defs
+
+ def create(self, subset_name, instance_data, pre_create_data):
+ instance = super(CreatePointCache, self).create(
+ subset_name, instance_data, pre_create_data
+ )
+ instance_node = instance.get("instance_node")
+
+ # For Arnold standin proxy
+ proxy_set = cmds.sets(name=instance_node + "_proxy_SET", empty=True)
+ cmds.sets(proxy_set, forceElement=instance_node)
diff --git a/openpype/hosts/maya/plugins/create/create_pointcache.py b/openpype/hosts/maya/plugins/create/create_pointcache.py
deleted file mode 100644
index f4e8cbfc9a2..00000000000
--- a/openpype/hosts/maya/plugins/create/create_pointcache.py
+++ /dev/null
@@ -1,88 +0,0 @@
-from maya import cmds
-
-from openpype.hosts.maya.api import (
- lib,
- plugin
-)
-from openpype.lib import (
- BoolDef,
- TextDef
-)
-
-
-class CreatePointCache(plugin.MayaCreator):
- """Alembic pointcache for animated data"""
-
- identifier = "io.openpype.creators.maya.pointcache"
- label = "Pointcache"
- family = "pointcache"
- icon = "gears"
- write_color_sets = False
- write_face_sets = False
- include_user_defined_attributes = False
-
- def get_instance_attr_defs(self):
-
- defs = lib.collect_animation_defs()
-
- defs.extend([
- BoolDef("writeColorSets",
- label="Write vertex colors",
- tooltip="Write vertex colors with the geometry",
- default=False),
- BoolDef("writeFaceSets",
- label="Write face sets",
- tooltip="Write face sets with the geometry",
- default=False),
- BoolDef("renderableOnly",
- label="Renderable Only",
- tooltip="Only export renderable visible shapes",
- default=False),
- BoolDef("visibleOnly",
- label="Visible Only",
- tooltip="Only export dag objects visible during "
- "frame range",
- default=False),
- BoolDef("includeParentHierarchy",
- label="Include Parent Hierarchy",
- tooltip="Whether to include parent hierarchy of nodes in "
- "the publish instance",
- default=False),
- BoolDef("worldSpace",
- label="World-Space Export",
- default=True),
- BoolDef("refresh",
- label="Refresh viewport during export",
- default=False),
- BoolDef("includeUserDefinedAttributes",
- label="Include User Defined Attributes",
- default=self.include_user_defined_attributes),
- TextDef("attr",
- label="Custom Attributes",
- default="",
- placeholder="attr1, attr2"),
- TextDef("attrPrefix",
- label="Custom Attributes Prefix",
- default="",
- placeholder="prefix1, prefix2")
- ])
-
- # TODO: Implement these on a Deadline plug-in instead?
- """
- # Default to not send to farm.
- self.data["farm"] = False
- self.data["priority"] = 50
- """
-
- return defs
-
- def create(self, subset_name, instance_data, pre_create_data):
-
- instance = super(CreatePointCache, self).create(
- subset_name, instance_data, pre_create_data
- )
- instance_node = instance.get("instance_node")
-
- # For Arnold standin proxy
- proxy_set = cmds.sets(name=instance_node + "_proxy_SET", empty=True)
- cmds.sets(proxy_set, forceElement=instance_node)
diff --git a/openpype/hosts/maya/plugins/publish/collect_animation.py b/openpype/hosts/maya/plugins/publish/collect_animation.py
index 26a0a01c8bb..8628622bdd2 100644
--- a/openpype/hosts/maya/plugins/publish/collect_animation.py
+++ b/openpype/hosts/maya/plugins/publish/collect_animation.py
@@ -17,7 +17,7 @@ class CollectAnimationOutputGeometry(pyblish.api.InstancePlugin):
order = pyblish.api.CollectorOrder + 0.4
families = ["animation"]
- label = "Collect Animation Output Geometry"
+ label = "Collect Animation"
hosts = ["maya"]
ignore_type = ["constraints"]
@@ -58,3 +58,7 @@ def process(self, instance):
if instance.data.get("farm"):
instance.data["families"].append("publish.farm")
+ # User defined attributes.
+ instance.data["includeUserDefinedAttributes"] = (
+ instance.data["creator_attributes"]["includeUserDefinedAttributes"]
+ )
diff --git a/openpype/hosts/maya/plugins/publish/collect_pointcache.py b/openpype/hosts/maya/plugins/publish/collect_pointcache.py
index 5578a57f314..8b4289ed80c 100644
--- a/openpype/hosts/maya/plugins/publish/collect_pointcache.py
+++ b/openpype/hosts/maya/plugins/publish/collect_pointcache.py
@@ -45,3 +45,8 @@ def process(self, instance):
if proxy_set:
instance.remove(proxy_set)
instance.data["setMembers"].remove(proxy_set)
+
+ # User defined attributes.
+ instance.data["includeUserDefinedAttributes"] = (
+ instance.data["creator_attributes"]["includeUserDefinedAttributes"]
+ )
diff --git a/openpype/hosts/maya/plugins/publish/extract_pointcache.py b/openpype/hosts/maya/plugins/publish/extract_pointcache.py
index 0cc802fa7aa..79e07cf7322 100644
--- a/openpype/hosts/maya/plugins/publish/extract_pointcache.py
+++ b/openpype/hosts/maya/plugins/publish/extract_pointcache.py
@@ -3,15 +3,24 @@
from maya import cmds
from openpype.pipeline import publish
+from openpype.hosts.maya.api.alembic import extract_alembic
from openpype.hosts.maya.api.lib import (
- extract_alembic,
suspended_refresh,
maintained_selection,
- iter_visible_nodes_in_range
+ iter_visible_nodes_in_range,
)
+from openpype.lib import (
+ BoolDef,
+ TextDef,
+ NumberDef,
+ EnumDef,
+ UISeparatorDef,
+ UILabelDef,
+)
+from openpype.pipeline.publish import OpenPypePyblishPluginMixin
-class ExtractAlembic(publish.Extractor):
+class ExtractAlembic(publish.Extractor, OpenPypePyblishPluginMixin):
"""Produce an alembic of just point positions and normals.
Positions and normals, uvs, creases are preserved, but nothing more,
@@ -25,6 +34,19 @@ class ExtractAlembic(publish.Extractor):
hosts = ["maya"]
families = ["pointcache", "model", "vrayproxy.alembic"]
targets = ["local", "remote"]
+ flags = []
+ attr = []
+ attrPrefix = []
+ dataFormat = "ogawa"
+ melPerFrameCallback = ""
+ melPostJobCallback = ""
+ preRollStartFrame = 0
+ pythonPerFrameCallback = ""
+ pythonPostJobCallback = ""
+ userAttr = ""
+ userAttrPrefix = ""
+ visibleOnly = False
+ overrides = []
def process(self, instance):
if instance.data.get("farm"):
@@ -37,43 +59,75 @@ def process(self, instance):
start = float(instance.data.get("frameStartHandle", 1))
end = float(instance.data.get("frameEndHandle", 1))
- attrs = instance.data.get("attr", "").split(";")
- attrs = [value for value in attrs if value.strip()]
+ attribute_values = self.get_attr_values_from_data(
+ instance.data
+ )
+
+ attrs = [
+ attr.strip()
+ for attr in attribute_values.get("attr", "").split(";")
+ if attr.strip()
+ ]
attrs += instance.data.get("userDefinedAttributes", [])
attrs += ["cbId"]
- attr_prefixes = instance.data.get("attrPrefix", "").split(";")
- attr_prefixes = [value for value in attr_prefixes if value.strip()]
+ attr_prefixes = [
+ attr.strip()
+ for attr in attribute_values.get("attrPrefix", "").split(";")
+ if attr.strip()
+ ]
- self.log.debug("Extracting pointcache..")
+ self.log.debug("Extracting pointcache...")
dirname = self.staging_dir(instance)
parent_dir = self.staging_dir(instance)
filename = "{name}.abc".format(**instance.data)
path = os.path.join(parent_dir, filename)
- options = {
- "step": instance.data.get("step", 1.0),
- "attr": attrs,
- "attrPrefix": attr_prefixes,
- "writeVisibility": True,
- "writeCreases": True,
- "writeColorSets": instance.data.get("writeColorSets", False),
- "writeFaceSets": instance.data.get("writeFaceSets", False),
- "uvWrite": True,
- "selection": True,
- "worldSpace": instance.data.get("worldSpace", True)
- }
-
+ root = None
if not instance.data.get("includeParentHierarchy", True):
# Set the root nodes if we don't want to include parents
# The roots are to be considered the ones that are the actual
# direct members of the set
- options["root"] = roots
+ root = roots
- if int(cmds.about(version=True)) >= 2017:
- # Since Maya 2017 alembic supports multiple uv sets - write them.
- options["writeUVSets"] = True
+ args = {
+ "file": path,
+ "attr": attrs,
+ "attrPrefix": attr_prefixes,
+ "dataFormat": attribute_values.get("dataFormat", "ogawa"),
+ "endFrame": end,
+ "eulerFilter": False,
+ "noNormals": False,
+ "preRoll": False,
+ "preRollStartFrame": attribute_values.get(
+ "preRollStartFrame", 0
+ ),
+ "renderableOnly": False,
+ "root": root,
+ "selection": True,
+ "startFrame": start,
+ "step": instance.data.get(
+ "creator_attributes", {}
+ ).get("step", 1.0),
+ "stripNamespaces": False,
+ "uvWrite": False,
+ "verbose": False,
+ "wholeFrameGeo": False,
+ "worldSpace": False,
+ "writeColorSets": False,
+ "writeCreases": False,
+ "writeFaceSets": False,
+ "writeUVSets": False,
+ "writeVisibility": False,
+ }
+
+ # Export flags are defined as default enabled flags plus publisher
+ # enabled flags.
+ non_exposed_flags = list(set(self.flags) - set(self.overrides))
+ flags = attribute_values["flags"] + non_exposed_flags
+ for flag in flags:
+ args[flag] = True
if instance.data.get("visibleOnly", False):
# If we only want to include nodes that are visible in the frame
@@ -81,20 +135,20 @@ def process(self, instance):
# flag does not filter out those that are only hidden on some
# frames as it counts "animated" or "connected" visibilities as
# if it's always visible.
- nodes = list(iter_visible_nodes_in_range(nodes,
- start=start,
- end=end))
+ nodes = list(
+ iter_visible_nodes_in_range(nodes, start=start, end=end)
+ )
suspend = not instance.data.get("refresh", False)
with suspended_refresh(suspend=suspend):
with maintained_selection():
cmds.select(nodes, noExpand=True)
- extract_alembic(
- file=path,
- startFrame=start,
- endFrame=end,
- **options
+ self.log.debug(
+ "Running `extract_alembic` with the arguments: {}".format(
+ args
+ )
)
+ extract_alembic(**args)
if "representations" not in instance.data:
instance.data["representations"] = []
@@ -103,7 +157,7 @@ def process(self, instance):
"name": "abc",
"ext": "abc",
"files": filename,
- "stagingDir": dirname
+ "stagingDir": dirname,
}
instance.data["representations"].append(representation)
@@ -118,52 +172,171 @@ def process(self, instance):
return
path = path.replace(".abc", "_proxy.abc")
+ args["file"] = path
if not instance.data.get("includeParentHierarchy", True):
# Set the root nodes if we don't want to include parents
# The roots are to be considered the ones that are the actual
# direct members of the set
- options["root"] = instance.data["proxyRoots"]
+ args["root"] = instance.data["proxyRoots"]
with suspended_refresh(suspend=suspend):
with maintained_selection():
cmds.select(instance.data["proxy"])
- extract_alembic(
- file=path,
- startFrame=start,
- endFrame=end,
- **options
- )
+ extract_alembic(**args)
representation = {
"name": "proxy",
"ext": "abc",
"files": os.path.basename(path),
"stagingDir": dirname,
- "outputName": "proxy"
+ "outputName": "proxy",
}
instance.data["representations"].append(representation)
def get_members_and_roots(self, instance):
return instance[:], instance.data.get("setMembers")
+ @classmethod
+ def get_attribute_defs(cls):
+ override_defs = {
+ "attr": {
+ "def": TextDef,
+ "kwargs": {
+ "label": "Custom Attributes",
+ "placeholder": "attr1; attr2; ...",
+ }
+ },
+ "attrPrefix": {
+ "def": TextDef,
+ "kwargs": {
+ "label": "Custom Attributes Prefix",
+ "placeholder": "prefix1; prefix2; ...",
+ }
+ },
+ "dataFormat": {
+ "def": EnumDef,
+ "kwargs": {
+ "label": "Data Format",
+ "items": ["ogawa", "HDF"],
+ }
+ },
+ "melPerFrameCallback": {
+ "def": TextDef,
+ "kwargs": {
+ "label": "melPerFrameCallback",
+ }
+ },
+ "melPostJobCallback": {
+ "def": TextDef,
+ "kwargs": {
+ "label": "melPostJobCallback",
+ }
+ },
+ "preRollStartFrame": {
+ "def": NumberDef,
+ "kwargs": {
+ "label": "Start frame for preroll",
+ "tooltip": (
+ "The frame to start scene evaluation at. This is used"
+ " to set the starting frame for time dependent "
+ "translations and can be used to evaluate run-up that"
+ " isn't actually translated."
+ ),
+ }
+ },
+ "pythonPerFrameCallback": {
+ "def": TextDef,
+ "kwargs": {
+ "label": "pythonPerFrameCallback",
+ }
+ },
+ "pythonPostJobCallback": {
+ "def": TextDef,
+ "kwargs": {
+ "label": "pythonPostJobCallback",
+ }
+ },
+ "userAttr": {
+ "def": TextDef,
+ "kwargs": {
+ "label": "userAttr",
+ }
+ },
+ "userAttrPrefix": {
+ "def": TextDef,
+ "kwargs": {
+ "label": "userAttrPrefix",
+ }
+ },
+ "visibleOnly": {
+ "def": BoolDef,
+ "kwargs": {
+ "label": "Visible Only",
+ }
+ }
+ }
+
+ defs = super(ExtractAlembic, cls).get_attribute_defs()
+
+ defs.extend([
+ UISeparatorDef("sep_alembic_options"),
+ UILabelDef("Alembic Options"),
+ ])
+
+ # The Arguments that can be modified by the Publisher
+ overrides = set(getattr(cls, "overrides", set()))
+
+ # What we have set in the Settings as defaults.
+ flags = set(getattr(cls, "flags", set()))
+
+ enabled_flags = [x for x in flags if x in overrides]
+ flags = overrides - set(override_defs.keys())
+ defs.append(
+ EnumDef(
+ "flags",
+ flags,
+ default=enabled_flags,
+ multiselection=True,
+ label="Export Flags",
+ )
+ )
+
+ for key, value in override_defs.items():
+ if key not in overrides:
+ continue
+
+ kwargs = value["kwargs"]
+ kwargs["default"] = getattr(cls, key, None)
+ defs.append(
+ value["def"](key, **value["kwargs"])
+ )
+
+ defs.append(
+ UISeparatorDef("sep_alembic_options")
+ )
+
+ return defs
+
class ExtractAnimation(ExtractAlembic):
- label = "Extract Animation"
+ label = "Extract Animation (Alembic)"
families = ["animation"]
def get_members_and_roots(self, instance):
-
# Collect the out set nodes
out_sets = [node for node in instance if node.endswith("out_SET")]
if len(out_sets) != 1:
- raise RuntimeError("Couldn't find exactly one out_SET: "
- "{0}".format(out_sets))
+ raise RuntimeError(
+ "Couldn't find exactly one out_SET: " "{0}".format(out_sets)
+ )
out_set = out_sets[0]
roots = cmds.sets(out_set, query=True)
# Include all descendants
- nodes = roots + cmds.listRelatives(roots,
- allDescendents=True,
- fullPath=True) or []
+ nodes = (
+ roots
+ + cmds.listRelatives(roots, allDescendents=True, fullPath=True)
+ or []
+ )
return nodes, roots
diff --git a/openpype/hosts/maya/plugins/publish/extract_proxy_abc.py b/openpype/hosts/maya/plugins/publish/extract_proxy_abc.py
index d9bec87cfd3..b54c91f05af 100644
--- a/openpype/hosts/maya/plugins/publish/extract_proxy_abc.py
+++ b/openpype/hosts/maya/plugins/publish/extract_proxy_abc.py
@@ -3,8 +3,8 @@
from maya import cmds
from openpype.pipeline import publish
+from openpype.hosts.maya.api.alembic import extract_alembic
from openpype.hosts.maya.api.lib import (
- extract_alembic,
suspended_refresh,
maintained_selection,
iter_visible_nodes_in_range
diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_abc.py b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_abc.py
index 780ed2377cc..70d39d97098 100644
--- a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_abc.py
+++ b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_abc.py
@@ -6,8 +6,8 @@
from maya import cmds # noqa
from openpype.pipeline import publish
+from openpype.hosts.maya.api.alembic import extract_alembic
from openpype.hosts.maya.api.lib import (
- extract_alembic,
suspended_refresh,
maintained_selection
)
diff --git a/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py b/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py
index 4bd01c2df2b..bfcf65c6521 100644
--- a/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py
+++ b/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py
@@ -5,7 +5,7 @@
from maya import cmds
import pyblish.api
-from openpype.hosts.maya.api.lib import extract_alembic
+from openpype.hosts.maya.api.alembic import extract_alembic
from openpype.pipeline import publish
from openpype.lib import StringTemplate
diff --git a/openpype/hosts/maya/plugins/publish/validate_alembic_options_defaults.py b/openpype/hosts/maya/plugins/publish/validate_alembic_options_defaults.py
new file mode 100644
index 00000000000..e16196a6d38
--- /dev/null
+++ b/openpype/hosts/maya/plugins/publish/validate_alembic_options_defaults.py
@@ -0,0 +1,106 @@
+import pyblish.api
+
+from openpype.pipeline import OptionalPyblishPluginMixin
+from openpype.pipeline.publish import RepairAction, PublishValidationError
+
+
+class ValidateAlembicOptionsDefaults(
+ pyblish.api.InstancePlugin, OptionalPyblishPluginMixin
+):
+ """Validate the attributes on the instance are defaults."""
+
+ order = pyblish.api.ValidatorOrder
+ families = ["pointcache", "animation"]
+ hosts = ["maya"]
+ label = "Validate Alembic Options Defaults"
+ actions = [RepairAction]
+ optional = True
+
+ @classmethod
+ def _get_plugin_name(self, publish_attributes):
+ for key in ["ExtractAnimation", "ExtractAlembic"]:
+ if key in publish_attributes.keys():
+ return key
+
+ @classmethod
+ def _get_settings(self, context):
+ maya_settings = context.data["project_settings"]["maya"]
+ settings = maya_settings["publish"]["ExtractAlembic"]
+ # Flags are a special case since they are a combination of overrides
+ # and default flags from the settings.
+ settings["flags"] = [
+ x for x in settings["flags"] if x in settings["overrides"]
+ ]
+ return settings
+
+ @classmethod
+ def _get_publish_attributes(self, instance):
+ attributes = instance.data["publish_attributes"][
+ self._get_plugin_name(
+ instance.data["publish_attributes"]
+ )
+ ]
+
+ settings = self._get_settings(instance.context)
+
+ # Flags are a special case since they are a combination of exposed
+ # flags and default flags from the settings. So we need to add the
+ # default flags from the settings and ensure unique items.
+ non_exposed_flags = [
+ x for x in settings["flags"] if x not in settings["overrides"]
+ ]
+ attributes["flags"] = attributes["flags"] + non_exposed_flags
+
+ return attributes
+
+ def process(self, instance):
+ if not self.is_active(instance.data):
+ return
+
+ settings = self._get_settings(instance.context)
+
+ attributes = self._get_publish_attributes(instance)
+
+ msg = (
+ "Alembic Extract setting \"{}\" is not the default value:"
+ "\nCurrent: {}"
+ "\nDefault Value: {}\n"
+ )
+ errors = []
+ for key, value in attributes.items():
+ default_value = settings[key]
+
+ # Lists are best to compared sorted since we cant rely on the order
+ # of the items.
+ if isinstance(value, list):
+ value = sorted(value)
+ default_value = sorted(default_value)
+
+ if value != default_value:
+ errors.append(msg.format(key, value, default_value))
+
+ if errors:
+ raise PublishValidationError("\n".join(errors))
+
+ @classmethod
+ def repair(cls, instance):
+ # Find create instance twin.
+ create_context = instance.context.data["create_context"]
+ create_instance = None
+ for Instance in create_context.instances:
+ if Instance.data["instance_id"] == instance.data["instance_id"]:
+ create_instance = Instance
+ break
+
+ assert create_instance is not None
+
+ # Set the settings values on the create context then save to workfile.
+ publish_attributes = instance.data["publish_attributes"]
+ plugin_name = cls._get_plugin_name(publish_attributes)
+ attributes = cls._get_publish_attributes(instance)
+ settings = cls._get_settings(instance.context)
+ create_publish_attributes = create_instance.data["publish_attributes"]
+ for key in attributes.keys():
+ create_publish_attributes[plugin_name][key] = settings[key]
+
+ create_context.save_changes()
diff --git a/openpype/hosts/maya/plugins/publish/validate_animated_reference.py b/openpype/hosts/maya/plugins/publish/validate_animated_reference.py
index 4537892d6d7..dd7b361780d 100644
--- a/openpype/hosts/maya/plugins/publish/validate_animated_reference.py
+++ b/openpype/hosts/maya/plugins/publish/validate_animated_reference.py
@@ -2,12 +2,14 @@
import openpype.hosts.maya.api.action
from openpype.pipeline.publish import (
PublishValidationError,
- ValidateContentsOrder
+ ValidateContentsOrder,
+ OptionalPyblishPluginMixin
)
from maya import cmds
-class ValidateAnimatedReferenceRig(pyblish.api.InstancePlugin):
+class ValidateAnimatedReferenceRig(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""Validate all nodes in skeletonAnim_SET are referenced"""
order = ValidateContentsOrder
@@ -16,8 +18,11 @@ class ValidateAnimatedReferenceRig(pyblish.api.InstancePlugin):
label = "Animated Reference Rig"
accepted_controllers = ["transform", "locator"]
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
+ optional = False
def process(self, instance):
+ if not self.is_active(instance.data):
+ return
animated_sets = instance.data.get("animated_skeleton", [])
if not animated_sets:
self.log.debug(
diff --git a/openpype/hosts/maya/plugins/publish/validate_animation_content.py b/openpype/hosts/maya/plugins/publish/validate_animation_content.py
index 99acdc7b8f1..69c43d6bcc0 100644
--- a/openpype/hosts/maya/plugins/publish/validate_animation_content.py
+++ b/openpype/hosts/maya/plugins/publish/validate_animation_content.py
@@ -2,12 +2,14 @@
import openpype.hosts.maya.api.action
from openpype.pipeline.publish import (
PublishValidationError,
- ValidateContentsOrder
+ ValidateContentsOrder,
+ OptionalPyblishPluginMixin
)
-class ValidateAnimationContent(pyblish.api.InstancePlugin):
- """Adheres to the content of 'animation' family
+class ValidateAnimationContent(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
+ """Adheres to the content of 'animation' product type
- Must have collected `out_hierarchy` data.
- All nodes in `out_hierarchy` must be in the instance.
@@ -19,6 +21,7 @@ class ValidateAnimationContent(pyblish.api.InstancePlugin):
families = ["animation"]
label = "Animation Content"
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
+ optional = False
@classmethod
def get_invalid(cls, instance):
@@ -48,6 +51,8 @@ def get_invalid(cls, instance):
return invalid
def process(self, instance):
+ if not self.is_active(instance.data):
+ return
invalid = self.get_invalid(instance)
if invalid:
raise PublishValidationError(
diff --git a/openpype/hosts/maya/plugins/publish/validate_animation_out_set_related_node_ids.py b/openpype/hosts/maya/plugins/publish/validate_animation_out_set_related_node_ids.py
index 6f5f03ab396..e30a58ca998 100644
--- a/openpype/hosts/maya/plugins/publish/validate_animation_out_set_related_node_ids.py
+++ b/openpype/hosts/maya/plugins/publish/validate_animation_out_set_related_node_ids.py
@@ -6,11 +6,13 @@
from openpype.pipeline.publish import (
RepairAction,
ValidateContentsOrder,
- PublishValidationError
+ PublishValidationError,
+ OptionalPyblishPluginMixin
)
-class ValidateOutRelatedNodeIds(pyblish.api.InstancePlugin):
+class ValidateOutRelatedNodeIds(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""Validate if deformed shapes have related IDs to the original shapes
When a deformer is applied in the scene on a referenced mesh that already
@@ -28,10 +30,12 @@ class ValidateOutRelatedNodeIds(pyblish.api.InstancePlugin):
openpype.hosts.maya.api.action.SelectInvalidAction,
RepairAction
]
+ optional = False
def process(self, instance):
"""Process all meshes"""
-
+ if not self.is_active(instance.data):
+ return
# Ensure all nodes have a cbId and a related ID to the original shapes
# if a deformer has been created on the shape
invalid = self.get_invalid(instance)
diff --git a/openpype/hosts/maya/plugins/publish/validate_ass_relative_paths.py b/openpype/hosts/maya/plugins/publish/validate_ass_relative_paths.py
index 49913fa42b5..ce0610dc204 100644
--- a/openpype/hosts/maya/plugins/publish/validate_ass_relative_paths.py
+++ b/openpype/hosts/maya/plugins/publish/validate_ass_relative_paths.py
@@ -8,11 +8,13 @@
from openpype.pipeline.publish import (
RepairAction,
ValidateContentsOrder,
- PublishValidationError
+ PublishValidationError,
+ OptionalPyblishPluginMixin
)
-class ValidateAssRelativePaths(pyblish.api.InstancePlugin):
+class ValidateAssRelativePaths(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""Ensure exporting ass file has set relative texture paths"""
order = ValidateContentsOrder
@@ -20,8 +22,11 @@ class ValidateAssRelativePaths(pyblish.api.InstancePlugin):
families = ['ass']
label = "ASS has relative texture paths"
actions = [RepairAction]
+ optional = False
def process(self, instance):
+ if not self.is_active(instance.data):
+ return
# we cannot ask this until user open render settings as
# `defaultArnoldRenderOptions` doesn't exist
errors = []
diff --git a/openpype/hosts/maya/plugins/publish/validate_assembly_name.py b/openpype/hosts/maya/plugins/publish/validate_assembly_name.py
index 00588cd3006..636b9004e8f 100644
--- a/openpype/hosts/maya/plugins/publish/validate_assembly_name.py
+++ b/openpype/hosts/maya/plugins/publish/validate_assembly_name.py
@@ -2,11 +2,13 @@
import maya.cmds as cmds
import openpype.hosts.maya.api.action
from openpype.pipeline.publish import (
- PublishValidationError
+ PublishValidationError,
+ OptionalPyblishPluginMixin
)
-class ValidateAssemblyName(pyblish.api.InstancePlugin):
+class ValidateAssemblyName(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
""" Ensure Assembly name ends with `GRP`
Check if assembly name ends with `_GRP` string.
@@ -17,6 +19,7 @@ class ValidateAssemblyName(pyblish.api.InstancePlugin):
families = ["assembly"]
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
active = False
+ optional = True
@classmethod
def get_invalid(cls, instance):
@@ -47,7 +50,8 @@ def get_invalid(cls, instance):
return invalid
def process(self, instance):
-
+ if not self.is_active(instance.data):
+ return
invalid = self.get_invalid(instance)
if invalid:
raise PublishValidationError("Found {} invalid named assembly "
diff --git a/openpype/hosts/maya/plugins/publish/validate_assembly_namespaces.py b/openpype/hosts/maya/plugins/publish/validate_assembly_namespaces.py
index 06577f38f7e..9f8d3483fd0 100644
--- a/openpype/hosts/maya/plugins/publish/validate_assembly_namespaces.py
+++ b/openpype/hosts/maya/plugins/publish/validate_assembly_namespaces.py
@@ -1,10 +1,13 @@
import pyblish.api
import openpype.hosts.maya.api.action
from openpype.pipeline.publish import (
- PublishValidationError
+ PublishValidationError,
+ OptionalPyblishPluginMixin
)
-class ValidateAssemblyNamespaces(pyblish.api.InstancePlugin):
+
+class ValidateAssemblyNamespaces(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""Ensure namespaces are not nested
In the outliner an item in a normal namespace looks as following:
@@ -20,9 +23,11 @@ class ValidateAssemblyNamespaces(pyblish.api.InstancePlugin):
order = pyblish.api.ValidatorOrder
families = ["assembly"]
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
+ optional = False
def process(self, instance):
-
+ if not self.is_active(instance.data):
+ return
self.log.debug("Checking namespace for %s" % instance.name)
if self.get_invalid(instance):
raise PublishValidationError("Nested namespaces found")
diff --git a/openpype/hosts/maya/plugins/publish/validate_assembly_transforms.py b/openpype/hosts/maya/plugins/publish/validate_assembly_transforms.py
index a24455ebaac..de881e2a84e 100644
--- a/openpype/hosts/maya/plugins/publish/validate_assembly_transforms.py
+++ b/openpype/hosts/maya/plugins/publish/validate_assembly_transforms.py
@@ -2,10 +2,15 @@
from maya import cmds
import openpype.hosts.maya.api.action
-from openpype.pipeline.publish import PublishValidationError, RepairAction
+from openpype.pipeline.publish import (
+ PublishValidationError,
+ RepairAction,
+ OptionalPyblishPluginMixin
+)
-class ValidateAssemblyModelTransforms(pyblish.api.InstancePlugin):
+class ValidateAssemblyModelTransforms(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""Verify only root nodes of the loaded asset have transformations.
Note: This check is temporary and is subject to change.
@@ -34,7 +39,11 @@ class ValidateAssemblyModelTransforms(pyblish.api.InstancePlugin):
" This can alter the look of your scene. "
"Are you sure you want to continue?")
+ optional = False
+
def process(self, instance):
+ if not self.is_active(instance.data):
+ return
invalid = self.get_invalid(instance)
if invalid:
raise PublishValidationError(
diff --git a/openpype/hosts/maya/plugins/publish/validate_camera_attributes.py b/openpype/hosts/maya/plugins/publish/validate_camera_attributes.py
index e5745612e99..3aca7c01904 100644
--- a/openpype/hosts/maya/plugins/publish/validate_camera_attributes.py
+++ b/openpype/hosts/maya/plugins/publish/validate_camera_attributes.py
@@ -3,10 +3,14 @@
import openpype.hosts.maya.api.action
from openpype.pipeline.publish import (
- PublishValidationError, ValidateContentsOrder)
+ PublishValidationError,
+ ValidateContentsOrder,
+ OptionalPyblishPluginMixin
+)
-class ValidateCameraAttributes(pyblish.api.InstancePlugin):
+class ValidateCameraAttributes(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""Validates Camera has no invalid attribute keys or values.
The Alembic file format does not a specific subset of attributes as such
@@ -20,6 +24,7 @@ class ValidateCameraAttributes(pyblish.api.InstancePlugin):
hosts = ['maya']
label = 'Camera Attributes'
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
+ optional = True
DEFAULTS = [
("filmFitOffset", 0.0),
@@ -62,7 +67,8 @@ def get_invalid(cls, instance):
def process(self, instance):
"""Process all the nodes in the instance"""
-
+ if not self.is_active(instance.data):
+ return
invalid = self.get_invalid(instance)
if invalid:
diff --git a/openpype/hosts/maya/plugins/publish/validate_camera_contents.py b/openpype/hosts/maya/plugins/publish/validate_camera_contents.py
index 767ac55718f..689189371ad 100644
--- a/openpype/hosts/maya/plugins/publish/validate_camera_contents.py
+++ b/openpype/hosts/maya/plugins/publish/validate_camera_contents.py
@@ -3,10 +3,13 @@
import openpype.hosts.maya.api.action
from openpype.pipeline.publish import (
- PublishValidationError, ValidateContentsOrder)
+ PublishValidationError,
+ ValidateContentsOrder,
+ OptionalPyblishPluginMixin)
-class ValidateCameraContents(pyblish.api.InstancePlugin):
+class ValidateCameraContents(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""Validates Camera instance contents.
A Camera instance may only hold a SINGLE camera's transform, nothing else.
@@ -22,6 +25,7 @@ class ValidateCameraContents(pyblish.api.InstancePlugin):
label = 'Camera Contents'
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
validate_shapes = True
+ optional = False
@classmethod
def get_invalid(cls, instance):
@@ -71,7 +75,8 @@ def get_invalid(cls, instance):
def process(self, instance):
"""Process all the nodes in the instance"""
-
+ if not self.is_active(instance.data):
+ return
invalid = self.get_invalid(instance)
if invalid:
raise PublishValidationError("Invalid camera contents: "
diff --git a/openpype/hosts/maya/plugins/publish/validate_current_renderlayer_renderable.py b/openpype/hosts/maya/plugins/publish/validate_current_renderlayer_renderable.py
index f072e5e3234..992143e8277 100644
--- a/openpype/hosts/maya/plugins/publish/validate_current_renderlayer_renderable.py
+++ b/openpype/hosts/maya/plugins/publish/validate_current_renderlayer_renderable.py
@@ -1,10 +1,14 @@
import pyblish.api
from maya import cmds
-from openpype.pipeline.publish import context_plugin_should_run
+from openpype.pipeline.publish import (
+ context_plugin_should_run,
+ OptionalPyblishPluginMixin
+)
-class ValidateCurrentRenderLayerIsRenderable(pyblish.api.ContextPlugin):
+class ValidateCurrentRenderLayerIsRenderable(pyblish.api.ContextPlugin,
+ OptionalPyblishPluginMixin):
"""Validate if current render layer has a renderable camera
There is a bug in Redshift which occurs when the current render layer
@@ -20,9 +24,11 @@ class ValidateCurrentRenderLayerIsRenderable(pyblish.api.ContextPlugin):
order = pyblish.api.ValidatorOrder
hosts = ["maya"]
families = ["renderlayer"]
+ optional = False
def process(self, context):
-
+ if not self.is_active(context.data):
+ return
# Workaround bug pyblish-base#250
if not context_plugin_should_run(self, context):
return
diff --git a/openpype/hosts/maya/plugins/publish/validate_glsl_material.py b/openpype/hosts/maya/plugins/publish/validate_glsl_material.py
index 3b386c3def1..a34f8206ec7 100644
--- a/openpype/hosts/maya/plugins/publish/validate_glsl_material.py
+++ b/openpype/hosts/maya/plugins/publish/validate_glsl_material.py
@@ -6,10 +6,14 @@
RepairAction,
ValidateContentsOrder
)
-from openpype.pipeline import PublishValidationError
+from openpype.pipeline import (
+ PublishValidationError,
+ OptionalPyblishPluginMixin
+)
-class ValidateGLSLMaterial(pyblish.api.InstancePlugin):
+class ValidateGLSLMaterial(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""
Validate if the asset uses GLSL Shader
"""
@@ -23,6 +27,8 @@ class ValidateGLSLMaterial(pyblish.api.InstancePlugin):
active = True
def process(self, instance):
+ if not self.is_active(instance.data):
+ return
shading_grp = self.get_material_from_shapes(instance)
if not shading_grp:
raise PublishValidationError("No shading group found")
diff --git a/openpype/hosts/maya/plugins/publish/validate_glsl_plugin.py b/openpype/hosts/maya/plugins/publish/validate_glsl_plugin.py
index da065fcf946..61201afa21b 100644
--- a/openpype/hosts/maya/plugins/publish/validate_glsl_plugin.py
+++ b/openpype/hosts/maya/plugins/publish/validate_glsl_plugin.py
@@ -5,11 +5,13 @@
from openpype.pipeline.publish import (
RepairAction,
ValidateContentsOrder,
- PublishValidationError
+ PublishValidationError,
+ OptionalPyblishPluginMixin
)
-class ValidateGLSLPlugin(pyblish.api.InstancePlugin):
+class ValidateGLSLPlugin(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""
Validate if the asset uses GLSL Shader
"""
@@ -19,8 +21,11 @@ class ValidateGLSLPlugin(pyblish.api.InstancePlugin):
hosts = ['maya']
label = 'maya2glTF plugin'
actions = [RepairAction]
+ optional = False
def process(self, instance):
+ if not self.is_active(instance.data):
+ return
if not cmds.pluginInfo("maya2glTF", query=True, loaded=True):
raise PublishValidationError("maya2glTF is not loaded")
diff --git a/openpype/hosts/maya/plugins/publish/validate_instancer_content.py b/openpype/hosts/maya/plugins/publish/validate_instancer_content.py
index 236adfb03d7..92bbd1ef78a 100644
--- a/openpype/hosts/maya/plugins/publish/validate_instancer_content.py
+++ b/openpype/hosts/maya/plugins/publish/validate_instancer_content.py
@@ -2,10 +2,14 @@
import pyblish.api
from openpype.hosts.maya.api import lib
-from openpype.pipeline.publish import PublishValidationError
+from openpype.pipeline.publish import (
+ PublishValidationError,
+ OptionalPyblishPluginMixin
+)
-class ValidateInstancerContent(pyblish.api.InstancePlugin):
+class ValidateInstancerContent(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""Validates that all meshes in the instance have object IDs.
This skips a check on intermediate objects because we consider them
@@ -14,9 +18,11 @@ class ValidateInstancerContent(pyblish.api.InstancePlugin):
order = pyblish.api.ValidatorOrder
label = 'Instancer Content'
families = ['instancer']
+ optional = False
def process(self, instance):
-
+ if not self.is_active(instance.data):
+ return
error = False
members = instance.data['setMembers']
export_members = instance.data['exactExportMembers']
diff --git a/openpype/hosts/maya/plugins/publish/validate_instancer_frame_ranges.py b/openpype/hosts/maya/plugins/publish/validate_instancer_frame_ranges.py
index 714c6229d6e..05917b5b3bd 100644
--- a/openpype/hosts/maya/plugins/publish/validate_instancer_frame_ranges.py
+++ b/openpype/hosts/maya/plugins/publish/validate_instancer_frame_ranges.py
@@ -3,8 +3,10 @@
import pyblish.api
-from openpype.pipeline.publish import PublishValidationError
-
+from openpype.pipeline.publish import (
+ PublishValidationError,
+ OptionalPyblishPluginMixin
+)
def is_cache_resource(resource):
"""Return whether resource is a cacheFile resource"""
@@ -34,7 +36,8 @@ def filter_ticks(files):
return tick_files, ticks
-class ValidateInstancerFrameRanges(pyblish.api.InstancePlugin):
+class ValidateInstancerFrameRanges(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""Validates all instancer particle systems are cached correctly.
This means they should have the files/frames as required by the start-end
@@ -46,6 +49,7 @@ class ValidateInstancerFrameRanges(pyblish.api.InstancePlugin):
order = pyblish.api.ValidatorOrder
label = 'Instancer Cache Frame Ranges'
families = ['instancer']
+ optional = False
@classmethod
def get_invalid(cls, instance):
@@ -157,7 +161,8 @@ def get_invalid(cls, instance):
return invalid
def process(self, instance):
-
+ if not self.is_active(instance.data):
+ return
invalid = self.get_invalid(instance)
if invalid:
diff --git a/openpype/hosts/maya/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/maya/plugins/publish/validate_loaded_plugin.py
index eac13053db4..2a790c56630 100644
--- a/openpype/hosts/maya/plugins/publish/validate_loaded_plugin.py
+++ b/openpype/hosts/maya/plugins/publish/validate_loaded_plugin.py
@@ -4,17 +4,20 @@
from openpype.pipeline.publish import (
RepairContextAction,
- PublishValidationError
+ PublishValidationError,
+ OptionalPyblishPluginMixin
)
-class ValidateLoadedPlugin(pyblish.api.ContextPlugin):
+class ValidateLoadedPlugin(pyblish.api.ContextPlugin,
+ OptionalPyblishPluginMixin):
"""Ensure there are no unauthorized loaded plugins"""
label = "Loaded Plugin"
order = pyblish.api.ValidatorOrder
host = ["maya"]
actions = [RepairContextAction]
+ optional = True
@classmethod
def get_invalid(cls, context):
@@ -35,7 +38,8 @@ def get_invalid(cls, context):
return invalid
def process(self, context):
-
+ if not self.is_active(context.data):
+ return
invalid = self.get_invalid(context)
if invalid:
raise PublishValidationError(
diff --git a/openpype/hosts/maya/plugins/publish/validate_look_shading_group.py b/openpype/hosts/maya/plugins/publish/validate_look_shading_group.py
index dbe7a70e6a6..e2c241f9a0b 100644
--- a/openpype/hosts/maya/plugins/publish/validate_look_shading_group.py
+++ b/openpype/hosts/maya/plugins/publish/validate_look_shading_group.py
@@ -5,16 +5,17 @@
from openpype.pipeline.publish import (
RepairAction,
ValidateContentsOrder,
- PublishValidationError
+ PublishValidationError,
+ OptionalPyblishPluginMixin
)
-class ValidateShadingEngine(pyblish.api.InstancePlugin):
+class ValidateShadingEngine(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""Validate all shading engines are named after the surface material.
Shading engines should be named "{surface_shader}SG"
"""
-
order = ValidateContentsOrder
families = ["look"]
hosts = ["maya"]
@@ -22,9 +23,12 @@ class ValidateShadingEngine(pyblish.api.InstancePlugin):
actions = [
openpype.hosts.maya.api.action.SelectInvalidAction, RepairAction
]
+ optional = True
# The default connections to check
def process(self, instance):
+ if not self.is_active(instance.data):
+ return
invalid = self.get_invalid(instance)
if invalid:
diff --git a/openpype/hosts/maya/plugins/publish/validate_maya_units.py b/openpype/hosts/maya/plugins/publish/validate_maya_units.py
index ae6dc093a91..553fd06f8e7 100644
--- a/openpype/hosts/maya/plugins/publish/validate_maya_units.py
+++ b/openpype/hosts/maya/plugins/publish/validate_maya_units.py
@@ -7,11 +7,13 @@
from openpype.pipeline.publish import (
RepairContextAction,
ValidateSceneOrder,
- PublishXmlValidationError
+ PublishXmlValidationError,
+ OptionalPyblishPluginMixin
)
-class ValidateMayaUnits(pyblish.api.ContextPlugin):
+class ValidateMayaUnits(pyblish.api.ContextPlugin,
+ OptionalPyblishPluginMixin):
"""Check if the Maya units are set correct"""
order = ValidateSceneOrder
@@ -35,6 +37,7 @@ class ValidateMayaUnits(pyblish.api.ContextPlugin):
"Maya scene {setting} must be '{required_value}'. "
"Current value is '{current_value}'."
)
+ optional = False
@classmethod
def apply_settings(cls, project_settings):
@@ -52,7 +55,8 @@ def apply_settings(cls, project_settings):
cls.validate_fps = settings.get("validate_fps", cls.validate_fps)
def process(self, context):
-
+ if not self.is_active(context.data):
+ return
# Collected units
linearunits = context.data.get('linearUnits')
angularunits = context.data.get('angularUnits')
diff --git a/openpype/hosts/maya/plugins/publish/validate_mesh_lamina_faces.py b/openpype/hosts/maya/plugins/publish/validate_mesh_lamina_faces.py
index f120361583c..eda1c63ddd6 100644
--- a/openpype/hosts/maya/plugins/publish/validate_mesh_lamina_faces.py
+++ b/openpype/hosts/maya/plugins/publish/validate_mesh_lamina_faces.py
@@ -2,10 +2,14 @@
import pyblish.api
import openpype.hosts.maya.api.action
-from openpype.pipeline.publish import ValidateMeshOrder
+from openpype.pipeline.publish import (
+ ValidateMeshOrder,
+ OptionalPyblishPluginMixin
+)
-class ValidateMeshLaminaFaces(pyblish.api.InstancePlugin):
+class ValidateMeshLaminaFaces(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""Validate meshes don't have lamina faces.
Lamina faces share all of their edges.
@@ -17,6 +21,7 @@ class ValidateMeshLaminaFaces(pyblish.api.InstancePlugin):
families = ['model']
label = 'Mesh Lamina Faces'
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
+ optional = True
@staticmethod
def get_invalid(instance):
@@ -28,6 +33,8 @@ def get_invalid(instance):
def process(self, instance):
"""Process all the nodes in the instance 'objectSet'"""
+ if not self.is_active(instance.data):
+ return
invalid = self.get_invalid(instance)
diff --git a/openpype/hosts/maya/plugins/publish/validate_mesh_ngons.py b/openpype/hosts/maya/plugins/publish/validate_mesh_ngons.py
index 5b67db33070..0e70360442c 100644
--- a/openpype/hosts/maya/plugins/publish/validate_mesh_ngons.py
+++ b/openpype/hosts/maya/plugins/publish/validate_mesh_ngons.py
@@ -3,10 +3,14 @@
import pyblish.api
import openpype.hosts.maya.api.action
from openpype.hosts.maya.api import lib
-from openpype.pipeline.publish import ValidateContentsOrder
+from openpype.pipeline.publish import (
+ ValidateContentsOrder,
+ OptionalPyblishPluginMixin
+)
-class ValidateMeshNgons(pyblish.api.Validator):
+class ValidateMeshNgons(pyblish.api.Validator,
+ OptionalPyblishPluginMixin):
"""Ensure that meshes don't have ngons
Ngon are faces with more than 4 sides.
@@ -21,6 +25,7 @@ class ValidateMeshNgons(pyblish.api.Validator):
families = ["model"]
label = "Mesh ngons"
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
+ optional = True
@staticmethod
def get_invalid(instance):
@@ -39,6 +44,8 @@ def get_invalid(instance):
def process(self, instance):
"""Process all the nodes in the instance "objectSet"""
+ if not self.is_active(instance.data):
+ return
invalid = self.get_invalid(instance)
if invalid:
diff --git a/openpype/hosts/maya/plugins/publish/validate_mesh_no_negative_scale.py b/openpype/hosts/maya/plugins/publish/validate_mesh_no_negative_scale.py
index 48b4d0f557e..048d757000c 100644
--- a/openpype/hosts/maya/plugins/publish/validate_mesh_no_negative_scale.py
+++ b/openpype/hosts/maya/plugins/publish/validate_mesh_no_negative_scale.py
@@ -4,7 +4,8 @@
import openpype.hosts.maya.api.action
from openpype.pipeline.publish import (
ValidateMeshOrder,
- PublishValidationError
+ PublishValidationError,
+ OptionalPyblishPluginMixin
)
@@ -15,7 +16,8 @@ def _as_report_list(values, prefix="- ", suffix="\n"):
return prefix + (suffix + prefix).join(values)
-class ValidateMeshNoNegativeScale(pyblish.api.Validator):
+class ValidateMeshNoNegativeScale(pyblish.api.Validator,
+ OptionalPyblishPluginMixin):
"""Ensure that meshes don't have a negative scale.
Using negatively scaled proxies in a VRayMesh results in inverted
@@ -32,6 +34,7 @@ class ValidateMeshNoNegativeScale(pyblish.api.Validator):
families = ['model']
label = 'Mesh No Negative Scale'
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
+ optional = False
@staticmethod
def get_invalid(instance):
@@ -52,7 +55,8 @@ def get_invalid(instance):
def process(self, instance):
"""Process all the nodes in the instance 'objectSet'"""
-
+ if not self.is_active(instance.data):
+ return
invalid = self.get_invalid(instance)
if invalid:
diff --git a/openpype/hosts/maya/plugins/publish/validate_mesh_non_manifold.py b/openpype/hosts/maya/plugins/publish/validate_mesh_non_manifold.py
index 6fd63fb29f2..d9a21dddbff 100644
--- a/openpype/hosts/maya/plugins/publish/validate_mesh_non_manifold.py
+++ b/openpype/hosts/maya/plugins/publish/validate_mesh_non_manifold.py
@@ -4,7 +4,8 @@
import openpype.hosts.maya.api.action
from openpype.pipeline.publish import (
ValidateMeshOrder,
- PublishValidationError
+ PublishValidationError,
+ OptionalPyblishPluginMixin
)
@@ -15,7 +16,8 @@ def _as_report_list(values, prefix="- ", suffix="\n"):
return prefix + (suffix + prefix).join(values)
-class ValidateMeshNonManifold(pyblish.api.Validator):
+class ValidateMeshNonManifold(pyblish.api.Validator,
+ OptionalPyblishPluginMixin):
"""Ensure that meshes don't have non-manifold edges or vertices
To debug the problem on the meshes you can use Maya's modeling
@@ -28,6 +30,7 @@ class ValidateMeshNonManifold(pyblish.api.Validator):
families = ['model']
label = 'Mesh Non-Manifold Edges/Vertices'
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
+ optional = True
@staticmethod
def get_invalid(instance):
@@ -44,7 +47,8 @@ def get_invalid(instance):
def process(self, instance):
"""Process all the nodes in the instance 'objectSet'"""
-
+ if not self.is_active(instance.data):
+ return
invalid = self.get_invalid(instance)
if invalid:
diff --git a/openpype/hosts/maya/plugins/publish/validate_mesh_shader_connections.py b/openpype/hosts/maya/plugins/publish/validate_mesh_shader_connections.py
index 1db7613999b..9c57bc8d101 100644
--- a/openpype/hosts/maya/plugins/publish/validate_mesh_shader_connections.py
+++ b/openpype/hosts/maya/plugins/publish/validate_mesh_shader_connections.py
@@ -5,7 +5,8 @@
from openpype.pipeline.publish import (
RepairAction,
ValidateMeshOrder,
- PublishValidationError
+ PublishValidationError,
+ OptionalPyblishPluginMixin
)
@@ -79,7 +80,8 @@ def disconnect(node_a, node_b):
cmds.disconnectAttr(source, input)
-class ValidateMeshShaderConnections(pyblish.api.InstancePlugin):
+class ValidateMeshShaderConnections(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""Ensure mesh shading engine connections are valid.
In some scenarios Maya keeps connections to multiple shaders even if just
@@ -96,10 +98,12 @@ class ValidateMeshShaderConnections(pyblish.api.InstancePlugin):
label = "Mesh Shader Connections"
actions = [openpype.hosts.maya.api.action.SelectInvalidAction,
RepairAction]
+ optional = True
def process(self, instance):
"""Process all the nodes in the instance 'objectSet'"""
-
+ if not self.is_active(instance.data):
+ return
invalid = self.get_invalid(instance)
if invalid:
diff --git a/openpype/hosts/maya/plugins/publish/validate_mesh_vertices_have_edges.py b/openpype/hosts/maya/plugins/publish/validate_mesh_vertices_have_edges.py
index 71678594443..9275bb31234 100644
--- a/openpype/hosts/maya/plugins/publish/validate_mesh_vertices_have_edges.py
+++ b/openpype/hosts/maya/plugins/publish/validate_mesh_vertices_have_edges.py
@@ -4,10 +4,15 @@
import openpype.hosts.maya.api.action
from openpype.hosts.maya.api.lib import len_flattened
from openpype.pipeline.publish import (
- PublishValidationError, RepairAction, ValidateMeshOrder)
+ PublishValidationError,
+ RepairAction,
+ ValidateMeshOrder,
+ OptionalPyblishPluginMixin
+)
-class ValidateMeshVerticesHaveEdges(pyblish.api.InstancePlugin):
+class ValidateMeshVerticesHaveEdges(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""Validate meshes have only vertices that are connected to edges.
Maya can have invalid geometry with vertices that have no edges or
@@ -32,6 +37,7 @@ class ValidateMeshVerticesHaveEdges(pyblish.api.InstancePlugin):
label = 'Mesh Vertices Have Edges'
actions = [openpype.hosts.maya.api.action.SelectInvalidAction,
RepairAction]
+ optional = True
@classmethod
def repair(cls, instance):
@@ -72,7 +78,8 @@ def get_invalid(cls, instance):
return invalid
def process(self, instance):
-
+ if not self.is_active(instance.data):
+ return
invalid = self.get_invalid(instance)
if invalid:
raise PublishValidationError(
diff --git a/openpype/hosts/maya/plugins/publish/validate_model_content.py b/openpype/hosts/maya/plugins/publish/validate_model_content.py
index 19373efad92..343133f738d 100644
--- a/openpype/hosts/maya/plugins/publish/validate_model_content.py
+++ b/openpype/hosts/maya/plugins/publish/validate_model_content.py
@@ -5,12 +5,14 @@
from openpype.hosts.maya.api import lib
from openpype.pipeline.publish import (
ValidateContentsOrder,
- PublishValidationError
+ PublishValidationError,
+ OptionalPyblishPluginMixin
)
-class ValidateModelContent(pyblish.api.InstancePlugin):
- """Adheres to the content of 'model' family
+class ValidateModelContent(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
+ """Adheres to the content of 'model' product type
- Must have one top group. (configurable)
- Must only contain: transforms, meshes and groups
@@ -24,6 +26,7 @@ class ValidateModelContent(pyblish.api.InstancePlugin):
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
validate_top_group = True
+ optional = False
@classmethod
def get_invalid(cls, instance):
@@ -91,7 +94,8 @@ def _is_visible(node):
return list(invalid)
def process(self, instance):
-
+ if not self.is_active(instance.data):
+ return
invalid = self.get_invalid(instance)
if invalid:
diff --git a/openpype/hosts/maya/plugins/publish/validate_no_default_camera.py b/openpype/hosts/maya/plugins/publish/validate_no_default_camera.py
index f0aa9261f72..62b46125a43 100644
--- a/openpype/hosts/maya/plugins/publish/validate_no_default_camera.py
+++ b/openpype/hosts/maya/plugins/publish/validate_no_default_camera.py
@@ -4,7 +4,8 @@
import openpype.hosts.maya.api.action
from openpype.pipeline.publish import (
ValidateContentsOrder,
- PublishValidationError
+ PublishValidationError,
+ OptionalPyblishPluginMixin
)
@@ -15,7 +16,8 @@ def _as_report_list(values, prefix="- ", suffix="\n"):
return prefix + (suffix + prefix).join(values)
-class ValidateNoDefaultCameras(pyblish.api.InstancePlugin):
+class ValidateNoDefaultCameras(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""Ensure no default (startup) cameras are in the instance.
This might be unnecessary. In the past there were some issues with
@@ -28,6 +30,7 @@ class ValidateNoDefaultCameras(pyblish.api.InstancePlugin):
families = ['camera']
label = "No Default Cameras"
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
+ optional = False
@staticmethod
def get_invalid(instance):
@@ -37,6 +40,8 @@ def get_invalid(instance):
def process(self, instance):
"""Process all the cameras in the instance"""
+ if not self.is_active(instance.data):
+ return
invalid = self.get_invalid(instance)
if invalid:
raise PublishValidationError(
diff --git a/openpype/hosts/maya/plugins/publish/validate_no_namespace.py b/openpype/hosts/maya/plugins/publish/validate_no_namespace.py
index 13eeae58591..59ec4f3cc35 100644
--- a/openpype/hosts/maya/plugins/publish/validate_no_namespace.py
+++ b/openpype/hosts/maya/plugins/publish/validate_no_namespace.py
@@ -4,7 +4,8 @@
from openpype.pipeline.publish import (
RepairAction,
ValidateContentsOrder,
- PublishValidationError
+ PublishValidationError,
+ OptionalPyblishPluginMixin
)
import openpype.hosts.maya.api.action
@@ -24,7 +25,8 @@ def get_namespace(node_name):
return node_name.rpartition(":")[0]
-class ValidateNoNamespace(pyblish.api.InstancePlugin):
+class ValidateNoNamespace(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""Ensure the nodes don't have a namespace"""
order = ValidateContentsOrder
@@ -33,6 +35,7 @@ class ValidateNoNamespace(pyblish.api.InstancePlugin):
label = 'No Namespaces'
actions = [openpype.hosts.maya.api.action.SelectInvalidAction,
RepairAction]
+ optional = False
@staticmethod
def get_invalid(instance):
@@ -41,6 +44,8 @@ def get_invalid(instance):
def process(self, instance):
"""Process all the nodes in the instance"""
+ if not self.is_active(instance.data):
+ return
invalid = self.get_invalid(instance)
if invalid:
diff --git a/openpype/hosts/maya/plugins/publish/validate_no_null_transforms.py b/openpype/hosts/maya/plugins/publish/validate_no_null_transforms.py
index 187135fdf30..acd742e4a05 100644
--- a/openpype/hosts/maya/plugins/publish/validate_no_null_transforms.py
+++ b/openpype/hosts/maya/plugins/publish/validate_no_null_transforms.py
@@ -5,7 +5,8 @@
from openpype.pipeline.publish import (
RepairAction,
ValidateContentsOrder,
- PublishValidationError
+ PublishValidationError,
+ OptionalPyblishPluginMixin
)
@@ -37,7 +38,8 @@ def has_shape_children(node):
return True
-class ValidateNoNullTransforms(pyblish.api.InstancePlugin):
+class ValidateNoNullTransforms(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""Ensure no null transforms are in the scene.
Warning:
@@ -54,6 +56,7 @@ class ValidateNoNullTransforms(pyblish.api.InstancePlugin):
label = 'No Empty/Null Transforms'
actions = [RepairAction,
openpype.hosts.maya.api.action.SelectInvalidAction]
+ optional = False
@staticmethod
def get_invalid(instance):
@@ -70,6 +73,8 @@ def get_invalid(instance):
def process(self, instance):
"""Process all the transform nodes in the instance """
+ if not self.is_active(instance.data):
+ return
invalid = self.get_invalid(instance)
if invalid:
raise PublishValidationError(
diff --git a/openpype/hosts/maya/plugins/publish/validate_no_vraymesh.py b/openpype/hosts/maya/plugins/publish/validate_no_vraymesh.py
index 22fd1edc29b..d6d8fc25424 100644
--- a/openpype/hosts/maya/plugins/publish/validate_no_vraymesh.py
+++ b/openpype/hosts/maya/plugins/publish/validate_no_vraymesh.py
@@ -1,7 +1,9 @@
import pyblish.api
from maya import cmds
-from openpype.pipeline.publish import PublishValidationError
-
+from openpype.pipeline.publish import (
+ PublishValidationError,
+ OptionalPyblishPluginMixin
+)
def _as_report_list(values, prefix="- ", suffix="\n"):
"""Return list as bullet point list for a report"""
@@ -10,15 +12,18 @@ def _as_report_list(values, prefix="- ", suffix="\n"):
return prefix + (suffix + prefix).join(values)
-class ValidateNoVRayMesh(pyblish.api.InstancePlugin):
+class ValidateNoVRayMesh(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""Validate there are no VRayMesh objects in the instance"""
order = pyblish.api.ValidatorOrder
label = 'No V-Ray Proxies (VRayMesh)'
families = ["pointcache"]
+ optional = False
def process(self, instance):
-
+ if not self.is_active(instance.data):
+ return
if not cmds.pluginInfo("vrayformaya", query=True, loaded=True):
return
diff --git a/openpype/hosts/maya/plugins/publish/validate_node_no_ghosting.py b/openpype/hosts/maya/plugins/publish/validate_node_no_ghosting.py
index 0f608dab2c2..dc0e1aba469 100644
--- a/openpype/hosts/maya/plugins/publish/validate_node_no_ghosting.py
+++ b/openpype/hosts/maya/plugins/publish/validate_node_no_ghosting.py
@@ -3,10 +3,14 @@
import pyblish.api
import openpype.hosts.maya.api.action
-from openpype.pipeline.publish import ValidateContentsOrder
+from openpype.pipeline.publish import (
+ ValidateContentsOrder,
+ OptionalPyblishPluginMixin
+)
-class ValidateNodeNoGhosting(pyblish.api.InstancePlugin):
+class ValidateNodeNoGhosting(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""Ensure nodes do not have ghosting enabled.
If one would publish towards a non-Maya format it's likely that stats
@@ -23,6 +27,7 @@ class ValidateNodeNoGhosting(pyblish.api.InstancePlugin):
families = ['model', 'rig']
label = "No Ghosting"
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
+ optional = False
_attributes = {'ghosting': 0}
@@ -46,7 +51,8 @@ def get_invalid(cls, instance):
return invalid
def process(self, instance):
-
+ if not self.is_active(instance.data):
+ return
invalid = self.get_invalid(instance)
if invalid:
diff --git a/openpype/hosts/maya/plugins/publish/validate_plugin_path_attributes.py b/openpype/hosts/maya/plugins/publish/validate_plugin_path_attributes.py
index cb5c68e4ab4..3382e2ab338 100644
--- a/openpype/hosts/maya/plugins/publish/validate_plugin_path_attributes.py
+++ b/openpype/hosts/maya/plugins/publish/validate_plugin_path_attributes.py
@@ -8,11 +8,13 @@
from openpype.hosts.maya.api.action import SelectInvalidAction
from openpype.pipeline.publish import (
ValidateContentsOrder,
- PublishValidationError
+ PublishValidationError,
+ OptionalPyblishPluginMixin
)
-class ValidatePluginPathAttributes(pyblish.api.InstancePlugin):
+class ValidatePluginPathAttributes(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""
Validate plug-in path attributes point to existing file paths.
"""
@@ -22,6 +24,7 @@ class ValidatePluginPathAttributes(pyblish.api.InstancePlugin):
families = ["workfile"]
label = "Plug-in Path Attributes"
actions = [SelectInvalidAction]
+ optional = False
# Attributes are defined in project settings
attribute = []
@@ -56,6 +59,8 @@ def get_invalid(cls, instance):
def process(self, instance):
"""Process all directories Set as Filenames in Non-Maya Nodes"""
+ if not self.is_active(instance.data):
+ return
invalid = self.get_invalid(instance)
if invalid:
raise PublishValidationError(
diff --git a/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py b/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py
index 030e41ca1f2..c0d2472a67b 100644
--- a/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py
+++ b/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py
@@ -5,10 +5,15 @@
from maya import cmds
from openpype.pipeline.publish import (
- PublishValidationError, RepairAction, ValidateContentsOrder)
+ PublishValidationError,
+ RepairAction,
+ ValidateContentsOrder,
+ OptionalPyblishPluginMixin
+)
-class ValidateRenderImageRule(pyblish.api.InstancePlugin):
+class ValidateRenderImageRule(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""Validates Maya Workpace "images" file rule matches project settings.
This validates against the configured default render image folder:
@@ -22,9 +27,11 @@ class ValidateRenderImageRule(pyblish.api.InstancePlugin):
hosts = ["maya"]
families = ["renderlayer"]
actions = [RepairAction]
+ optional = False
def process(self, instance):
-
+ if not self.is_active(instance.data):
+ return
required_images_rule = os.path.normpath(
self.get_default_render_image_folder(instance)
)
diff --git a/openpype/hosts/maya/plugins/publish/validate_render_no_default_cameras.py b/openpype/hosts/maya/plugins/publish/validate_render_no_default_cameras.py
index 9d4410186b1..e7957851d65 100644
--- a/openpype/hosts/maya/plugins/publish/validate_render_no_default_cameras.py
+++ b/openpype/hosts/maya/plugins/publish/validate_render_no_default_cameras.py
@@ -6,10 +6,12 @@
from openpype.pipeline.publish import (
ValidateContentsOrder,
PublishValidationError,
+ OptionalPyblishPluginMixin
)
-class ValidateRenderNoDefaultCameras(pyblish.api.InstancePlugin):
+class ValidateRenderNoDefaultCameras(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""Ensure no default (startup) cameras are to be rendered."""
order = ValidateContentsOrder
@@ -17,6 +19,7 @@ class ValidateRenderNoDefaultCameras(pyblish.api.InstancePlugin):
families = ['renderlayer']
label = "No Default Cameras Renderable"
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
+ optional = False
@staticmethod
def get_invalid(instance):
@@ -32,6 +35,8 @@ def get_invalid(instance):
def process(self, instance):
"""Process all the cameras in the instance"""
+ if not self.is_active(instance.data):
+ return
invalid = self.get_invalid(instance)
if invalid:
raise PublishValidationError(
diff --git a/openpype/hosts/maya/plugins/publish/validate_render_single_camera.py b/openpype/hosts/maya/plugins/publish/validate_render_single_camera.py
index 2c0d6041756..706704099d4 100644
--- a/openpype/hosts/maya/plugins/publish/validate_render_single_camera.py
+++ b/openpype/hosts/maya/plugins/publish/validate_render_single_camera.py
@@ -7,11 +7,13 @@
from openpype.hosts.maya.api.lib_rendersettings import RenderSettings
from openpype.pipeline.publish import (
ValidateContentsOrder,
- PublishValidationError
+ PublishValidationError,
+ OptionalPyblishPluginMixin
)
-class ValidateRenderSingleCamera(pyblish.api.InstancePlugin):
+class ValidateRenderSingleCamera(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""Validate renderable camera count for layer and token.
Pipeline is supporting multiple renderable cameras per layer, but image
@@ -24,11 +26,14 @@ class ValidateRenderSingleCamera(pyblish.api.InstancePlugin):
families = ["renderlayer",
"vrayscene"]
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
+ optional = False
R_CAMERA_TOKEN = re.compile(r'%c|', re.IGNORECASE)
def process(self, instance):
"""Process all the cameras in the instance"""
+ if not self.is_active(instance.data):
+ return
invalid = self.get_invalid(instance)
if invalid:
raise PublishValidationError("Invalid cameras for render.")
diff --git a/openpype/hosts/maya/plugins/publish/validate_renderlayer_aovs.py b/openpype/hosts/maya/plugins/publish/validate_renderlayer_aovs.py
index f8de983e060..3e7f8d9591d 100644
--- a/openpype/hosts/maya/plugins/publish/validate_renderlayer_aovs.py
+++ b/openpype/hosts/maya/plugins/publish/validate_renderlayer_aovs.py
@@ -3,13 +3,17 @@
import openpype.hosts.maya.api.action
from openpype.client import get_subset_by_name
from openpype.pipeline import legacy_io
-from openpype.pipeline.publish import PublishValidationError
+from openpype.pipeline.publish import (
+ PublishValidationError,
+ OptionalPyblishPluginMixin
+)
-class ValidateRenderLayerAOVs(pyblish.api.InstancePlugin):
+class ValidateRenderLayerAOVs(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""Validate created AOVs / RenderElement is registered in the database
- Each render element is registered as a subset which is formatted based on
+ Each render element is registered as a product which is formatted based on
the render layer and the render element, example:
.
@@ -27,8 +31,12 @@ class ValidateRenderLayerAOVs(pyblish.api.InstancePlugin):
hosts = ["maya"]
families = ["renderlayer"]
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
+ optional = False
def process(self, instance):
+ if not self.is_active(instance.data):
+ return
+
invalid = self.get_invalid(instance)
if invalid:
raise PublishValidationError(
diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_contents.py b/openpype/hosts/maya/plugins/publish/validate_rig_contents.py
index 106b4024e28..9c679013ec3 100644
--- a/openpype/hosts/maya/plugins/publish/validate_rig_contents.py
+++ b/openpype/hosts/maya/plugins/publish/validate_rig_contents.py
@@ -3,11 +3,13 @@
import openpype.hosts.maya.api.action
from openpype.pipeline.publish import (
PublishValidationError,
- ValidateContentsOrder
+ ValidateContentsOrder,
+ OptionalPyblishPluginMixin
)
-class ValidateRigContents(pyblish.api.InstancePlugin):
+class ValidateRigContents(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""Ensure rig contains pipeline-critical content
Every rig must contain at least two object sets:
@@ -21,11 +23,14 @@ class ValidateRigContents(pyblish.api.InstancePlugin):
hosts = ["maya"]
families = ["rig"]
action = [openpype.hosts.maya.api.action.SelectInvalidAction]
+ optional = True
accepted_output = ["mesh", "transform"]
accepted_controllers = ["transform"]
def process(self, instance):
+ if not self.is_active(instance.data):
+ return
invalid = self.get_invalid(instance)
if invalid:
raise PublishValidationError(
@@ -213,6 +218,7 @@ class ValidateSkeletonRigContents(ValidateRigContents):
label = "Skeleton Rig Contents"
hosts = ["maya"]
families = ["rig.fbx"]
+ optional = True
@classmethod
def get_invalid(cls, instance):
diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py b/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py
index 82248c57b3f..0cf320045c9 100644
--- a/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py
+++ b/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py
@@ -5,13 +5,15 @@
from openpype.pipeline.publish import (
ValidateContentsOrder,
RepairAction,
- PublishValidationError
+ PublishValidationError,
+ OptionalPyblishPluginMixin
)
import openpype.hosts.maya.api.action
from openpype.hosts.maya.api.lib import undo_chunk
-class ValidateRigControllers(pyblish.api.InstancePlugin):
+class ValidateRigControllers(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""Validate rig controllers.
Controls must have the transformation attributes on their default
@@ -33,6 +35,7 @@ class ValidateRigControllers(pyblish.api.InstancePlugin):
label = "Rig Controllers"
hosts = ["maya"]
families = ["rig"]
+ optional = True
actions = [RepairAction,
openpype.hosts.maya.api.action.SelectInvalidAction]
@@ -50,6 +53,9 @@ class ValidateRigControllers(pyblish.api.InstancePlugin):
}
def process(self, instance):
+ if not self.is_active(instance.data):
+ return
+
invalid = self.get_invalid(instance)
if invalid:
raise PublishValidationError(
diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_controllers_arnold_attributes.py b/openpype/hosts/maya/plugins/publish/validate_rig_controllers_arnold_attributes.py
index 03f6a5f1ab6..c32ca6a9262 100644
--- a/openpype/hosts/maya/plugins/publish/validate_rig_controllers_arnold_attributes.py
+++ b/openpype/hosts/maya/plugins/publish/validate_rig_controllers_arnold_attributes.py
@@ -5,14 +5,16 @@
from openpype.pipeline.publish import (
ValidateContentsOrder,
RepairAction,
- PublishValidationError
+ PublishValidationError,
+ OptionalPyblishPluginMixin
)
from openpype.hosts.maya.api import lib
import openpype.hosts.maya.api.action
-class ValidateRigControllersArnoldAttributes(pyblish.api.InstancePlugin):
+class ValidateRigControllersArnoldAttributes(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""Validate rig control curves have no keyable arnold attributes.
The Arnold plug-in will create curve attributes like:
@@ -35,6 +37,7 @@ class ValidateRigControllersArnoldAttributes(pyblish.api.InstancePlugin):
label = "Rig Controllers (Arnold Attributes)"
hosts = ["maya"]
families = ["rig"]
+ optional = False
actions = [RepairAction,
openpype.hosts.maya.api.action.SelectInvalidAction]
@@ -48,6 +51,9 @@ class ValidateRigControllersArnoldAttributes(pyblish.api.InstancePlugin):
]
def process(self, instance):
+ if not self.is_active(instance.data):
+ return
+
invalid = self.get_invalid(instance)
if invalid:
raise PublishValidationError('{} failed, see log '
diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_joints_hidden.py b/openpype/hosts/maya/plugins/publish/validate_rig_joints_hidden.py
index 2bb5036f8ba..4f9fc6577c9 100644
--- a/openpype/hosts/maya/plugins/publish/validate_rig_joints_hidden.py
+++ b/openpype/hosts/maya/plugins/publish/validate_rig_joints_hidden.py
@@ -7,11 +7,13 @@
from openpype.pipeline.publish import (
RepairAction,
ValidateContentsOrder,
- PublishValidationError
+ PublishValidationError,
+ OptionalPyblishPluginMixin
)
-class ValidateRigJointsHidden(pyblish.api.InstancePlugin):
+class ValidateRigJointsHidden(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""Validate all joints are hidden visually.
This includes being hidden:
@@ -28,6 +30,7 @@ class ValidateRigJointsHidden(pyblish.api.InstancePlugin):
label = "Joints Hidden"
actions = [openpype.hosts.maya.api.action.SelectInvalidAction,
RepairAction]
+ optional = True
@staticmethod
def get_invalid(instance):
@@ -36,6 +39,8 @@ def get_invalid(instance):
def process(self, instance):
"""Process all the nodes in the instance 'objectSet'"""
+ if not self.is_active(instance.data):
+ return
invalid = self.get_invalid(instance)
if invalid:
diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py b/openpype/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py
index 80ac0f27e69..1a43ae810ad 100644
--- a/openpype/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py
+++ b/openpype/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py
@@ -7,11 +7,13 @@
from openpype.pipeline.publish import (
RepairAction,
ValidateContentsOrder,
- PublishValidationError
+ PublishValidationError,
+ OptionalPyblishPluginMixin
)
-class ValidateRigOutSetNodeIds(pyblish.api.InstancePlugin):
+class ValidateRigOutSetNodeIds(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""Validate if deformed shapes have related IDs to the original shapes.
When a deformer is applied in the scene on a referenced mesh that already
@@ -30,10 +32,12 @@ class ValidateRigOutSetNodeIds(pyblish.api.InstancePlugin):
RepairAction
]
allow_history_only = False
+ optional = False
def process(self, instance):
"""Process all meshes"""
-
+ if not self.is_active(instance.data):
+ return
# Ensure all nodes have a cbId and a related ID to the original shapes
# if a deformer has been created on the shape
invalid = self.get_invalid(instance)
@@ -114,6 +118,7 @@ class ValidateSkeletonRigOutSetNodeIds(ValidateRigOutSetNodeIds):
families = ["rig.fbx"]
hosts = ['maya']
label = 'Skeleton Rig Out Set Node Ids'
+ optional = False
@classmethod
def get_node(cls, instance):
diff --git a/openpype/hosts/maya/plugins/publish/validate_shape_render_stats.py b/openpype/hosts/maya/plugins/publish/validate_shape_render_stats.py
index f58c0aaf81d..a494a6cf45c 100644
--- a/openpype/hosts/maya/plugins/publish/validate_shape_render_stats.py
+++ b/openpype/hosts/maya/plugins/publish/validate_shape_render_stats.py
@@ -6,10 +6,12 @@
from openpype.pipeline.publish import (
RepairAction,
ValidateMeshOrder,
+ OptionalPyblishPluginMixin
)
-class ValidateShapeRenderStats(pyblish.api.Validator):
+class ValidateShapeRenderStats(pyblish.api.Validator,
+ OptionalPyblishPluginMixin):
"""Ensure all render stats are set to the default values."""
order = ValidateMeshOrder
@@ -18,6 +20,7 @@ class ValidateShapeRenderStats(pyblish.api.Validator):
label = 'Shape Default Render Stats'
actions = [openpype.hosts.maya.api.action.SelectInvalidAction,
RepairAction]
+ optional = True
defaults = {'castsShadows': 1,
'receiveShadows': 1,
@@ -46,7 +49,8 @@ def get_invalid(cls, instance):
return invalid
def process(self, instance):
-
+ if not self.is_active(instance.data):
+ return
invalid = self.get_invalid(instance)
if invalid:
diff --git a/openpype/hosts/maya/plugins/publish/validate_shape_zero.py b/openpype/hosts/maya/plugins/publish/validate_shape_zero.py
index c7af6a60dbe..7850407e487 100644
--- a/openpype/hosts/maya/plugins/publish/validate_shape_zero.py
+++ b/openpype/hosts/maya/plugins/publish/validate_shape_zero.py
@@ -7,11 +7,13 @@
from openpype.pipeline.publish import (
ValidateContentsOrder,
RepairAction,
- PublishValidationError
+ PublishValidationError,
+ OptionalPyblishPluginMixin
)
-class ValidateShapeZero(pyblish.api.Validator):
+class ValidateShapeZero(pyblish.api.Validator,
+ OptionalPyblishPluginMixin):
"""Shape components may not have any "tweak" values
To solve this issue, try freezing the shapes.
@@ -26,6 +28,7 @@ class ValidateShapeZero(pyblish.api.Validator):
openpype.hosts.maya.api.action.SelectInvalidAction,
RepairAction
]
+ optional = True
@staticmethod
def get_invalid(instance):
@@ -65,6 +68,8 @@ def repair(cls, instance):
def process(self, instance):
"""Process all the nodes in the instance "objectSet"""
+ if not self.is_active(instance.data):
+ return
invalid = self.get_invalid(instance)
if invalid:
diff --git a/openpype/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py b/openpype/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py
index 9084374c769..eaf74cb810a 100644
--- a/openpype/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py
+++ b/openpype/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py
@@ -4,20 +4,25 @@
from openpype.pipeline.publish import (
ValidateContentsOrder,
PublishXmlValidationError,
+ OptionalPyblishPluginMixin
)
from maya import cmds
-class ValidateSkeletalMeshHierarchy(pyblish.api.InstancePlugin):
+class ValidateSkeletalMeshHierarchy(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""Validates that nodes has common root."""
order = ValidateContentsOrder
hosts = ["maya"]
families = ["skeletalMesh"]
label = "Skeletal Mesh Top Node"
+ optional = False
def process(self, instance):
+ if not self.is_active(instance.data):
+ return
geo = instance.data.get("geometry")
joints = instance.data.get("joints")
diff --git a/openpype/hosts/maya/plugins/publish/validate_skinCluster_deformer_set.py b/openpype/hosts/maya/plugins/publish/validate_skinCluster_deformer_set.py
index b45d2b120af..1b70a9f38ab 100644
--- a/openpype/hosts/maya/plugins/publish/validate_skinCluster_deformer_set.py
+++ b/openpype/hosts/maya/plugins/publish/validate_skinCluster_deformer_set.py
@@ -3,10 +3,14 @@
import pyblish.api
import openpype.hosts.maya.api.action
-from openpype.pipeline.publish import ValidateContentsOrder
+from openpype.pipeline.publish import (
+ ValidateContentsOrder,
+ OptionalPyblishPluginMixin
+)
-class ValidateSkinclusterDeformerSet(pyblish.api.InstancePlugin):
+class ValidateSkinclusterDeformerSet(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""Validate skinClusters on meshes have valid member relationships.
In rare cases it can happen that a mesh has a skinCluster in its history
@@ -20,9 +24,12 @@ class ValidateSkinclusterDeformerSet(pyblish.api.InstancePlugin):
families = ['fbx']
label = "Skincluster Deformer Relationships"
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
+ optional = False
def process(self, instance):
"""Process all the transform nodes in the instance"""
+ if not self.is_active(instance.data):
+ return
invalid = self.get_invalid(instance)
if invalid:
diff --git a/openpype/hosts/maya/plugins/publish/validate_step_size.py b/openpype/hosts/maya/plugins/publish/validate_step_size.py
index 493a6ee65c6..16c2b6b321c 100644
--- a/openpype/hosts/maya/plugins/publish/validate_step_size.py
+++ b/openpype/hosts/maya/plugins/publish/validate_step_size.py
@@ -3,11 +3,13 @@
import openpype.hosts.maya.api.action
from openpype.pipeline.publish import (
PublishValidationError,
- ValidateContentsOrder
+ ValidateContentsOrder,
+ OptionalPyblishPluginMixin
)
-class ValidateStepSize(pyblish.api.InstancePlugin):
+class ValidateStepSize(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""Validates the step size for the instance is in a valid range.
For example the `step` size should never be lower or equal to zero.
@@ -20,7 +22,7 @@ class ValidateStepSize(pyblish.api.InstancePlugin):
'pointcache',
'animation']
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
-
+ optional = False
MIN = 0.01
MAX = 1.0
@@ -40,7 +42,8 @@ def get_invalid(cls, instance):
return []
def process(self, instance):
-
+ if not self.is_active(instance.data):
+ return
invalid = self.get_invalid(instance)
if invalid:
raise PublishValidationError(
diff --git a/openpype/hosts/maya/plugins/publish/validate_transform_zero.py b/openpype/hosts/maya/plugins/publish/validate_transform_zero.py
index 906ff17ec9a..6bdc2d2a598 100644
--- a/openpype/hosts/maya/plugins/publish/validate_transform_zero.py
+++ b/openpype/hosts/maya/plugins/publish/validate_transform_zero.py
@@ -5,11 +5,13 @@
import openpype.hosts.maya.api.action
from openpype.pipeline.publish import (
ValidateContentsOrder,
- PublishValidationError
+ PublishValidationError,
+ OptionalPyblishPluginMixin
)
-class ValidateTransformZero(pyblish.api.Validator):
+class ValidateTransformZero(pyblish.api.Validator,
+ OptionalPyblishPluginMixin):
"""Transforms can't have any values
To solve this issue, try freezing the transforms. So long
@@ -29,6 +31,7 @@ class ValidateTransformZero(pyblish.api.Validator):
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0]
_tolerance = 1e-30
+ optional = True
@classmethod
def get_invalid(cls, instance):
@@ -62,7 +65,8 @@ def get_invalid(cls, instance):
def process(self, instance):
"""Process all the nodes in the instance "objectSet"""
-
+ if not self.is_active(instance.data):
+ return
invalid = self.get_invalid(instance)
if invalid:
diff --git a/openpype/hosts/maya/plugins/publish/validate_unique_names.py b/openpype/hosts/maya/plugins/publish/validate_unique_names.py
index 05776ee0f37..4f69ebbfe6e 100644
--- a/openpype/hosts/maya/plugins/publish/validate_unique_names.py
+++ b/openpype/hosts/maya/plugins/publish/validate_unique_names.py
@@ -2,10 +2,14 @@
import pyblish.api
import openpype.hosts.maya.api.action
-from openpype.pipeline.publish import ValidateContentsOrder
+from openpype.pipeline.publish import (
+ ValidateContentsOrder,
+ OptionalPyblishPluginMixin
+)
-class ValidateUniqueNames(pyblish.api.Validator):
+class ValidateUniqueNames(pyblish.api.Validator,
+ OptionalPyblishPluginMixin):
"""transform names should be unique
ie: using cmds.ls(someNodeName) should always return shortname
@@ -17,6 +21,7 @@ class ValidateUniqueNames(pyblish.api.Validator):
families = ["model"]
label = "Unique transform name"
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
+ optional = True
@staticmethod
def get_invalid(instance):
@@ -32,7 +37,8 @@ def get_invalid(instance):
def process(self, instance):
"""Process all the nodes in the instance "objectSet"""
-
+ if not self.is_active(instance.data):
+ return
invalid = self.get_invalid(instance)
if invalid:
raise ValueError("Nodes found with none unique names. "
diff --git a/openpype/hosts/maya/plugins/publish/validate_unreal_mesh_triangulated.py b/openpype/hosts/maya/plugins/publish/validate_unreal_mesh_triangulated.py
index e78962bf97c..70435bfb3dc 100644
--- a/openpype/hosts/maya/plugins/publish/validate_unreal_mesh_triangulated.py
+++ b/openpype/hosts/maya/plugins/publish/validate_unreal_mesh_triangulated.py
@@ -3,11 +3,15 @@
from maya import cmds
import pyblish.api
-from openpype.pipeline.publish import ValidateMeshOrder
+from openpype.pipeline.publish import (
+ ValidateMeshOrder,
+ OptionalPyblishPluginMixin
+)
import openpype.hosts.maya.api.action
-class ValidateUnrealMeshTriangulated(pyblish.api.InstancePlugin):
+class ValidateUnrealMeshTriangulated(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""Validate if mesh is made of triangles for Unreal Engine"""
order = ValidateMeshOrder
@@ -30,6 +34,8 @@ def get_invalid(cls, instance):
return invalid
def process(self, instance):
+ if not self.is_active(instance.data):
+ return
invalid = self.get_invalid(instance)
assert len(invalid) == 0, (
"Found meshes without triangles")
diff --git a/openpype/hosts/maya/plugins/publish/validate_visible_only.py b/openpype/hosts/maya/plugins/publish/validate_visible_only.py
index e72782e552e..4a17e567931 100644
--- a/openpype/hosts/maya/plugins/publish/validate_visible_only.py
+++ b/openpype/hosts/maya/plugins/publish/validate_visible_only.py
@@ -4,11 +4,13 @@
import openpype.hosts.maya.api.action
from openpype.pipeline.publish import (
ValidateContentsOrder,
- PublishValidationError
+ PublishValidationError,
+ OptionalPyblishPluginMixin
)
-class ValidateAlembicVisibleOnly(pyblish.api.InstancePlugin):
+class ValidateAlembicVisibleOnly(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""Validates at least a single node is visible in frame range.
This validation only validates if the `visibleOnly` flag is enabled
@@ -20,9 +22,11 @@ class ValidateAlembicVisibleOnly(pyblish.api.InstancePlugin):
hosts = ["maya"]
families = ["pointcache", "animation"]
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
+ optional = False
def process(self, instance):
-
+ if not self.is_active(instance.data):
+ return
if not instance.data.get("visibleOnly", False):
self.log.debug("Visible only is disabled. Validation skipped..")
return
diff --git a/openpype/hosts/maya/plugins/publish/validate_vray_distributed_rendering.py b/openpype/hosts/maya/plugins/publish/validate_vray_distributed_rendering.py
index 14571203ea6..a11ebb5b8ee 100644
--- a/openpype/hosts/maya/plugins/publish/validate_vray_distributed_rendering.py
+++ b/openpype/hosts/maya/plugins/publish/validate_vray_distributed_rendering.py
@@ -3,10 +3,15 @@
from openpype.hosts.maya.api import lib
from openpype.pipeline.publish import (
- PublishValidationError, RepairAction, ValidateContentsOrder)
+ PublishValidationError,
+ RepairAction,
+ ValidateContentsOrder,
+ OptionalPyblishPluginMixin
+)
-class ValidateVRayDistributedRendering(pyblish.api.InstancePlugin):
+class ValidateVRayDistributedRendering(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""Validate V-Ray Distributed Rendering is ignored in batch mode.
Whenever Distributed Rendering is enabled for V-Ray in the render settings
@@ -20,13 +25,15 @@ class ValidateVRayDistributedRendering(pyblish.api.InstancePlugin):
label = "VRay Distributed Rendering"
families = ["renderlayer"]
actions = [RepairAction]
+ optional = False
# V-Ray attribute names
enabled_attr = "vraySettings.sys_distributed_rendering_on"
ignored_attr = "vraySettings.sys_distributed_rendering_ignore_batch"
def process(self, instance):
-
+ if not self.is_active(instance.data):
+ return
if instance.data.get("renderer") != "vray":
# If not V-Ray ignore..
return
diff --git a/openpype/hosts/maya/plugins/publish/validate_vray_referenced_aovs.py b/openpype/hosts/maya/plugins/publish/validate_vray_referenced_aovs.py
index 39c721e7175..7cb4fc53195 100644
--- a/openpype/hosts/maya/plugins/publish/validate_vray_referenced_aovs.py
+++ b/openpype/hosts/maya/plugins/publish/validate_vray_referenced_aovs.py
@@ -4,10 +4,14 @@
import types
from maya import cmds
-from openpype.pipeline.publish import RepairContextAction
+from openpype.pipeline.publish import (
+ RepairContextAction,
+ OptionalPyblishPluginMixin
+)
-class ValidateVrayReferencedAOVs(pyblish.api.InstancePlugin):
+class ValidateVrayReferencedAOVs(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""Validate whether the V-Ray Render Elements (AOVs) include references.
This will check if there are AOVs pulled from references. If
@@ -21,9 +25,12 @@ class ValidateVrayReferencedAOVs(pyblish.api.InstancePlugin):
hosts = ['maya']
families = ['renderlayer']
actions = [RepairContextAction]
+ optional = False
def process(self, instance):
"""Plugin main entry point."""
+ if not self.is_active(instance.data):
+ return
if instance.data.get("renderer") != "vray":
# If not V-Ray ignore..
return
diff --git a/openpype/hosts/maya/plugins/publish/validate_vray_translator_settings.py b/openpype/hosts/maya/plugins/publish/validate_vray_translator_settings.py
index 4474f08ba44..1095eda5f49 100644
--- a/openpype/hosts/maya/plugins/publish/validate_vray_translator_settings.py
+++ b/openpype/hosts/maya/plugins/publish/validate_vray_translator_settings.py
@@ -5,22 +5,27 @@
context_plugin_should_run,
RepairContextAction,
ValidateContentsOrder,
- PublishValidationError
+ PublishValidationError,
+ OptionalPyblishPluginMixin
)
from maya import cmds
-class ValidateVRayTranslatorEnabled(pyblish.api.ContextPlugin):
+class ValidateVRayTranslatorEnabled(pyblish.api.ContextPlugin,
+ OptionalPyblishPluginMixin):
"""Validate VRay Translator settings for extracting vrscenes."""
order = ValidateContentsOrder
label = "VRay Translator Settings"
families = ["vrayscene_layer"]
actions = [RepairContextAction]
+ optional = False
def process(self, context):
"""Plugin entry point."""
+ if not self.is_active(context.data):
+ return
# Workaround bug pyblish-base#250
if not context_plugin_should_run(self, context):
return
diff --git a/openpype/hosts/maya/plugins/publish/validate_vrayproxy.py b/openpype/hosts/maya/plugins/publish/validate_vrayproxy.py
index a106b970b47..ecf2a571725 100644
--- a/openpype/hosts/maya/plugins/publish/validate_vrayproxy.py
+++ b/openpype/hosts/maya/plugins/publish/validate_vrayproxy.py
@@ -1,18 +1,22 @@
import pyblish.api
from openpype.pipeline import KnownPublishError
+from openpype.pipeline.publish import OptionalPyblishPluginMixin
-class ValidateVrayProxy(pyblish.api.InstancePlugin):
+class ValidateVrayProxy(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
order = pyblish.api.ValidatorOrder
label = "VRay Proxy Settings"
hosts = ["maya"]
families = ["vrayproxy"]
+ optional = False
def process(self, instance):
data = instance.data
-
+ if not self.is_active(data):
+ return
if not data["setMembers"]:
raise KnownPublishError(
"'%s' is empty! This is a bug" % instance.name
diff --git a/openpype/hosts/maya/plugins/publish/validate_vrayproxy_members.py b/openpype/hosts/maya/plugins/publish/validate_vrayproxy_members.py
index 7b726de3a80..6c1edd72094 100644
--- a/openpype/hosts/maya/plugins/publish/validate_vrayproxy_members.py
+++ b/openpype/hosts/maya/plugins/publish/validate_vrayproxy_members.py
@@ -4,12 +4,13 @@
import openpype.hosts.maya.api.action
from openpype.pipeline.publish import (
- PublishValidationError
+ PublishValidationError,
+ OptionalPyblishPluginMixin
)
-
-class ValidateVrayProxyMembers(pyblish.api.InstancePlugin):
+class ValidateVrayProxyMembers(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""Validate whether the V-Ray Proxy instance has shape members"""
order = pyblish.api.ValidatorOrder
@@ -17,9 +18,11 @@ class ValidateVrayProxyMembers(pyblish.api.InstancePlugin):
hosts = ['maya']
families = ['vrayproxy']
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
+ optional = False
def process(self, instance):
-
+ if not self.is_active(instance.data):
+ return
invalid = self.get_invalid(instance)
if invalid:
diff --git a/openpype/hosts/maya/plugins/publish/validate_yeti_renderscript_callbacks.py b/openpype/hosts/maya/plugins/publish/validate_yeti_renderscript_callbacks.py
index a8085418e7f..40818ef7d2d 100644
--- a/openpype/hosts/maya/plugins/publish/validate_yeti_renderscript_callbacks.py
+++ b/openpype/hosts/maya/plugins/publish/validate_yeti_renderscript_callbacks.py
@@ -1,10 +1,14 @@
from maya import cmds
import pyblish.api
-from openpype.pipeline.publish import ValidateContentsOrder
+from openpype.pipeline.publish import (
+ ValidateContentsOrder,
+ OptionalPyblishPluginMixin
+)
-class ValidateYetiRenderScriptCallbacks(pyblish.api.InstancePlugin):
+class ValidateYetiRenderScriptCallbacks(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""Check if the render script callbacks will be used during the rendering
In order to ensure the render tasks are executed properly we need to check
@@ -24,6 +28,7 @@ class ValidateYetiRenderScriptCallbacks(pyblish.api.InstancePlugin):
label = "Yeti Render Script Callbacks"
hosts = ["maya"]
families = ["renderlayer"]
+ optional = False
# Settings per renderer
callbacks = {
@@ -37,7 +42,8 @@ class ValidateYetiRenderScriptCallbacks(pyblish.api.InstancePlugin):
}
def process(self, instance):
-
+ if not self.is_active(instance.data):
+ return
invalid = self.get_invalid(instance)
if invalid:
raise ValueError("Invalid render callbacks found for '%s'!"
diff --git a/openpype/hosts/maya/plugins/publish/validate_yeti_rig_cache_state.py b/openpype/hosts/maya/plugins/publish/validate_yeti_rig_cache_state.py
index 2b7249ad943..ece6f7f5912 100644
--- a/openpype/hosts/maya/plugins/publish/validate_yeti_rig_cache_state.py
+++ b/openpype/hosts/maya/plugins/publish/validate_yeti_rig_cache_state.py
@@ -3,12 +3,13 @@
import openpype.hosts.maya.api.action
from openpype.pipeline.publish import (
RepairAction,
- PublishValidationError
+ PublishValidationError,
+ OptionalPyblishPluginMixin
)
-
-class ValidateYetiRigCacheState(pyblish.api.InstancePlugin):
+class ValidateYetiRigCacheState(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""Validate the I/O attributes of the node
Every pgYetiMaya cache node per instance should have:
@@ -23,8 +24,11 @@ class ValidateYetiRigCacheState(pyblish.api.InstancePlugin):
families = ["yetiRig"]
actions = [RepairAction,
openpype.hosts.maya.api.action.SelectInvalidAction]
+ optional = False
def process(self, instance):
+ if not self.is_active(instance.data):
+ return
invalid = self.get_invalid(instance)
if invalid:
raise PublishValidationError("Nodes have incorrect I/O settings")
diff --git a/openpype/hosts/maya/plugins/publish/validate_yeti_rig_input_in_instance.py b/openpype/hosts/maya/plugins/publish/validate_yeti_rig_input_in_instance.py
index 50a27589ad8..4a25485365c 100644
--- a/openpype/hosts/maya/plugins/publish/validate_yeti_rig_input_in_instance.py
+++ b/openpype/hosts/maya/plugins/publish/validate_yeti_rig_input_in_instance.py
@@ -5,11 +5,13 @@
import openpype.hosts.maya.api.action
from openpype.pipeline.publish import (
ValidateContentsOrder,
- PublishValidationError
+ PublishValidationError,
+ OptionalPyblishPluginMixin
)
-class ValidateYetiRigInputShapesInInstance(pyblish.api.Validator):
+class ValidateYetiRigInputShapesInInstance(pyblish.api.Validator,
+ OptionalPyblishPluginMixin):
"""Validate if all input nodes are part of the instance's hierarchy"""
order = ValidateContentsOrder
@@ -17,9 +19,11 @@ class ValidateYetiRigInputShapesInInstance(pyblish.api.Validator):
families = ["yetiRig"]
label = "Yeti Rig Input Shapes In Instance"
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
+ optional = False
def process(self, instance):
-
+ if not self.is_active(instance.data):
+ return
invalid = self.get_invalid(instance)
if invalid:
raise PublishValidationError("Yeti Rig has invalid input meshes")
diff --git a/openpype/hosts/maya/plugins/publish/validate_yeti_rig_settings.py b/openpype/hosts/maya/plugins/publish/validate_yeti_rig_settings.py
index 455bf5291a7..fec45f5b0e6 100644
--- a/openpype/hosts/maya/plugins/publish/validate_yeti_rig_settings.py
+++ b/openpype/hosts/maya/plugins/publish/validate_yeti_rig_settings.py
@@ -1,9 +1,13 @@
import pyblish.api
-from openpype.pipeline.publish import PublishValidationError
+from openpype.pipeline.publish import (
+ PublishValidationError,
+ OptionalPyblishPluginMixin
+)
-class ValidateYetiRigSettings(pyblish.api.InstancePlugin):
+class ValidateYetiRigSettings(pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin):
"""Validate Yeti Rig Settings have collected input connections.
The input connections are collected for the nodes in the `input_SET`.
@@ -15,9 +19,11 @@ class ValidateYetiRigSettings(pyblish.api.InstancePlugin):
order = pyblish.api.ValidatorOrder
label = "Yeti Rig Settings"
families = ["yetiRig"]
+ optional = False
def process(self, instance):
-
+ if not self.is_active(instance.data):
+ return
invalid = self.get_invalid(instance)
if invalid:
raise PublishValidationError(
diff --git a/openpype/hosts/nuke/api/utils.py b/openpype/hosts/nuke/api/utils.py
index a7df1dee71d..4220f007354 100644
--- a/openpype/hosts/nuke/api/utils.py
+++ b/openpype/hosts/nuke/api/utils.py
@@ -1,4 +1,6 @@
import os
+import re
+
import nuke
from openpype import resources
@@ -103,9 +105,8 @@ def colorspace_exists_on_node(node, colorspace_name):
except ValueError:
# knob is not available on input node
return False
- all_clrs = get_colorspace_list(colorspace_knob)
- return colorspace_name in all_clrs
+ return colorspace_name in get_colorspace_list(colorspace_knob)
def get_colorspace_list(colorspace_knob):
@@ -117,19 +118,22 @@ def get_colorspace_list(colorspace_knob):
Returns:
list: list of strings names of profiles
"""
-
- all_clrs = list(colorspace_knob.values())
- reduced_clrs = []
-
- if not colorspace_knob.getFlag(nuke.STRIP_CASCADE_PREFIX):
- return all_clrs
-
- # strip colorspace with nested path
- for clrs in all_clrs:
- clrs = clrs.split('/')[-1]
- reduced_clrs.append(clrs)
-
- return reduced_clrs
+ results = []
+
+ # This pattern is to match with roles which uses an indentation and
+ # parentheses with original colorspace. The value returned from the
+ # colorspace is the string before the indentation, so we'll need to
+ # convert the values to match with value returned from the knob,
+ # ei. knob.value().
+ pattern = r".*\t.* \(.*\)"
+ for colorspace in nuke.getColorspaceList(colorspace_knob):
+ match = re.search(pattern, colorspace)
+ if match:
+ results.append(colorspace.split("\t", 1)[0])
+ else:
+ results.append(colorspace)
+
+ return results
def is_headless():
diff --git a/openpype/hosts/nuke/plugins/load/load_clip.py b/openpype/hosts/nuke/plugins/load/load_clip.py
index 3a2ec3dbee5..e2bd2180b76 100644
--- a/openpype/hosts/nuke/plugins/load/load_clip.py
+++ b/openpype/hosts/nuke/plugins/load/load_clip.py
@@ -11,6 +11,9 @@
get_current_project_name,
get_representation_path,
)
+from openpype.pipeline.colorspace import (
+ get_imageio_file_rules_colorspace_from_filepath
+)
from openpype.hosts.nuke.api.lib import (
get_imageio_input_colorspace,
maintained_selection
@@ -101,7 +104,6 @@ def load(self, context, name, namespace, options):
filepath = self.filepath_from_context(context)
filepath = filepath.replace("\\", "/")
- self.log.debug("_ filepath: {}".format(filepath))
start_at_workfile = options.get(
"start_at_workfile", self.options_defaults["start_at_workfile"])
@@ -154,8 +156,8 @@ def load(self, context, name, namespace, options):
with viewer_update_and_undo_stop():
read_node["file"].setValue(filepath)
- used_colorspace = self._set_colorspace(
- read_node, version_data, representation["data"], filepath)
+ self.set_colorspace_to_node(
+ read_node, filepath, version, representation)
self._set_range_to_node(read_node, first, last, start_at_workfile)
@@ -180,8 +182,6 @@ def load(self, context, name, namespace, options):
colorspace = representation["data"].get(key)
colorspace = colorspace or version_data.get(key)
data_imprint["db_colorspace"] = colorspace
- if used_colorspace:
- data_imprint["used_colorspace"] = used_colorspace
else:
value_ = context["version"]['data'].get(
key, str(None))
@@ -302,8 +302,8 @@ def update(self, container, representation):
# to avoid multiple undo steps for rest of process
# we will switch off undo-ing
with viewer_update_and_undo_stop():
- used_colorspace = self._set_colorspace(
- read_node, version_data, representation["data"], filepath)
+ self.set_colorspace_to_node(
+ read_node, filepath, version_doc, representation)
self._set_range_to_node(read_node, first, last, start_at_workfile)
@@ -320,10 +320,6 @@ def update(self, container, representation):
"author": version_data.get("author")
}
- # add used colorspace if found any
- if used_colorspace:
- updated_dict["used_colorspace"] = used_colorspace
-
last_version_doc = get_last_version_by_subset_id(
project_name, version_doc["parent"], fields=["_id"]
)
@@ -350,6 +346,36 @@ def update(self, container, representation):
self.set_as_member(read_node)
+ def set_colorspace_to_node(
+ self,
+ read_node,
+ filepath,
+ version_doc,
+ representation_doc,
+ ):
+ """Set colorspace to read node.
+
+ Sets colorspace with available names validation.
+
+ Args:
+ read_node (nuke.Node): The nuke's read node
+ filepath (str): file path
+ version_doc (dict): version document
+ representation_doc (dict): representation document
+
+ """
+ used_colorspace = self._get_colorspace_data(
+ version_doc, representation_doc, filepath)
+
+ if (
+ used_colorspace
+ and colorspace_exists_on_node(read_node, used_colorspace)
+ ):
+ self.log.info(f"Used colorspace: {used_colorspace}")
+ read_node["colorspace"].setValue(used_colorspace)
+ else:
+ self.log.info("Colorspace not set...")
+
def remove(self, container):
read_node = container["node"]
assert read_node.Class() == "Read", "Must be Read"
@@ -450,25 +476,49 @@ def _get_node_name(self, representation):
return self.node_name_template.format(**name_data)
- def _set_colorspace(self, node, version_data, repre_data, path):
- output_color = None
- path = path.replace("\\", "/")
- # get colorspace
- colorspace = repre_data.get("colorspace")
- colorspace = colorspace or version_data.get("colorspace")
+ def _get_colorspace_data(self, version_doc, representation_doc, filepath):
+ """Get colorspace data from version and representation documents
- # colorspace from `project_settings/nuke/imageio/regexInputs`
- iio_colorspace = get_imageio_input_colorspace(path)
+ Args:
+ version_doc (dict): version document
+ representation_doc (dict): representation document
+ filepath (str): file path
- # Set colorspace defined in version data
- if (
- colorspace is not None
- and colorspace_exists_on_node(node, str(colorspace))
- ):
- node["colorspace"].setValue(str(colorspace))
- output_color = str(colorspace)
- elif iio_colorspace is not None:
- node["colorspace"].setValue(iio_colorspace)
- output_color = iio_colorspace
+ Returns:
+ Any[str,None]: colorspace name or None
+ """
+ # Get backward compatible colorspace key.
+ colorspace = representation_doc["data"].get("colorspace")
+ self.log.debug(
+ f"Colorspace from representation colorspace: {colorspace}"
+ )
+
+ # Get backward compatible version data key if colorspace is not found.
+ colorspace = colorspace or version_doc["data"].get("colorspace")
+ self.log.debug(f"Colorspace from version colorspace: {colorspace}")
+
+ # Get colorspace from representation colorspaceData if colorspace is
+ # not found.
+ colorspace_data = representation_doc["data"].get("colorspaceData", {})
+ colorspace = colorspace or colorspace_data.get("colorspace")
+ self.log.debug(
+ f"Colorspace from representation colorspaceData: {colorspace}"
+ )
+
+ print(f"Colorspace found: {colorspace}")
+
+ # check if any filerules are not applicable
+ new_parsed_colorspace = get_imageio_file_rules_colorspace_from_filepath( # noqa
+ filepath, "nuke", get_current_project_name()
+ )
+ self.log.debug(f"Colorspace new filerules: {new_parsed_colorspace}")
+
+ # colorspace from `project_settings/nuke/imageio/regexInputs`
+ old_parsed_colorspace = get_imageio_input_colorspace(filepath)
+ self.log.debug(f"Colorspace old filerules: {old_parsed_colorspace}")
- return output_color
+ return (
+ new_parsed_colorspace
+ or old_parsed_colorspace
+ or colorspace
+ )
diff --git a/openpype/hosts/nuke/plugins/publish/collect_slate_node.py b/openpype/hosts/nuke/plugins/publish/collect_slate_node.py
index 3baa0cd9b56..ac30bd6051a 100644
--- a/openpype/hosts/nuke/plugins/publish/collect_slate_node.py
+++ b/openpype/hosts/nuke/plugins/publish/collect_slate_node.py
@@ -17,7 +17,8 @@ def process(self, instance):
(
n_ for n_ in nuke.allNodes()
if "slate" in n_.name().lower()
- if not n_["disable"].getValue()
+ if not n_["disable"].getValue() and
+ "publish_instance" not in n_.knobs() # Exclude instance nodes.
),
None
)
diff --git a/openpype/hosts/nuke/plugins/publish/collect_writes.py b/openpype/hosts/nuke/plugins/publish/collect_writes.py
index 6f9245f5b96..2bf43ed75a8 100644
--- a/openpype/hosts/nuke/plugins/publish/collect_writes.py
+++ b/openpype/hosts/nuke/plugins/publish/collect_writes.py
@@ -194,7 +194,6 @@ def _set_additional_instance_data(
"frameEndHandle": last_frame,
})
-
# TODO temporarily set stagingDir as persistent for backward
# compatibility. This is mainly focused on `renders`folders which
# were previously not cleaned up (and could be used in read notes)
@@ -269,10 +268,6 @@ def _get_existing_frames_representation(
"tags": []
}
- frame_start_str = self._get_frame_start_str(first_frame, last_frame)
-
- representation['frameStart'] = frame_start_str
-
# set slate frame
collected_frames = self._add_slate_frame_to_collected_frames(
instance,
diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py
index 2346739e20b..fc7900321c4 100644
--- a/openpype/hosts/resolve/api/plugin.py
+++ b/openpype/hosts/resolve/api/plugin.py
@@ -409,6 +409,10 @@ def load(self, files):
source_in = int(_clip_property("Start"))
source_out = int(_clip_property("End"))
source_duration = int(_clip_property("Frames"))
+ # Trim clip start if slate is present
+ if "slate" in self.data["versionData"]["families"]:
+ source_in += 1
+ source_duration = source_out - source_in + 1
if not self.with_handles:
# Load file without the handles of the source media
@@ -435,7 +439,7 @@ def load(self, files):
handle_start = version_data.get("handleStart", 0)
handle_end = version_data.get("handleEnd", 0)
frame_start_handle = frame_start - handle_start
- frame_end_handle = frame_start + handle_end
+ frame_end_handle = frame_end + handle_end
database_frame_duration = int(
frame_end_handle - frame_start_handle + 1
)
diff --git a/openpype/hosts/substancepainter/api/lib.py b/openpype/hosts/substancepainter/api/lib.py
index 1cb480b552c..d9084c57d64 100644
--- a/openpype/hosts/substancepainter/api/lib.py
+++ b/openpype/hosts/substancepainter/api/lib.py
@@ -606,7 +606,7 @@ def _setup_prompt():
mesh_select.setVisible(False)
# Ensure UI is visually up-to-date
- app.processEvents(QtCore.QEventLoop.ExcludeUserInputEvents)
+ app.processEvents(QtCore.QEventLoop.ExcludeUserInputEvents, 8000)
# Trigger the 'select file' dialog to set the path and have the
# new file dialog to use the path.
@@ -623,8 +623,6 @@ def _setup_prompt():
"Failed to set mesh path with the prompt dialog:"
f"{mesh_filepath}\n\n"
"Creating new project directly with the mesh path instead.")
- else:
- dialog.done(dialog.Accepted)
new_action = _get_new_project_action()
if not new_action:
diff --git a/openpype/hosts/substancepainter/plugins/load/load_mesh.py b/openpype/hosts/substancepainter/plugins/load/load_mesh.py
index 08c1d5c3918..014dbd214f0 100644
--- a/openpype/hosts/substancepainter/plugins/load/load_mesh.py
+++ b/openpype/hosts/substancepainter/plugins/load/load_mesh.py
@@ -1,3 +1,5 @@
+import copy
+from qtpy import QtWidgets, QtCore
from openpype.pipeline import (
load,
get_representation_path,
@@ -11,7 +13,131 @@
from openpype.hosts.substancepainter.api.lib import prompt_new_file_with_mesh
import substance_painter.project
-import qargparse
+
+
+def _convert(substance_attr):
+ """Return Substance Painter Python API Project attribute from string.
+
+ This converts a string like "ProjectWorkflow.Default" to for example
+ the Substance Painter Python API equivalent object, like:
+ `substance_painter.project.ProjectWorkflow.Default`
+
+ Args:
+ substance_attr (str): The `substance_painter.project` attribute,
+ for example "ProjectWorkflow.Default"
+
+ Returns:
+ Any: Substance Python API object of the project attribute.
+
+ Raises:
+ ValueError: If attribute does not exist on the
+ `substance_painter.project` python api.
+ """
+ root = substance_painter.project
+ for attr in substance_attr.split("."):
+ root = getattr(root, attr, None)
+ if root is None:
+ raise ValueError(
+ "Substance Painter project attribute"
+ f" does not exist: {substance_attr}")
+
+ return root
+
+
+def get_template_by_name(name: str, templates: list[dict]) -> dict:
+ return next(
+ template for template in templates
+ if template["name"] == name
+ )
+
+
+class SubstanceProjectConfigurationWindow(QtWidgets.QDialog):
+ """The pop-up dialog allows users to choose material
+ duplicate options for importing Max objects when updating
+ or switching assets.
+ """
+ def __init__(self, project_templates):
+ super(SubstanceProjectConfigurationWindow, self).__init__()
+ self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint)
+
+ self.configuration = None
+ self.template_names = [template["name"] for template
+ in project_templates]
+ self.project_templates = project_templates
+
+ self.widgets = {
+ "label": QtWidgets.QLabel(
+ "Select your template for project configuration"),
+ "template_options": QtWidgets.QComboBox(),
+ "import_cameras": QtWidgets.QCheckBox("Import Cameras"),
+ "preserve_strokes": QtWidgets.QCheckBox("Preserve Strokes"),
+ "clickbox": QtWidgets.QWidget(),
+ "combobox": QtWidgets.QWidget(),
+ "buttons": QtWidgets.QDialogButtonBox(
+ QtWidgets.QDialogButtonBox.Ok
+ | QtWidgets.QDialogButtonBox.Cancel)
+ }
+
+ self.widgets["template_options"].addItems(self.template_names)
+
+ template_name = self.widgets["template_options"].currentText()
+ self._update_to_match_template(template_name)
+ # Build clickboxes
+ layout = QtWidgets.QHBoxLayout(self.widgets["clickbox"])
+ layout.addWidget(self.widgets["import_cameras"])
+ layout.addWidget(self.widgets["preserve_strokes"])
+ # Build combobox
+ layout = QtWidgets.QHBoxLayout(self.widgets["combobox"])
+ layout.addWidget(self.widgets["template_options"])
+ # Build buttons
+ layout = QtWidgets.QHBoxLayout(self.widgets["buttons"])
+ # Build layout.
+ layout = QtWidgets.QVBoxLayout(self)
+ layout.addWidget(self.widgets["label"])
+ layout.addWidget(self.widgets["combobox"])
+ layout.addWidget(self.widgets["clickbox"])
+ layout.addWidget(self.widgets["buttons"])
+
+ self.widgets["template_options"].currentTextChanged.connect(
+ self._update_to_match_template)
+ self.widgets["buttons"].accepted.connect(self.on_accept)
+ self.widgets["buttons"].rejected.connect(self.on_reject)
+
+ def on_accept(self):
+ self.configuration = self.get_project_configuration()
+ self.close()
+
+ def on_reject(self):
+ self.close()
+
+ def _update_to_match_template(self, template_name):
+ template = get_template_by_name(template_name, self.project_templates)
+ self.widgets["import_cameras"].setChecked(template["import_cameras"])
+ self.widgets["preserve_strokes"].setChecked(
+ template["preserve_strokes"])
+
+ def get_project_configuration(self):
+ templates = self.project_templates
+ template_name = self.widgets["template_options"].currentText()
+ template = get_template_by_name(template_name, templates)
+ template = copy.deepcopy(template) # do not edit the original
+ template["import_cameras"] = self.widgets["import_cameras"].isChecked()
+ template["preserve_strokes"] = (
+ self.widgets["preserve_strokes"].isChecked()
+ )
+ for key in ["normal_map_format",
+ "project_workflow",
+ "tangent_space_mode"]:
+ template[key] = _convert(template[key])
+ return template
+
+ @classmethod
+ def prompt(cls, templates):
+ dialog = cls(templates)
+ dialog.exec_()
+ configuration = dialog.configuration
+ dialog.deleteLater()
+ return configuration
class SubstanceLoadProjectMesh(load.LoaderPlugin):
@@ -25,48 +151,42 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin):
icon = "code-fork"
color = "orange"
- options = [
- qargparse.Boolean(
- "preserve_strokes",
- default=True,
- help="Preserve strokes positions on mesh.\n"
- "(only relevant when loading into existing project)"
- ),
- qargparse.Boolean(
- "import_cameras",
- default=True,
- help="Import cameras from the mesh file."
- )
- ]
+ # Defined via settings
+ project_templates = []
- def load(self, context, name, namespace, data):
+ def load(self, context, name, namespace, options=None):
# Get user inputs
- import_cameras = data.get("import_cameras", True)
- preserve_strokes = data.get("preserve_strokes", True)
+ result = SubstanceProjectConfigurationWindow.prompt(
+ self.project_templates)
+ if not result:
+ # cancelling loader action
+ return
sp_settings = substance_painter.project.Settings(
- import_cameras=import_cameras
+ import_cameras=result["import_cameras"],
+ normal_map_format=result["normal_map_format"],
+ project_workflow=result["project_workflow"],
+ tangent_space_mode=result["tangent_space_mode"],
+ default_texture_resolution=result["default_texture_resolution"]
)
if not substance_painter.project.is_open():
# Allow to 'initialize' a new project
path = self.filepath_from_context(context)
- # TODO: improve the prompt dialog function to not
- # only works for simple polygon scene
- result = prompt_new_file_with_mesh(mesh_filepath=path)
- if not result:
- self.log.info("User cancelled new project prompt."
- "Creating new project directly from"
- " Substance Painter API Instead.")
- settings = substance_painter.project.create(
- mesh_file_path=path, settings=sp_settings
- )
-
+ sp_settings = substance_painter.project.Settings(
+ import_cameras=result["import_cameras"],
+ normal_map_format=result["normal_map_format"],
+ project_workflow=result["project_workflow"],
+ tangent_space_mode=result["tangent_space_mode"],
+ default_texture_resolution=result["default_texture_resolution"]
+ )
+ settings = substance_painter.project.create(
+ mesh_file_path=path, settings=sp_settings
+ )
else:
# Reload the mesh
settings = substance_painter.project.MeshReloadingSettings(
- import_cameras=import_cameras,
- preserve_strokes=preserve_strokes
- )
+ import_cameras=result["import_cameras"],
+ preserve_strokes=result["preserve_strokes"])
def on_mesh_reload(status: substance_painter.project.ReloadMeshStatus): # noqa
if status == substance_painter.project.ReloadMeshStatus.SUCCESS: # noqa
@@ -92,7 +212,7 @@ def on_mesh_reload(status: substance_painter.project.ReloadMeshStatus): # noqa
# from the user's original choice. We don't store 'preserve_strokes'
# as we always preserve strokes on updates.
container["options"] = {
- "import_cameras": import_cameras,
+ "import_cameras": result["import_cameras"],
}
set_container_metadata(project_mesh_object_name, container)
diff --git a/openpype/hosts/unreal/plugins/publish/collect_render_instances.py b/openpype/hosts/unreal/plugins/publish/collect_render_instances.py
index dad0310dfcc..d7b9191fa32 100644
--- a/openpype/hosts/unreal/plugins/publish/collect_render_instances.py
+++ b/openpype/hosts/unreal/plugins/publish/collect_render_instances.py
@@ -64,7 +64,7 @@ def process(self, instance):
new_data = new_instance.data
- new_data["asset"] = seq_name
+ new_data["asset"] = f"/{s.get('output')}"
new_data["setMembers"] = seq_name
new_data["family"] = "render"
new_data["families"] = ["render", "review"]
diff --git a/openpype/modules/deadline/plugins/publish/submit_aftereffects_deadline.py b/openpype/modules/deadline/plugins/publish/submit_aftereffects_deadline.py
index 009375e87ee..d40c371de0e 100644
--- a/openpype/modules/deadline/plugins/publish/submit_aftereffects_deadline.py
+++ b/openpype/modules/deadline/plugins/publish/submit_aftereffects_deadline.py
@@ -82,6 +82,7 @@ def get_job_info(self):
"FTRACK_API_KEY",
"FTRACK_API_USER",
"FTRACK_SERVER",
+ "AVALON_DB",
"AVALON_PROJECT",
"AVALON_ASSET",
"AVALON_TASK",
diff --git a/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py b/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py
index 8f9e9a74253..58e69d0aeae 100644
--- a/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py
+++ b/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py
@@ -104,6 +104,7 @@ def get_job_info(self):
"FTRACK_API_USER",
"FTRACK_SERVER",
"OPENPYPE_SG_USER",
+ "AVALON_DB",
"AVALON_PROJECT",
"AVALON_ASSET",
"AVALON_TASK",
diff --git a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py
index 9a718aa089a..dcb79588a74 100644
--- a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py
+++ b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py
@@ -223,6 +223,7 @@ def process(self, instance):
"FTRACK_API_KEY",
"FTRACK_API_USER",
"FTRACK_SERVER",
+ "AVALON_DB",
"AVALON_PROJECT",
"AVALON_ASSET",
"AVALON_TASK",
diff --git a/openpype/modules/deadline/plugins/publish/submit_harmony_deadline.py b/openpype/modules/deadline/plugins/publish/submit_harmony_deadline.py
index 17e672334cf..73bc10465d1 100644
--- a/openpype/modules/deadline/plugins/publish/submit_harmony_deadline.py
+++ b/openpype/modules/deadline/plugins/publish/submit_harmony_deadline.py
@@ -275,6 +275,7 @@ def get_job_info(self):
"FTRACK_API_KEY",
"FTRACK_API_USER",
"FTRACK_SERVER",
+ "AVALON_DB",
"AVALON_PROJECT",
"AVALON_ASSET",
"AVALON_TASK",
diff --git a/openpype/modules/deadline/plugins/publish/submit_houdini_cache_deadline.py b/openpype/modules/deadline/plugins/publish/submit_houdini_cache_deadline.py
index ada69575a8b..bef93b3947c 100644
--- a/openpype/modules/deadline/plugins/publish/submit_houdini_cache_deadline.py
+++ b/openpype/modules/deadline/plugins/publish/submit_houdini_cache_deadline.py
@@ -110,6 +110,7 @@ def get_job_info(self):
"FTRACK_API_USER",
"FTRACK_SERVER",
"OPENPYPE_SG_USER",
+ "AVALON_DB",
"AVALON_PROJECT",
"AVALON_ASSET",
"AVALON_TASK",
diff --git a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py
index bf7fb45a8b7..6ed9e66ce08 100644
--- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py
+++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py
@@ -205,6 +205,7 @@ def get_job_info(self, dependency_job_ids=None):
"FTRACK_API_USER",
"FTRACK_SERVER",
"OPENPYPE_SG_USER",
+ "AVALON_DB",
"AVALON_PROJECT",
"AVALON_ASSET",
"AVALON_TASK",
diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py
index f06bd4dbe69..07bbb1cacbb 100644
--- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py
+++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py
@@ -16,11 +16,6 @@
replace_with_published_scene_path
)
from openpype.pipeline.publish import KnownPublishError
-from openpype.hosts.max.api.lib import (
- get_current_renderer,
- get_multipass_setting
-)
-from openpype.hosts.max.api.lib_rendersettings import RenderSettings
from openpype_modules.deadline import abstract_submit_deadline
from openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo
from openpype.lib import is_running_from_build
@@ -108,6 +103,7 @@ def get_job_info(self):
"FTRACK_API_USER",
"FTRACK_SERVER",
"OPENPYPE_SG_USER",
+ "AVALON_DB",
"AVALON_PROJECT",
"AVALON_ASSET",
"AVALON_TASK",
@@ -293,6 +289,9 @@ def get_plugin_info_through_camera(self, camera):
Args:
infos(dict): a dictionary with plugin info.
"""
+ from openpype.hosts.max.api.lib import get_current_renderer
+ from openpype.hosts.max.api.lib_rendersettings import RenderSettings
+
instance = self._instance
# set the target camera
plugin_info = copy.deepcopy(self.plugin_info)
@@ -358,6 +357,8 @@ def _use_published_name_for_multiples(self, data, project_settings):
job_info_list (list): A list of multiple job infos
plugin_info_list (list): A list of multiple plugin infos
"""
+ from openpype.hosts.max.api.lib import get_multipass_setting
+
job_info_list = []
plugin_info_list = []
instance = self._instance
diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py
index 5591db151a2..4cd417b83b2 100644
--- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py
+++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py
@@ -201,6 +201,7 @@ def get_job_info(self):
"FTRACK_API_USER",
"FTRACK_SERVER",
"OPENPYPE_SG_USER",
+ "AVALON_DB",
"AVALON_PROJECT",
"AVALON_ASSET",
"AVALON_TASK",
diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py
index 41a2a64ab58..01398576ada 100644
--- a/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py
+++ b/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py
@@ -121,6 +121,7 @@ def get_job_info(self):
environment["AYON_REMOTE_PUBLISH"] = "1"
else:
environment["OPENPYPE_REMOTE_PUBLISH"] = "1"
+ environment["AVALON_DB"] = os.environ.get("AVALON_DB")
for key, value in environment.items():
job_info.EnvironmentKeyValue[key] = value
diff --git a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py
index 746b009255f..9c2d2128067 100644
--- a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py
+++ b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py
@@ -376,6 +376,7 @@ def payload_submit(
keys = [
"PYTHONPATH",
"PATH",
+ "AVALON_DB",
"AVALON_PROJECT",
"AVALON_ASSET",
"AVALON_TASK",
diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_cache_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_cache_job.py
index 1bb45b77ccf..6f826e85671 100644
--- a/openpype/modules/deadline/plugins/publish/submit_publish_cache_job.py
+++ b/openpype/modules/deadline/plugins/publish/submit_publish_cache_job.py
@@ -146,6 +146,7 @@ def _submit_deadline_post_job(self, instance, job):
environment["AYON_BUNDLE_NAME"] = os.environ["AYON_BUNDLE_NAME"]
deadline_plugin = "Ayon"
else:
+ environment["AVALON_DB"] = os.environ["AVALON_DB"]
environment["OPENPYPE_PUBLISH_JOB"] = "1"
environment["OPENPYPE_RENDER_JOB"] = "0"
environment["OPENPYPE_REMOTE_PUBLISH"] = "0"
diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py
index 4e9df976cd2..1fe678616aa 100644
--- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py
+++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py
@@ -202,6 +202,7 @@ def _submit_deadline_post_job(self, instance, job, instances):
environment["AYON_BUNDLE_NAME"] = os.environ["AYON_BUNDLE_NAME"]
deadline_plugin = "Ayon"
else:
+ environment["AVALON_DB"] = os.environ["AVALON_DB"]
environment["OPENPYPE_PUBLISH_JOB"] = "1"
environment["OPENPYPE_RENDER_JOB"] = "0"
environment["OPENPYPE_REMOTE_PUBLISH"] = "0"
diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py
index 9f720f6ae95..b42080ef061 100644
--- a/openpype/pipeline/colorspace.py
+++ b/openpype/pipeline/colorspace.py
@@ -463,7 +463,11 @@ def compatibility_check():
try:
import PyOpenColorIO # noqa: F401
- CachedData.has_compatible_ocio_package = True
+ # Requirement, introduced in newer ocio version
+ config = PyOpenColorIO.GetCurrentConfig()
+ CachedData.has_compatible_ocio_package = (
+ hasattr(config, "getDisplayViewColorSpaceName")
+ )
except ImportError:
CachedData.has_compatible_ocio_package = False
diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py
index 975fdd31cc0..8872ae64cec 100644
--- a/openpype/pipeline/farm/pyblish_functions.py
+++ b/openpype/pipeline/farm/pyblish_functions.py
@@ -620,15 +620,32 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data,
aov_patterns = aov_filter
preview = match_aov_pattern(app, aov_patterns, render_file_name)
- # toggle preview on if multipart is on
- if instance.data.get("multipartExr"):
- log.debug("Adding preview tag because its multipartExr")
- preview = True
new_instance = deepcopy(skeleton)
new_instance["subset"] = subset_name
new_instance["subsetGroup"] = group_name
+ # toggle preview on if multipart is on
+ # Because we cant query the multipartExr data member of each AOV we'll
+ # need to have hardcoded rule of excluding any renders with
+ # "cryptomatte" in the file name from being a multipart EXR. This issue
+ # happens with Redshift that forces Cryptomatte renders to be separate
+ # files even when the rest of the AOVs are merged into a single EXR.
+ # There might be an edge case where the main instance has cryptomatte
+ # in the name even though it's a multipart EXR.
+ if instance.data.get("renderer") == "redshift":
+ if (
+ instance.data.get("multipartExr") and
+ "cryptomatte" not in render_file_name.lower()
+ ):
+ log.debug("Adding preview tag because it's multipartExr")
+ preview = True
+ else:
+ new_instance["multipartExr"] = False
+ elif instance.data.get("multipartExr"):
+ log.debug("Adding preview tag because its multipartExr")
+ preview = True
+
# explicitly disable review by user
preview = preview and not do_not_add_review
if preview:
diff --git a/openpype/plugins/publish/extract_thumbnail.py b/openpype/plugins/publish/extract_thumbnail.py
index 240e18d2135..40e4b23ee0e 100644
--- a/openpype/plugins/publish/extract_thumbnail.py
+++ b/openpype/plugins/publish/extract_thumbnail.py
@@ -470,7 +470,15 @@ def _create_frame_from_video(self, video_file_path, output_dir):
# Set video input attributes
max_int = str(2147483647)
video_data = get_ffprobe_data(video_file_path, logger=self.log)
- duration = float(video_data["format"]["duration"])
+ # Use duration of the individual streams since it is returned with
+ # higher decimal precision than 'format.duration'. We need this
+ # more precise value for calculating the correct amount of frames
+ # for higher FPS ranges or decimal ranges, e.g. 29.97 FPS
+ duration = max(
+ float(stream.get("duration", 0))
+ for stream in video_data["streams"]
+ if stream.get("codec_type") == "video"
+ )
cmd_args = [
"-y",
diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py
index f744337c672..7c4fd8f1a38 100644
--- a/openpype/pype_commands.py
+++ b/openpype/pype_commands.py
@@ -97,8 +97,6 @@ def publish(paths, targets=None, gui=False):
install_openpype_plugins,
get_global_context,
)
- from openpype.tools.utils.host_tools import show_publish
- from openpype.tools.utils.lib import qt_app_context
# Register target and host
import pyblish.api
@@ -150,6 +148,8 @@ def publish(paths, targets=None, gui=False):
print(plugin)
if gui:
+ from openpype.tools.utils.host_tools import show_publish
+ from openpype.tools.utils.lib import qt_app_context
with qt_app_context():
show_publish()
else:
diff --git a/openpype/scripts/ocio_wrapper.py b/openpype/scripts/ocio_wrapper.py
index 0a78e33c1ff..fe3fbb5d920 100644
--- a/openpype/scripts/ocio_wrapper.py
+++ b/openpype/scripts/ocio_wrapper.py
@@ -18,10 +18,9 @@
- returning all available viewers
found in input config path.
"""
-
+import os
import click
import json
-from pathlib import Path
import PyOpenColorIO as ocio
@@ -76,14 +75,12 @@ def get_colorspace(in_path, out_path):
> pyton.exe ./ocio_wrapper.py config get_colorspace
--in_path= --out_path=
"""
- json_path = Path(out_path)
-
out_data = _get_colorspace_data(in_path)
- with open(json_path, "w") as f_:
+ with open(out_path, "w") as f_:
json.dump(out_data, f_)
- print(f"Colorspace data are saved to '{json_path}'")
+ print("Colorspace data are saved to '{}'".format(out_path))
def _get_colorspace_data(config_path):
@@ -98,13 +95,11 @@ def _get_colorspace_data(config_path):
Returns:
dict: aggregated available colorspaces
"""
- config_path = Path(config_path)
-
- if not config_path.is_file():
+ if not os.path.isfile(config_path):
raise IOError(
- f"Input path `{config_path}` should be `config.ocio` file")
+ "Input path `{}` should be `config.ocio` file".format(config_path))
- config = ocio.Config().CreateFromFile(str(config_path))
+ config = ocio.Config().CreateFromFile(config_path)
colorspace_data = {
"roles": {},
@@ -118,10 +113,9 @@ def _get_colorspace_data(config_path):
for color in config.getColorSpaces()
},
"displays_views": {
- f"{view} ({display})": {
+ "{} ({})".format(display, view): {
"display": display,
"view": view
-
}
for display in config.getDisplays()
for view in config.getViews(display)
@@ -174,14 +168,12 @@ def get_views(in_path, out_path):
> pyton.exe ./ocio_wrapper.py config get_views \
--in_path= --out_path=
"""
- json_path = Path(out_path)
-
out_data = _get_views_data(in_path)
- with open(json_path, "w") as f_:
+ with open(out_path, "w") as f_:
json.dump(out_data, f_)
- print(f"Viewer data are saved to '{json_path}'")
+ print("Viewer data are saved to '{}'".format(out_path))
def _get_views_data(config_path):
@@ -196,22 +188,21 @@ def _get_views_data(config_path):
Returns:
dict: aggregated available viewers
"""
- config_path = Path(config_path)
-
- if not config_path.is_file():
+ if not os.path.isfile(config_path):
raise IOError("Input path should be `config.ocio` file")
- config = ocio.Config().CreateFromFile(str(config_path))
+ config = ocio.Config().CreateFromFile(config_path)
data_ = {}
for display in config.getDisplays():
for view in config.getViews(display):
colorspace = config.getDisplayViewColorSpaceName(display, view)
- # Special token. See https://opencolorio.readthedocs.io/en/latest/guides/authoring/authoring.html#shared-views # noqa
+ # Special token.
+ # See https://opencolorio.readthedocs.io/en/latest/guides/authoring/authoring.html#shared-views # noqa
if colorspace == "":
colorspace = display
- data_[f"{display}/{view}"] = {
+ data_["{}/{}".format(display, view)] = {
"display": display,
"view": view,
"colorspace": colorspace
@@ -247,14 +238,12 @@ def get_version(config_path, out_path):
> pyton.exe ./ocio_wrapper.py config get_version \
--config_path= --out_path=
"""
- json_path = Path(out_path)
-
out_data = _get_version_data(config_path)
- with open(json_path, "w") as f_:
+ with open(out_path, "w") as f_:
json.dump(out_data, f_)
- print(f"Config version data are saved to '{json_path}'")
+ print("Config version data are saved to '{}'".format(out_path))
def _get_version_data(config_path):
@@ -269,12 +258,10 @@ def _get_version_data(config_path):
Returns:
dict: minor and major keys with values
"""
- config_path = Path(config_path)
-
- if not config_path.is_file():
+ if not os.path.isfile(config_path):
raise IOError("Input path should be `config.ocio` file")
- config = ocio.Config().CreateFromFile(str(config_path))
+ config = ocio.Config().CreateFromFile(config_path)
return {
"major": config.getMajorVersion(),
@@ -317,15 +304,13 @@ def get_config_file_rules_colorspace_from_filepath(
colorspace get_config_file_rules_colorspace_from_filepath \
--config_path= --filepath= --out_path=
"""
- json_path = Path(out_path)
-
colorspace = _get_config_file_rules_colorspace_from_filepath(
config_path, filepath)
- with open(json_path, "w") as f_:
+ with open(out_path, "w") as f_:
json.dump(colorspace, f_)
- print(f"Colorspace name is saved to '{json_path}'")
+ print("Colorspace name is saved to '{}'".format(out_path))
def _get_config_file_rules_colorspace_from_filepath(config_path, filepath):
@@ -341,16 +326,14 @@ def _get_config_file_rules_colorspace_from_filepath(config_path, filepath):
Returns:
dict: aggregated available colorspaces
"""
- config_path = Path(config_path)
-
- if not config_path.is_file():
+ if not os.path.isfile(config_path):
raise IOError(
- f"Input path `{config_path}` should be `config.ocio` file")
+ "Input path `{}` should be `config.ocio` file".format(config_path))
- config = ocio.Config().CreateFromFile(str(config_path))
+ config = ocio.Config().CreateFromFile(config_path)
# TODO: use `parseColorSpaceFromString` instead if ocio v1
- colorspace = config.getColorSpaceFromFilepath(str(filepath))
+ colorspace = config.getColorSpaceFromFilepath(filepath)
return colorspace
@@ -370,13 +353,10 @@ def _get_display_view_colorspace_name(config_path, display, view):
Returns:
view color space name (str) e.g. "Output - sRGB"
"""
-
- config_path = Path(config_path)
-
- if not config_path.is_file():
+ if not os.path.isfile(config_path):
raise IOError("Input path should be `config.ocio` file")
- config = ocio.Config.CreateFromFile(str(config_path))
+ config = ocio.Config.CreateFromFile(config_path)
colorspace = config.getDisplayViewColorSpaceName(display, view)
return colorspace
@@ -427,7 +407,8 @@ def get_display_view_colorspace_name(in_path, out_path,
with open(out_path, "w") as f:
json.dump(out_data, f)
- print(f"Display view colorspace saved to '{out_path}'")
+ print("Display view colorspace saved to '{}'".format(out_path))
+
if __name__ == '__main__':
main()
diff --git a/openpype/settings/defaults/project_settings/hiero.json b/openpype/settings/defaults/project_settings/hiero.json
index 9c83733b096..c2f7736a7ce 100644
--- a/openpype/settings/defaults/project_settings/hiero.json
+++ b/openpype/settings/defaults/project_settings/hiero.json
@@ -10,15 +10,15 @@
"rules": {}
},
"workfile": {
- "ocioConfigName": "nuke-default",
- "workingSpace": "linear",
- "sixteenBitLut": "sRGB",
- "eightBitLut": "sRGB",
- "floatLut": "linear",
- "logLut": "Cineon",
- "viewerLut": "sRGB",
- "thumbnailLut": "sRGB",
- "monitorOutLut": "sRGB"
+ "ocioConfigName": "aces_1.2",
+ "workingSpace": "role_scene_linear",
+ "viewerLut": "ACES/sRGB",
+ "thumbnailLut": "ACES/sRGB",
+ "monitorOutLut": "ACES/sRGB",
+ "eightBitLut": "role_matte_paint",
+ "sixteenBitLut": "role_texture_paint",
+ "logLut": "role_compositing_log",
+ "floatLut": "role_scene_linear"
},
"regexInputs": {
"inputs": [
@@ -69,6 +69,10 @@
"tags_addition": [
"review"
]
+ },
+ "CollectClipEffects": {
+ "enabled": true,
+ "effect_categories": {}
}
},
"filters": {},
diff --git a/openpype/settings/defaults/project_settings/max.json b/openpype/settings/defaults/project_settings/max.json
index d1610610dc6..a0a4fcf83d8 100644
--- a/openpype/settings/defaults/project_settings/max.json
+++ b/openpype/settings/defaults/project_settings/max.json
@@ -56,6 +56,16 @@
"enabled": false,
"attributes": {}
},
+ "ValidateCameraAttributes": {
+ "enabled": true,
+ "optional": true,
+ "active": false,
+ "fov": 45.0,
+ "nearrange": 0.0,
+ "farrange": 1000.0,
+ "nearclip": 1.0,
+ "farclip": 1000.0
+ },
"ValidateLoadedPlugin": {
"enabled": false,
"optional": true,
diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json
index 615000183dc..7d0af6feb4f 100644
--- a/openpype/settings/defaults/project_settings/maya.json
+++ b/openpype/settings/defaults/project_settings/maya.json
@@ -557,13 +557,13 @@
"publish_mip_map": true
},
"CreateAnimation": {
- "write_color_sets": false,
- "write_face_sets": false,
- "include_parent_hierarchy": false,
- "include_user_defined_attributes": false,
- "default_variants": [
- "Main"
- ]
+ "default_variants": [],
+ "step": 1.0,
+ "includeParentHierarchy": false,
+ "farm": false,
+ "priority": 50,
+ "refresh": false,
+ "include_user_defined_attributes": false
},
"CreateModel": {
"enabled": true,
@@ -577,12 +577,15 @@
},
"CreatePointCache": {
"enabled": true,
- "write_color_sets": false,
- "write_face_sets": false,
- "include_user_defined_attributes": false,
"default_variants": [
"Main"
- ]
+ ],
+ "step": 1.0,
+ "includeParentHierarchy": false,
+ "farm": false,
+ "priority": 50,
+ "refresh": false,
+ "include_user_defined_attributes": false
},
"CreateProxyAlembic": {
"enabled": true,
@@ -1107,6 +1110,32 @@
"pointcache",
"model",
"vrayproxy.alembic"
+ ],
+ "flags": [
+ "stripNamespaces",
+ "writeNormals",
+ "worldSpace"
+ ],
+ "attr": "",
+ "attrPrefix": "",
+ "dataFormat": "ogawa",
+ "melPerFrameCallback": "",
+ "melPostJobCallback": "",
+ "preRollStartFrame": 0,
+ "pythonPerFrameCallback": "",
+ "pythonPostJobCallback": "",
+ "userAttr": "",
+ "userAttrPrefix": "",
+ "visibleOnly": false,
+ "overrides": [
+ "attr",
+ "attrPrefix",
+ "worldSpace",
+ "writeColorSets",
+ "writeNormals",
+ "writeFaceSets",
+ "renderableOnly",
+ "visibleOnly"
]
},
"ExtractObj": {
diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json
index 15c2d262e0a..9b46fe8767f 100644
--- a/openpype/settings/defaults/project_settings/nuke.json
+++ b/openpype/settings/defaults/project_settings/nuke.json
@@ -19,16 +19,21 @@
"rules": {}
},
"viewer": {
- "viewerProcess": "sRGB (default)"
+ "viewerProcess": "ACES/sRGB"
},
"baking": {
- "viewerProcess": "rec709 (default)"
+ "viewerProcess": "ACES/Rec.709"
},
"workfile": {
"colorManagement": "OCIO",
- "OCIO_config": "nuke-default",
- "workingSpaceLUT": "scene_linear",
- "monitorLut": "sRGB (default)"
+ "OCIO_config": "aces_1.2",
+ "workingSpaceLUT": "role_scene_linear",
+ "monitorLut": "ACES/sRGB",
+ "monitorOutLUT": "ACES/sRGB",
+ "int8Lut": "role_matte_paint",
+ "int16Lut": "role_texture_paint",
+ "logLut": "role_compositing_log",
+ "floatLut": "role_scene_linear"
},
"nodes": {
"requiredNodes": [
diff --git a/openpype/settings/defaults/project_settings/substancepainter.json b/openpype/settings/defaults/project_settings/substancepainter.json
index 2f9344d435c..f601814fdcf 100644
--- a/openpype/settings/defaults/project_settings/substancepainter.json
+++ b/openpype/settings/defaults/project_settings/substancepainter.json
@@ -10,5 +10,38 @@
"rules": {}
}
},
- "shelves": {}
+ "shelves": {},
+ "load": {
+ "SubstanceLoadProjectMesh": {
+ "project_templates": [
+ {
+ "name": "2K(Default)",
+ "default_texture_resolution": 2048,
+ "import_cameras": true,
+ "normal_map_format": "NormalMapFormat.DirectX",
+ "project_workflow": "ProjectWorkflow.Default",
+ "tangent_space_mode": "TangentSpace.PerFragment",
+ "preserve_strokes": true
+ },
+ {
+ "name": "2K(UV tile)",
+ "default_texture_resolution": 2048,
+ "import_cameras": true,
+ "normal_map_format": "NormalMapFormat.DirectX",
+ "project_workflow": "ProjectWorkflow.UVTile",
+ "tangent_space_mode": "TangentSpace.PerFragment",
+ "preserve_strokes": true
+ },
+ {
+ "name": "4K(Custom)",
+ "default_texture_resolution": 4096,
+ "import_cameras": true,
+ "normal_map_format": "NormalMapFormat.OpenGL",
+ "project_workflow": "ProjectWorkflow.UVTile",
+ "tangent_space_mode": "TangentSpace.PerFragment",
+ "preserve_strokes": true
+ }
+ ]
+ }
+ }
}
diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json b/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json
index d80edf902b2..2511e229ef1 100644
--- a/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json
+++ b/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json
@@ -57,38 +57,38 @@
},
{
"type": "text",
- "key": "sixteenBitLut",
- "label": "16 Bit Files"
+ "key": "viewerLut",
+ "label": "Viewer"
},
{
"type": "text",
- "key": "eightBitLut",
- "label": "8 Bit Files"
+ "key": "thumbnailLut",
+ "label": "Thumbnails"
},
{
"type": "text",
- "key": "floatLut",
- "label": "Floating Point Files"
+ "key": "monitorOutLut",
+ "label": "Monitor"
},
{
"type": "text",
- "key": "logLut",
- "label": "Log Files"
+ "key": "eightBitLut",
+ "label": "8 Bit Files"
},
{
"type": "text",
- "key": "viewerLut",
- "label": "Viewer"
+ "key": "sixteenBitLut",
+ "label": "16 Bit Files"
},
{
"type": "text",
- "key": "thumbnailLut",
- "label": "Thumbnails"
+ "key": "logLut",
+ "label": "Log Files"
},
{
"type": "text",
- "key": "monitorOutLut",
- "label": "Monitor"
+ "key": "floatLut",
+ "label": "Floating Point Files"
}
]
}
@@ -312,6 +312,31 @@
"label": "Tags addition"
}
]
+ },
+ {
+ "type": "dict",
+ "collapsible": true,
+ "checkbox_key": "enabled",
+ "key": "CollectClipEffects",
+ "label": "Collect Clip Effects",
+ "is_group": true,
+ "children": [
+ {
+ "type": "boolean",
+ "key": "enabled",
+ "label": "Enabled"
+ },
+ {
+ "type": "dict-modifiable",
+ "key": "effect_categories",
+ "label": "Effect Categories",
+ "object_type": {
+ "type": "list",
+ "key": "effects_classes",
+ "object_type": "text"
+ }
+ }
+ ]
}
]
},
diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_substancepainter.json b/openpype/settings/entities/schemas/projects_schema/schema_project_substancepainter.json
index 6be8cecad3c..d143aa0820c 100644
--- a/openpype/settings/entities/schemas/projects_schema/schema_project_substancepainter.json
+++ b/openpype/settings/entities/schemas/projects_schema/schema_project_substancepainter.json
@@ -25,6 +25,70 @@
"object_type": {
"type": "text"
}
+ },
+ {
+ "type": "dict",
+ "collapsible": true,
+ "key": "load",
+ "label": "Loaders",
+ "use_label_wrap": true,
+ "children": [
+ {
+ "type": "dict",
+ "collapsible": true,
+ "key": "SubstanceLoadProjectMesh",
+ "label": "Load Mesh",
+ "children": [
+ {
+ "type": "list",
+ "collapsible": true,
+ "key": "project_templates",
+ "label": "Project Templates",
+ "object_type": {
+ "type": "dict",
+ "children": [
+ {
+ "type": "text",
+ "key": "name",
+ "label": "Name"
+ },
+ {
+ "type": "number",
+ "key": "default_texture_resolution",
+ "label": "Document Resolution"
+ },
+ {
+ "type": "boolean",
+ "key": "import_cameras",
+ "label": "Import Cameras"
+ },
+ {
+ "type": "text",
+ "key": "normal_map_format",
+ "label": "Normal Map Format"
+ },
+ {
+ "type": "text",
+ "key": "project_workflow",
+ "label": "UV Tile Settings"
+ },
+ {
+ "type": "text",
+ "key": "tangent_space_mode",
+ "label": "Normal Map Format"
+ },
+ {
+ "type": "boolean",
+ "key": "preserve_strokes",
+ "label": "Preserve Strokes"
+ }
+ ]
+ }
+ }
+ ]
+ }
+
+ ]
}
]
}
diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json
index b4d85bda988..1e7a7c0c739 100644
--- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json
+++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json
@@ -48,6 +48,76 @@
}
]
},
+ {
+ "type": "dict",
+ "collapsible": true,
+ "checkbox_key": "enabled",
+ "key": "ValidateCameraAttributes",
+ "label": "Validate Camera Attributes",
+ "is_group": true,
+ "children": [
+ {
+ "type": "boolean",
+ "key": "enabled",
+ "label": "Enabled"
+ },
+ {
+ "type": "boolean",
+ "key": "optional",
+ "label": "Optional"
+ },
+ {
+ "type": "boolean",
+ "key": "active",
+ "label": "Active"
+ },
+ {
+ "type": "number",
+ "key": "fov",
+ "label": "Focal Length",
+ "decimal": 1,
+ "minimum": 0,
+ "maximum": 100.0
+ },
+ {
+ "type": "label",
+ "label": "If the value of the camera attributes set to 0, the system automatically skips checking it"
+ },
+ {
+ "type": "number",
+ "key": "nearrange",
+ "label": "Near Range",
+ "decimal": 1,
+ "minimum": 0,
+ "maximum": 100.0
+ },
+ {
+ "type": "number",
+ "key": "farrange",
+ "label": "Far Range",
+ "decimal": 1,
+ "minimum": 0,
+ "maximum": 2000.0
+ },
+ {
+ "type": "number",
+ "key": "nearclip",
+ "label": "Near Clip",
+ "decimal": 1,
+ "minimum": 0,
+ "maximum": 100.0
+ },
+ {
+ "type": "number",
+ "key": "farclip",
+ "label": "Far Clip",
+ "decimal": 1,
+ "minimum": 0,
+ "maximum": 2000.0
+ }
+ ]
+ },
+
{
"type": "dict",
"collapsible": true,
diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json
index b56e381c1da..16355eb1a2a 100644
--- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json
+++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json
@@ -131,31 +131,37 @@
"type": "label",
"label": "This plugin is not optional due to implicit creation through loading the \"rig\" family.\nThis family is also hidden from creation due to complexity in setup."
},
+ {
+ "type": "list",
+ "key": "default_variants",
+ "label": "Default Variants",
+ "object_type": "text"
+ },
{
"type": "boolean",
- "key": "write_color_sets",
- "label": "Write Color Sets"
+ "key": "includeParentHierarchy",
+ "label": "Include Parent Hierarchy"
},
{
"type": "boolean",
- "key": "write_face_sets",
- "label": "Write Face Sets"
+ "key": "farm",
+ "label": "Submit to the Farm"
+ },
+ {
+ "type": "number",
+ "key": "priority",
+ "label": "Farm Job Priority",
+ "minimum": 0
},
{
"type": "boolean",
- "key": "include_parent_hierarchy",
- "label": "Include Parent Hierarchy"
+ "key": "refresh",
+ "label": "Refresh"
},
{
"type": "boolean",
"key": "include_user_defined_attributes",
"label": "Include User Defined Attributes"
- },
- {
- "type": "list",
- "key": "default_variants",
- "label": "Default Variants",
- "object_type": "text"
}
]
},
@@ -201,26 +207,44 @@
"key": "enabled",
"label": "Enabled"
},
+ {
+ "type": "list",
+ "key": "default_variants",
+ "label": "Default Variants",
+ "object_type": "text"
+ },
+ {
+ "type": "number",
+ "key": "step",
+ "label": "Step default",
+ "minimum": 0.0,
+ "decimal": 4
+ },
{
"type": "boolean",
- "key": "write_color_sets",
- "label": "Write Color Sets"
+ "key": "includeParentHierarchy",
+ "label": "Include Parent Hierarchy default"
},
{
"type": "boolean",
- "key": "write_face_sets",
- "label": "Write Face Sets"
+ "key": "farm",
+ "label": "Farm default"
+ },
+ {
+ "type": "number",
+ "key": "priority",
+ "label": "Priority default",
+ "minimum": 0
},
{
"type": "boolean",
- "key": "include_user_defined_attributes",
- "label": "Include User Defined Attributes"
+ "key": "refresh",
+ "label": "Refresh default"
},
{
- "type": "list",
- "key": "default_variants",
- "label": "Default Variants",
- "object_type": "text"
+ "type": "boolean",
+ "key": "include_user_defined_attributes",
+ "label": "Include User Defined Attributes"
}
]
},
diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json
index d2e7c51e249..88f3d476ca3 100644
--- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json
+++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json
@@ -750,26 +750,6 @@
}
]
},
- {
- "type": "dict",
- "collapsible": true,
- "key": "ExtractAlembic",
- "label": "Extract Alembic",
- "checkbox_key": "enabled",
- "children": [
- {
- "type": "boolean",
- "key": "enabled",
- "label": "Enabled"
- },
- {
- "key": "families",
- "label": "Families",
- "type": "list",
- "object_type": "text"
- }
- ]
- },
{
"type": "dict",
"collapsible": true,
@@ -1008,6 +988,166 @@
}
]
},
+ {
+ "type": "dict",
+ "collapsible": true,
+ "key": "ExtractAlembic",
+ "label": "Extract Pointcache/Animation",
+ "checkbox_key": "enabled",
+ "children": [
+ {
+ "type": "boolean",
+ "key": "enabled",
+ "label": "Enabled"
+ },
+ {
+ "key": "families",
+ "label": "Families",
+ "type": "list",
+ "object_type": "text"
+ },
+ {
+ "type": "splitter"
+ },
+ {
+ "type": "label",
+ "label": "Export Defaults"
+ },
+ {
+ "type": "enum",
+ "key": "flags",
+ "multiselection": true,
+ "label": "Export Flags",
+ "enum_items": [
+ {"autoSubd": "autoSubd"},
+ {"dontSkipUnwrittenFrames": "dontSkipUnwrittenFrames"},
+ {"eulerFilter": "eulerFilter"},
+ {"noNormals": "noNormals"},
+ {"preRoll": "preRoll"},
+ {"renderableOnly": "renderableOnly"},
+ {"stripNamespaces": "stripNamespaces"},
+ {"uvWrite": "uvWrite"},
+ {"uvsOnly": "uvsOnly"},
+ {"verbose": "verbose"},
+ {"wholeFrameGeo": "wholeFrameGeo"},
+ {"worldSpace": "worldSpace"},
+ {"writeColorSets": "writeColorSets"},
+ {"writeFaceSets": "writeFaceSets"},
+ {"writeNormals": "writeNormals"},
+ {"writeUVSets": "writeUVSets"},
+ {"writeVisibility": "writeVisibility"}
+ ]
+ },
+ {
+ "type": "text",
+ "key": "attr",
+ "label": "Custom Attributes"
+ },
+ {
+ "type": "text",
+ "key": "attrPrefix",
+ "label": "Custom Attributes Prefix"
+ },
+ {
+ "type": "enum",
+ "key": "dataFormat",
+ "label": "Data Format",
+ "enum_items": [
+ {
+ "ogawa": "ogawa"
+ },
+ {
+ "HDF": "HDF"
+ }
+ ]
+ },
+ {
+ "type": "text",
+ "key": "melPerFrameCallback",
+ "label": "melPerFrameCallback"
+ },
+ {
+ "type": "text",
+ "key": "melPostJobCallback",
+ "label": "melPostJobCallback"
+ },
+ {
+ "type": "number",
+ "key": "preRollStartFrame",
+ "label": "Pre Roll Start Frame",
+ "minimum": 0
+ },
+ {
+ "type": "text",
+ "key": "pythonPerFrameCallback",
+ "label": "pythonPerFrameCallback"
+ },
+ {
+ "type": "text",
+ "key": "pythonPostJobCallback",
+ "label": "pythonPostJobCallback"
+ },
+ {
+ "type": "text",
+ "key": "userAttr",
+ "label": "userAttr"
+ },
+ {
+ "type": "text",
+ "key": "userAttrPrefix",
+ "label": "userAttrPrefix"
+ },
+ {
+ "type": "boolean",
+ "key": "visibleOnly",
+ "label": "Visible Only"
+ },
+ {
+ "type": "splitter"
+ },
+ {
+ "type": "label",
+ "label": "These attributes are exposed to the user when publishing with default values from above."
+ },
+ {
+ "type": "enum",
+ "key": "overrides",
+ "multiselection": true,
+ "label": "Exposed Overrides",
+ "enum_items": [
+ {"attr": "Custom Attributes"},
+ {"attrPrefix": "Custom Attributes Prefix"},
+ {"autoSubd": "autoSubd"},
+ {"dataFormat": "dataFormat"},
+ {"dontSkipUnwrittenFrames": "dontSkipUnwrittenFrames"},
+ {"eulerFilter": "eulerFilter"},
+ {"melPerFrameCallback": "melPerFrameCallback"},
+ {"melPostJobCallback": "melPostJobCallback"},
+ {"noNormals": "noNormals"},
+ {"preRoll": "preRoll"},
+ {"preRollStartFrame": "preRollStartFrame"},
+ {"pythonPerFrameCallback": "pythonPerFrameCallback"},
+ {"pythonPostJobCallback": "pythonPostJobCallback"},
+ {"renderableOnly": "renderableOnly"},
+ {"stripNamespaces": "stripNamespaces"},
+ {"userAttr": "userAttr"},
+ {"userAttrPrefix": "userAttrPrefix"},
+ {"uvWrite": "uvWrite"},
+ {"uvsOnly": "uvsOnly"},
+ {"verbose": "verbose"},
+ {"visibleOnly": "visibleOnly"},
+ {"wholeFrameGeo": "wholeFrameGeo"},
+ {"worldSpace": "worldSpace"},
+ {"writeColorSets": "writeColorSets"},
+ {"writeCreases": "writeCreases"},
+ {"writeFaceSets": "writeFaceSets"},
+ {"writeNormals": "writeNormals"},
+ {"writeUVSets": "writeUVSets"},
+ {"writeVisibility": "writeVisibility"}
+ ]
+ }
+ ]
+ },
{
"type": "dict",
"collapsible": true,
diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json
index af826fcf467..b925a98c897 100644
--- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json
+++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json
@@ -105,7 +105,32 @@
{
"type": "text",
"key": "monitorLut",
- "label": "monitor"
+ "label": "Thumbnails"
+ },
+ {
+ "type": "text",
+ "key": "monitorOutLUT",
+ "label": "Monitor Out"
+ },
+ {
+ "type": "text",
+ "key": "int8Lut",
+ "label": "8-bit files"
+ },
+ {
+ "type": "text",
+ "key": "int16Lut",
+ "label": "16-bit files"
+ },
+ {
+ "type": "text",
+ "key": "logLut",
+ "label": "log files"
+ },
+ {
+ "type": "text",
+ "key": "floatLut",
+ "label": "float files"
}
]
}
diff --git a/openpype/tools/ayon_workfiles/control.py b/openpype/tools/ayon_workfiles/control.py
index 9d19571267e..b932f4eeb2c 100644
--- a/openpype/tools/ayon_workfiles/control.py
+++ b/openpype/tools/ayon_workfiles/control.py
@@ -573,6 +573,7 @@ def copy_workfile_representation(
workdir,
filename,
template_key,
+ src_filepath=representation_filepath
)
except Exception:
failed = True
diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py
index 13d007dd356..f9b8bcc5129 100644
--- a/openpype/tools/publisher/control.py
+++ b/openpype/tools/publisher/control.py
@@ -2337,7 +2337,11 @@ def run_action(self, plugin_id, action_id):
"title": "Action failed",
"message": "Action failed.",
"traceback": "".join(
- traceback.format_exception(exception)
+ traceback.format_exception(
+ type(exception),
+ exception,
+ exception.__traceback__
+ )
),
"label": action.__name__,
"identifier": action.id
diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py
index ecccc4e0c8e..2b497707faf 100644
--- a/openpype/tools/publisher/widgets/widgets.py
+++ b/openpype/tools/publisher/widgets/widgets.py
@@ -1485,7 +1485,7 @@ def _input_value_changed(self, value, attr_id):
class PublishPluginAttrsWidget(QtWidgets.QWidget):
"""Widget showing publsish plugin attributes for selected instances.
- Attributes are defined on publish plugins. Publihs plugin may define
+ Attributes are defined on publish plugins. Publish plugin may define
attribute definitions but must inherit `OpenPypePyblishPluginMixin`
(~/openpype/pipeline/publish). At the moment requires to implement
`get_attribute_defs` and `convert_attribute_values` class methods.
diff --git a/openpype/version.py b/openpype/version.py
index 95203e17c9b..2210cf404d1 100644
--- a/openpype/version.py
+++ b/openpype/version.py
@@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring Pype version."""
-__version__ = "3.18.8-nightly.1"
+__version__ = "3.18.9-nightly.10"
diff --git a/pyproject.toml b/pyproject.toml
index eef6a2e978a..bbff2176bfc 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "OpenPype"
-version = "3.18.7" # OpenPype
+version = "3.18.8" # OpenPype
description = "Open VFX and Animation pipeline with support."
authors = ["OpenPype Team "]
license = "MIT License"
diff --git a/server_addon/max/server/settings/publishers.py b/server_addon/max/server/settings/publishers.py
index da782cb4949..9c301d10b7c 100644
--- a/server_addon/max/server/settings/publishers.py
+++ b/server_addon/max/server/settings/publishers.py
@@ -27,6 +27,17 @@ def validate_json(cls, value):
return value
+class ValidateCameraAttributesModel(BaseSettingsModel):
+ enabled: bool = SettingsField(title="Enabled")
+ optional: bool = SettingsField(title="Optional")
+ active: bool = SettingsField(title="Active")
+ fov: float = SettingsField(0.0, title="Focal Length")
+ nearrange: float = SettingsField(0.0, title="Near Range")
+ farrange: float = SettingsField(0.0, title="Far Range")
+ nearclip: float = SettingsField(0.0, title="Near Clip")
+ farclip: float = SettingsField(0.0, title="Far Clip")
+
+
class FamilyMappingItemModel(BaseSettingsModel):
product_types: list[str] = SettingsField(
default_factory=list,
@@ -63,7 +74,14 @@ class PublishersModel(BaseSettingsModel):
default_factory=ValidateAttributesModel,
title="Validate Attributes"
)
-
+ ValidateCameraAttributes: ValidateCameraAttributesModel = SettingsField(
+ default_factory=ValidateCameraAttributesModel,
+ title="Validate Camera Attributes",
+ description=(
+ "If the value of the camera attributes set to 0, "
+ "the system automatically skips checking it"
+ )
+ )
ValidateLoadedPlugin: ValidateLoadedPluginModel = SettingsField(
default_factory=ValidateLoadedPluginModel,
title="Validate Loaded Plugin"
@@ -101,6 +119,16 @@ class PublishersModel(BaseSettingsModel):
"enabled": False,
"attributes": "{}"
},
+ "ValidateCameraAttributes": {
+ "enabled": True,
+ "optional": True,
+ "active": False,
+ "fov": 45.0,
+ "nearrange": 0.0,
+ "farrange": 1000.0,
+ "nearclip": 1.0,
+ "farclip": 1000.0
+ },
"ValidateLoadedPlugin": {
"enabled": False,
"optional": True,
diff --git a/server_addon/max/server/version.py b/server_addon/max/server/version.py
index bbab0242f6a..1276d0254ff 100644
--- a/server_addon/max/server/version.py
+++ b/server_addon/max/server/version.py
@@ -1 +1 @@
-__version__ = "0.1.4"
+__version__ = "0.1.5"
diff --git a/server_addon/substancepainter/server/settings/load_plugins.py b/server_addon/substancepainter/server/settings/load_plugins.py
new file mode 100644
index 00000000000..e6b2fd86c3b
--- /dev/null
+++ b/server_addon/substancepainter/server/settings/load_plugins.py
@@ -0,0 +1,122 @@
+from ayon_server.settings import BaseSettingsModel, SettingsField
+
+
+def normal_map_format_enum():
+ return [
+ {"label": "DirectX", "value": "NormalMapFormat.DirectX"},
+ {"label": "OpenGL", "value": "NormalMapFormat.OpenGL"},
+ ]
+
+
+def tangent_space_enum():
+ return [
+ {"label": "Per Fragment", "value": "TangentSpace.PerFragment"},
+ {"label": "Per Vertex", "value": "TangentSpace.PerVertex"},
+ ]
+
+
+def uv_workflow_enum():
+ return [
+ {"label": "Default", "value": "ProjectWorkflow.Default"},
+ {"label": "UV Tile", "value": "ProjectWorkflow.UVTile"},
+ {"label": "Texture Set Per UV Tile",
+ "value": "ProjectWorkflow.TextureSetPerUVTile"}
+ ]
+
+
+def document_resolution_enum():
+ return [
+ {"label": "128", "value": 128},
+ {"label": "256", "value": 256},
+ {"label": "512", "value": 512},
+ {"label": "1024", "value": 1024},
+ {"label": "2048", "value": 2048},
+ {"label": "4096", "value": 4096}
+ ]
+
+
+class ProjectTemplatesModel(BaseSettingsModel):
+ _layout = "expanded"
+ name: str = SettingsField("default", title="Template Name")
+ default_texture_resolution: int = SettingsField(
+ 1024, enum_resolver=document_resolution_enum,
+ title="Document Resolution",
+ description=("Set texture resolution when "
+ "creating new project.")
+ )
+ import_cameras: bool = SettingsField(
+ True, title="Import Cameras",
+ description="Import cameras from the mesh file.")
+ normal_map_format: str = SettingsField(
+ "DirectX", enum_resolver=normal_map_format_enum,
+ title="Normal Map Format",
+ description=("Set normal map format when "
+ "creating new project.")
+ )
+ project_workflow: str = SettingsField(
+ "Default", enum_resolver=uv_workflow_enum,
+ title="UV Tile Settings",
+ description=("Set UV workflow when "
+ "creating new project.")
+ )
+ tangent_space_mode: str = SettingsField(
+ "PerFragment", enum_resolver=tangent_space_enum,
+ title="Tangent Space",
+ description=("An option to compute tangent space "
+ "when creating new project.")
+ )
+ preserve_strokes: bool = SettingsField(
+ True, title="Preserve Strokes",
+ description=("Preserve strokes positions on mesh.\n"
+ "(only relevant when loading into "
+ "existing project)")
+ )
+
+
+class ProjectTemplateSettingModel(BaseSettingsModel):
+ project_templates: list[ProjectTemplatesModel] = SettingsField(
+ default_factory=ProjectTemplatesModel,
+ title="Project Templates"
+ )
+
+
+class LoadersModel(BaseSettingsModel):
+ SubstanceLoadProjectMesh: ProjectTemplateSettingModel = SettingsField(
+ default_factory=ProjectTemplateSettingModel,
+ title="Load Mesh"
+ )
+
+
+DEFAULT_LOADER_SETTINGS = {
+ "SubstanceLoadProjectMesh": {
+ "project_templates": [
+ {
+ "name": "2K(Default)",
+ "default_texture_resolution": 2048,
+ "import_cameras": True,
+ "normal_map_format": "NormalMapFormat.DirectX",
+ "project_workflow": "ProjectWorkflow.Default",
+ "tangent_space_mode": "TangentSpace.PerFragment",
+ "preserve_strokes": True
+ },
+ {
+ "name": "2K(UV tile)",
+ "default_texture_resolution": 2048,
+ "import_cameras": True,
+ "normal_map_format": "NormalMapFormat.DirectX",
+ "project_workflow": "ProjectWorkflow.UVTile",
+ "tangent_space_mode": "TangentSpace.PerFragment",
+ "preserve_strokes": True
+ },
+ {
+ "name": "4K(Custom)",
+ "default_texture_resolution": 4096,
+ "import_cameras": True,
+ "normal_map_format": "NormalMapFormat.OpenGL",
+ "project_workflow": "ProjectWorkflow.UVTile",
+ "tangent_space_mode": "TangentSpace.PerFragment",
+ "preserve_strokes": True
+ }
+ ]
+ }
+}
diff --git a/server_addon/substancepainter/server/settings/main.py b/server_addon/substancepainter/server/settings/main.py
index f80fa9fe1ec..93523fd6507 100644
--- a/server_addon/substancepainter/server/settings/main.py
+++ b/server_addon/substancepainter/server/settings/main.py
@@ -1,5 +1,6 @@
from ayon_server.settings import BaseSettingsModel, SettingsField
from .imageio import ImageIOSettings, DEFAULT_IMAGEIO_SETTINGS
+from .load_plugins import LoadersModel, DEFAULT_LOADER_SETTINGS
class ShelvesSettingsModel(BaseSettingsModel):
@@ -17,9 +18,12 @@ class SubstancePainterSettings(BaseSettingsModel):
default_factory=list,
title="Shelves"
)
+ load: LoadersModel = SettingsField(
+ default_factory=DEFAULT_LOADER_SETTINGS, title="Loaders")
DEFAULT_SPAINTER_SETTINGS = {
"imageio": DEFAULT_IMAGEIO_SETTINGS,
- "shelves": []
+ "shelves": [],
+ "load": DEFAULT_LOADER_SETTINGS,
}
diff --git a/server_addon/substancepainter/server/version.py b/server_addon/substancepainter/server/version.py
index 3dc1f76bc69..485f44ac21b 100644
--- a/server_addon/substancepainter/server/version.py
+++ b/server_addon/substancepainter/server/version.py
@@ -1 +1 @@
-__version__ = "0.1.0"
+__version__ = "0.1.1"