mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-01 08:24:53 +01:00
[Automated] Merged develop into main
This commit is contained in:
commit
74b1ff580a
24 changed files with 432 additions and 149 deletions
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -35,6 +35,7 @@ body:
|
||||||
label: Version
|
label: Version
|
||||||
description: What version are you running? Look to OpenPype Tray
|
description: What version are you running? Look to OpenPype Tray
|
||||||
options:
|
options:
|
||||||
|
- 3.15.7-nightly.3
|
||||||
- 3.15.7-nightly.2
|
- 3.15.7-nightly.2
|
||||||
- 3.15.7-nightly.1
|
- 3.15.7-nightly.1
|
||||||
- 3.15.6
|
- 3.15.6
|
||||||
|
|
@ -134,7 +135,6 @@ body:
|
||||||
- 3.14.1-nightly.4
|
- 3.14.1-nightly.4
|
||||||
- 3.14.1-nightly.3
|
- 3.14.1-nightly.3
|
||||||
- 3.14.1-nightly.2
|
- 3.14.1-nightly.2
|
||||||
- 3.14.1-nightly.1
|
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
from copy import deepcopy
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from openpype.hosts.fusion.api import (
|
from openpype.hosts.fusion.api import (
|
||||||
|
|
@ -11,15 +12,13 @@ from openpype.lib import (
|
||||||
)
|
)
|
||||||
from openpype.pipeline import (
|
from openpype.pipeline import (
|
||||||
legacy_io,
|
legacy_io,
|
||||||
Creator,
|
Creator as NewCreator,
|
||||||
CreatedInstance,
|
CreatedInstance,
|
||||||
)
|
Anatomy
|
||||||
from openpype.client import (
|
|
||||||
get_asset_by_name,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CreateSaver(Creator):
|
class CreateSaver(NewCreator):
|
||||||
identifier = "io.openpype.creators.fusion.saver"
|
identifier = "io.openpype.creators.fusion.saver"
|
||||||
label = "Render (saver)"
|
label = "Render (saver)"
|
||||||
name = "render"
|
name = "render"
|
||||||
|
|
@ -28,9 +27,24 @@ class CreateSaver(Creator):
|
||||||
description = "Fusion Saver to generate image sequence"
|
description = "Fusion Saver to generate image sequence"
|
||||||
icon = "fa5.eye"
|
icon = "fa5.eye"
|
||||||
|
|
||||||
instance_attributes = ["reviewable"]
|
instance_attributes = [
|
||||||
|
"reviewable"
|
||||||
|
]
|
||||||
|
default_variants = [
|
||||||
|
"Main",
|
||||||
|
"Mask"
|
||||||
|
]
|
||||||
|
|
||||||
|
# TODO: This should be renamed together with Nuke so it is aligned
|
||||||
|
temp_rendering_path_template = (
|
||||||
|
"{workdir}/renders/fusion/{subset}/{subset}.{frame}.{ext}")
|
||||||
|
|
||||||
def create(self, subset_name, instance_data, pre_create_data):
|
def create(self, subset_name, instance_data, pre_create_data):
|
||||||
|
instance_data.update({
|
||||||
|
"id": "pyblish.avalon.instance",
|
||||||
|
"subset": subset_name
|
||||||
|
})
|
||||||
|
|
||||||
# TODO: Add pre_create attributes to choose file format?
|
# TODO: Add pre_create attributes to choose file format?
|
||||||
file_format = "OpenEXRFormat"
|
file_format = "OpenEXRFormat"
|
||||||
|
|
||||||
|
|
@ -39,7 +53,6 @@ class CreateSaver(Creator):
|
||||||
args = (-32768, -32768) # Magical position numbers
|
args = (-32768, -32768) # Magical position numbers
|
||||||
saver = comp.AddTool("Saver", *args)
|
saver = comp.AddTool("Saver", *args)
|
||||||
|
|
||||||
instance_data["subset"] = subset_name
|
|
||||||
self._update_tool_with_data(saver, data=instance_data)
|
self._update_tool_with_data(saver, data=instance_data)
|
||||||
|
|
||||||
saver["OutputFormat"] = file_format
|
saver["OutputFormat"] = file_format
|
||||||
|
|
@ -78,7 +91,7 @@ class CreateSaver(Creator):
|
||||||
for tool in tools:
|
for tool in tools:
|
||||||
data = self.get_managed_tool_data(tool)
|
data = self.get_managed_tool_data(tool)
|
||||||
if not data:
|
if not data:
|
||||||
data = self._collect_unmanaged_saver(tool)
|
continue
|
||||||
|
|
||||||
# Add instance
|
# Add instance
|
||||||
created_instance = CreatedInstance.from_existing(data, self)
|
created_instance = CreatedInstance.from_existing(data, self)
|
||||||
|
|
@ -125,60 +138,35 @@ class CreateSaver(Creator):
|
||||||
original_subset = tool.GetData("openpype.subset")
|
original_subset = tool.GetData("openpype.subset")
|
||||||
subset = data["subset"]
|
subset = data["subset"]
|
||||||
if original_subset != subset:
|
if original_subset != subset:
|
||||||
# Subset change detected
|
self._configure_saver_tool(data, tool, subset)
|
||||||
# Update output filepath
|
|
||||||
workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"])
|
|
||||||
filename = f"{subset}..exr"
|
|
||||||
filepath = os.path.join(workdir, "render", subset, filename)
|
|
||||||
tool["Clip"] = filepath
|
|
||||||
|
|
||||||
# Rename tool
|
def _configure_saver_tool(self, data, tool, subset):
|
||||||
if tool.Name != subset:
|
formatting_data = deepcopy(data)
|
||||||
print(f"Renaming {tool.Name} -> {subset}")
|
|
||||||
tool.SetAttrs({"TOOLS_Name": subset})
|
|
||||||
|
|
||||||
def _collect_unmanaged_saver(self, tool):
|
# get frame padding from anatomy templates
|
||||||
# TODO: this should not be done this way - this should actually
|
anatomy = Anatomy()
|
||||||
# get the data as stored on the tool explicitly (however)
|
frame_padding = int(
|
||||||
# that would disallow any 'regular saver' to be collected
|
anatomy.templates["render"].get("frame_padding", 4)
|
||||||
# unless the instance data is stored on it to begin with
|
|
||||||
|
|
||||||
print("Collecting unmanaged saver..")
|
|
||||||
comp = tool.Comp()
|
|
||||||
|
|
||||||
# Allow regular non-managed savers to also be picked up
|
|
||||||
project = legacy_io.Session["AVALON_PROJECT"]
|
|
||||||
asset = legacy_io.Session["AVALON_ASSET"]
|
|
||||||
task = legacy_io.Session["AVALON_TASK"]
|
|
||||||
|
|
||||||
asset_doc = get_asset_by_name(project_name=project, asset_name=asset)
|
|
||||||
|
|
||||||
path = tool["Clip"][comp.TIME_UNDEFINED]
|
|
||||||
fname = os.path.basename(path)
|
|
||||||
fname, _ext = os.path.splitext(fname)
|
|
||||||
variant = fname.rstrip(".")
|
|
||||||
subset = self.get_subset_name(
|
|
||||||
variant=variant,
|
|
||||||
task_name=task,
|
|
||||||
asset_doc=asset_doc,
|
|
||||||
project_name=project,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
attrs = tool.GetAttrs()
|
# Subset change detected
|
||||||
passthrough = attrs["TOOLB_PassThrough"]
|
workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"])
|
||||||
return {
|
formatting_data.update({
|
||||||
# Required data
|
"workdir": workdir,
|
||||||
"project": project,
|
"frame": "0" * frame_padding,
|
||||||
"asset": asset,
|
"ext": "exr"
|
||||||
"subset": subset,
|
})
|
||||||
"task": task,
|
|
||||||
"variant": variant,
|
# build file path to render
|
||||||
"active": not passthrough,
|
filepath = self.temp_rendering_path_template.format(
|
||||||
"family": self.family,
|
**formatting_data)
|
||||||
# Unique identifier for instance and this creator
|
|
||||||
"id": "pyblish.avalon.instance",
|
tool["Clip"] = os.path.normpath(filepath)
|
||||||
"creator_identifier": self.identifier,
|
|
||||||
}
|
# Rename tool
|
||||||
|
if tool.Name != subset:
|
||||||
|
print(f"Renaming {tool.Name} -> {subset}")
|
||||||
|
tool.SetAttrs({"TOOLS_Name": subset})
|
||||||
|
|
||||||
def get_managed_tool_data(self, tool):
|
def get_managed_tool_data(self, tool):
|
||||||
"""Return data of the tool if it matches creator identifier"""
|
"""Return data of the tool if it matches creator identifier"""
|
||||||
|
|
@ -238,3 +226,25 @@ class CreateSaver(Creator):
|
||||||
default=("reviewable" in self.instance_attributes),
|
default=("reviewable" in self.instance_attributes),
|
||||||
label="Review",
|
label="Review",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def apply_settings(
|
||||||
|
self,
|
||||||
|
project_settings,
|
||||||
|
system_settings
|
||||||
|
):
|
||||||
|
"""Method called on initialization of plugin to apply settings."""
|
||||||
|
|
||||||
|
# plugin settings
|
||||||
|
plugin_settings = (
|
||||||
|
project_settings["fusion"]["create"][self.__class__.__name__]
|
||||||
|
)
|
||||||
|
|
||||||
|
# individual attributes
|
||||||
|
self.instance_attributes = plugin_settings.get(
|
||||||
|
"instance_attributes") or self.instance_attributes
|
||||||
|
self.default_variants = plugin_settings.get(
|
||||||
|
"default_variants") or self.default_variants
|
||||||
|
self.temp_rendering_path_template = (
|
||||||
|
plugin_settings.get("temp_rendering_path_template")
|
||||||
|
or self.temp_rendering_path_template
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,7 @@ def get_default_render_folder(project_setting=None):
|
||||||
["default_render_image_folder"])
|
["default_render_image_folder"])
|
||||||
|
|
||||||
|
|
||||||
def set_framerange(start_frame, end_frame):
|
def set_render_frame_range(start_frame, end_frame):
|
||||||
"""
|
"""
|
||||||
Note:
|
Note:
|
||||||
Frame range can be specified in different types. Possible values are:
|
Frame range can be specified in different types. Possible values are:
|
||||||
|
|
@ -150,10 +150,10 @@ def set_framerange(start_frame, end_frame):
|
||||||
Todo:
|
Todo:
|
||||||
Current type is hard-coded, there should be a custom setting for this.
|
Current type is hard-coded, there should be a custom setting for this.
|
||||||
"""
|
"""
|
||||||
rt.rendTimeType = 4
|
rt.rendTimeType = 3
|
||||||
if start_frame is not None and end_frame is not None:
|
if start_frame is not None and end_frame is not None:
|
||||||
frame_range = "{0}-{1}".format(start_frame, end_frame)
|
rt.rendStart = int(start_frame)
|
||||||
rt.rendPickupFrames = frame_range
|
rt.rendEnd = int(end_frame)
|
||||||
|
|
||||||
|
|
||||||
def get_multipass_setting(project_setting=None):
|
def get_multipass_setting(project_setting=None):
|
||||||
|
|
@ -173,10 +173,16 @@ def set_scene_resolution(width: int, height: int):
|
||||||
None
|
None
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
# make sure the render dialog is closed
|
||||||
|
# for the update of resolution
|
||||||
|
# Changing the Render Setup dialog settingsshould be done
|
||||||
|
# with the actual Render Setup dialog in a closed state.
|
||||||
|
if rt.renderSceneDialog.isOpen():
|
||||||
|
rt.renderSceneDialog.close()
|
||||||
|
|
||||||
rt.renderWidth = width
|
rt.renderWidth = width
|
||||||
rt.renderHeight = height
|
rt.renderHeight = height
|
||||||
|
|
||||||
|
|
||||||
def reset_scene_resolution():
|
def reset_scene_resolution():
|
||||||
"""Apply the scene resolution from the project definition
|
"""Apply the scene resolution from the project definition
|
||||||
|
|
||||||
|
|
@ -243,6 +249,7 @@ def reset_frame_range(fps: bool = True):
|
||||||
frame_end = frame_range["frameEnd"] + int(frame_range["handleEnd"])
|
frame_end = frame_range["frameEnd"] + int(frame_range["handleEnd"])
|
||||||
frange_cmd = f"animationRange = interval {frame_start} {frame_end}"
|
frange_cmd = f"animationRange = interval {frame_start} {frame_end}"
|
||||||
rt.execute(frange_cmd)
|
rt.execute(frange_cmd)
|
||||||
|
set_render_frame_range(frame_start, frame_end)
|
||||||
|
|
||||||
|
|
||||||
def set_context_setting():
|
def set_context_setting():
|
||||||
|
|
@ -259,6 +266,7 @@ def set_context_setting():
|
||||||
None
|
None
|
||||||
"""
|
"""
|
||||||
reset_scene_resolution()
|
reset_scene_resolution()
|
||||||
|
reset_frame_range()
|
||||||
|
|
||||||
|
|
||||||
def get_max_version():
|
def get_max_version():
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,9 @@ class RenderProducts(object):
|
||||||
container)
|
container)
|
||||||
|
|
||||||
context = get_current_project_asset()
|
context = get_current_project_asset()
|
||||||
startFrame = context["data"].get("frameStart")
|
# TODO: change the frame range follows the current render setting
|
||||||
endFrame = context["data"].get("frameEnd") + 1
|
startFrame = int(rt.rendStart)
|
||||||
|
endFrame = int(rt.rendEnd) + 1
|
||||||
|
|
||||||
img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa
|
img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa
|
||||||
full_render_list = self.beauty_render_product(output_file,
|
full_render_list = self.beauty_render_product(output_file,
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ from openpype.pipeline import legacy_io
|
||||||
from openpype.pipeline.context_tools import get_current_project_asset
|
from openpype.pipeline.context_tools import get_current_project_asset
|
||||||
|
|
||||||
from openpype.hosts.max.api.lib import (
|
from openpype.hosts.max.api.lib import (
|
||||||
set_framerange,
|
set_render_frame_range,
|
||||||
get_current_renderer,
|
get_current_renderer,
|
||||||
get_default_render_folder
|
get_default_render_folder
|
||||||
)
|
)
|
||||||
|
|
@ -68,7 +68,7 @@ class RenderSettings(object):
|
||||||
# Set Frame Range
|
# Set Frame Range
|
||||||
frame_start = context["data"].get("frame_start")
|
frame_start = context["data"].get("frame_start")
|
||||||
frame_end = context["data"].get("frame_end")
|
frame_end = context["data"].get("frame_end")
|
||||||
set_framerange(frame_start, frame_end)
|
set_render_frame_range(frame_start, frame_end)
|
||||||
# get the production render
|
# get the production render
|
||||||
renderer_class = get_current_renderer()
|
renderer_class = get_current_renderer()
|
||||||
renderer = str(renderer_class).split(":")[0]
|
renderer = str(renderer_class).split(":")[0]
|
||||||
|
|
@ -105,6 +105,9 @@ class RenderSettings(object):
|
||||||
|
|
||||||
rt.rendSaveFile = True
|
rt.rendSaveFile = True
|
||||||
|
|
||||||
|
if rt.renderSceneDialog.isOpen():
|
||||||
|
rt.renderSceneDialog.close()
|
||||||
|
|
||||||
def arnold_setup(self):
|
def arnold_setup(self):
|
||||||
# get Arnold RenderView run in the background
|
# get Arnold RenderView run in the background
|
||||||
# for setting up renderable camera
|
# for setting up renderable camera
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ class MaxHost(HostBase, IWorkfileHost, ILoadHost, INewPublisher):
|
||||||
|
|
||||||
def context_setting():
|
def context_setting():
|
||||||
return lib.set_context_setting()
|
return lib.set_context_setting()
|
||||||
|
|
||||||
rt.callbacks.addScript(rt.Name('systemPostNew'),
|
rt.callbacks.addScript(rt.Name('systemPostNew'),
|
||||||
context_setting)
|
context_setting)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,11 @@ class CreateRender(plugin.MaxCreator):
|
||||||
# for additional work on the node:
|
# for additional work on the node:
|
||||||
# instance_node = rt.getNodeByName(instance.get("instance_node"))
|
# instance_node = rt.getNodeByName(instance.get("instance_node"))
|
||||||
|
|
||||||
|
# make sure the render dialog is closed
|
||||||
|
# for the update of resolution
|
||||||
|
# Changing the Render Setup dialog settings should be done
|
||||||
|
# with the actual Render Setup dialog in a closed state.
|
||||||
|
|
||||||
# set viewport camera for rendering(mandatory for deadline)
|
# set viewport camera for rendering(mandatory for deadline)
|
||||||
RenderSettings().set_render_camera(sel_obj)
|
RenderSettings().set_render_camera(sel_obj)
|
||||||
# set output paths for rendering(mandatory for deadline)
|
# set output paths for rendering(mandatory for deadline)
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,6 @@ class CollectRender(pyblish.api.InstancePlugin):
|
||||||
|
|
||||||
self.log.debug(f"Setting {version_int} to context.")
|
self.log.debug(f"Setting {version_int} to context.")
|
||||||
context.data["version"] = version_int
|
context.data["version"] = version_int
|
||||||
|
|
||||||
# setup the plugin as 3dsmax for the internal renderer
|
# setup the plugin as 3dsmax for the internal renderer
|
||||||
data = {
|
data = {
|
||||||
"subset": instance.name,
|
"subset": instance.name,
|
||||||
|
|
@ -59,8 +58,8 @@ class CollectRender(pyblish.api.InstancePlugin):
|
||||||
"source": filepath,
|
"source": filepath,
|
||||||
"expectedFiles": render_layer_files,
|
"expectedFiles": render_layer_files,
|
||||||
"plugin": "3dsmax",
|
"plugin": "3dsmax",
|
||||||
"frameStart": context.data['frameStart'],
|
"frameStart": int(rt.rendStart),
|
||||||
"frameEnd": context.data['frameEnd'],
|
"frameEnd": int(rt.rendEnd),
|
||||||
"version": version_int,
|
"version": version_int,
|
||||||
"farm": True
|
"farm": True
|
||||||
}
|
}
|
||||||
|
|
|
||||||
64
openpype/hosts/max/plugins/publish/validate_frame_range.py
Normal file
64
openpype/hosts/max/plugins/publish/validate_frame_range.py
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
import pyblish.api
|
||||||
|
|
||||||
|
from pymxs import runtime as rt
|
||||||
|
from openpype.pipeline import (
|
||||||
|
OptionalPyblishPluginMixin
|
||||||
|
)
|
||||||
|
from openpype.pipeline.publish import (
|
||||||
|
RepairAction,
|
||||||
|
ValidateContentsOrder,
|
||||||
|
PublishValidationError
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ValidateFrameRange(pyblish.api.InstancePlugin,
|
||||||
|
OptionalPyblishPluginMixin):
|
||||||
|
"""Validates the frame ranges.
|
||||||
|
|
||||||
|
This is an optional validator checking if the frame range on instance
|
||||||
|
matches the frame range specified for the asset.
|
||||||
|
|
||||||
|
It also validates render frame ranges of render layers.
|
||||||
|
|
||||||
|
Repair action will change everything to match the asset frame range.
|
||||||
|
|
||||||
|
This can be turned off by the artist to allow custom ranges.
|
||||||
|
"""
|
||||||
|
|
||||||
|
label = "Validate Frame Range"
|
||||||
|
order = ValidateContentsOrder
|
||||||
|
families = ["maxrender"]
|
||||||
|
hosts = ["max"]
|
||||||
|
optional = True
|
||||||
|
actions = [RepairAction]
|
||||||
|
|
||||||
|
def process(self, instance):
|
||||||
|
if not self.is_active(instance.data):
|
||||||
|
self.log.info("Skipping validation...")
|
||||||
|
return
|
||||||
|
context = instance.context
|
||||||
|
|
||||||
|
frame_start = int(context.data.get("frameStart"))
|
||||||
|
frame_end = int(context.data.get("frameEnd"))
|
||||||
|
|
||||||
|
inst_frame_start = int(instance.data.get("frameStart"))
|
||||||
|
inst_frame_end = int(instance.data.get("frameEnd"))
|
||||||
|
|
||||||
|
errors = []
|
||||||
|
if frame_start != inst_frame_start:
|
||||||
|
errors.append(
|
||||||
|
f"Start frame ({inst_frame_start}) on instance does not match " # noqa
|
||||||
|
f"with the start frame ({frame_start}) set on the asset data. ") # noqa
|
||||||
|
if frame_end != inst_frame_end:
|
||||||
|
errors.append(
|
||||||
|
f"End frame ({inst_frame_end}) on instance does not match "
|
||||||
|
f"with the end frame ({frame_start}) from the asset data. ")
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
errors.append("You can use repair action to fix it.")
|
||||||
|
raise PublishValidationError("\n".join(errors))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def repair(cls, instance):
|
||||||
|
rt.rendStart = instance.context.data.get("frameStart")
|
||||||
|
rt.rendEnd = instance.context.data.get("frameEnd")
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
import pyblish.api
|
||||||
|
from openpype.pipeline import (
|
||||||
|
PublishValidationError,
|
||||||
|
OptionalPyblishPluginMixin
|
||||||
|
)
|
||||||
|
from pymxs import runtime as rt
|
||||||
|
from openpype.hosts.max.api.lib import reset_scene_resolution
|
||||||
|
|
||||||
|
from openpype.pipeline.context_tools import (
|
||||||
|
get_current_project_asset,
|
||||||
|
get_current_project
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ValidateResolutionSetting(pyblish.api.InstancePlugin,
|
||||||
|
OptionalPyblishPluginMixin):
|
||||||
|
"""Validate the resolution setting aligned with DB"""
|
||||||
|
|
||||||
|
order = pyblish.api.ValidatorOrder - 0.01
|
||||||
|
families = ["maxrender"]
|
||||||
|
hosts = ["max"]
|
||||||
|
label = "Validate Resolution Setting"
|
||||||
|
optional = True
|
||||||
|
|
||||||
|
def process(self, instance):
|
||||||
|
if not self.is_active(instance.data):
|
||||||
|
return
|
||||||
|
width, height = self.get_db_resolution(instance)
|
||||||
|
current_width = rt.renderwidth
|
||||||
|
current_height = rt.renderHeight
|
||||||
|
if current_width != width and current_height != height:
|
||||||
|
raise PublishValidationError("Resolution Setting "
|
||||||
|
"not matching resolution "
|
||||||
|
"set on asset or shot.")
|
||||||
|
if current_width != width:
|
||||||
|
raise PublishValidationError("Width in Resolution Setting "
|
||||||
|
"not matching resolution set "
|
||||||
|
"on asset or shot.")
|
||||||
|
|
||||||
|
if current_height != height:
|
||||||
|
raise PublishValidationError("Height in Resolution Setting "
|
||||||
|
"not matching resolution set "
|
||||||
|
"on asset or shot.")
|
||||||
|
|
||||||
|
def get_db_resolution(self, instance):
|
||||||
|
data = ["data.resolutionWidth", "data.resolutionHeight"]
|
||||||
|
project_resolution = get_current_project(fields=data)
|
||||||
|
project_resolution_data = project_resolution["data"]
|
||||||
|
asset_resolution = get_current_project_asset(fields=data)
|
||||||
|
asset_resolution_data = asset_resolution["data"]
|
||||||
|
# Set project resolution
|
||||||
|
project_width = int(
|
||||||
|
project_resolution_data.get("resolutionWidth", 1920))
|
||||||
|
project_height = int(
|
||||||
|
project_resolution_data.get("resolutionHeight", 1080))
|
||||||
|
width = int(
|
||||||
|
asset_resolution_data.get("resolutionWidth", project_width))
|
||||||
|
height = int(
|
||||||
|
asset_resolution_data.get("resolutionHeight", project_height))
|
||||||
|
|
||||||
|
return width, height
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def repair(cls, instance):
|
||||||
|
reset_scene_resolution()
|
||||||
|
|
@ -50,7 +50,8 @@ class ValidateShaderName(pyblish.api.InstancePlugin):
|
||||||
asset_name = instance.data.get("asset", None)
|
asset_name = instance.data.get("asset", None)
|
||||||
|
|
||||||
# Check the number of connected shadingEngines per shape
|
# Check the number of connected shadingEngines per shape
|
||||||
r = re.compile(cls.regex)
|
regex_compile = re.compile(cls.regex)
|
||||||
|
error_message = "object {0} has invalid shader name {1}"
|
||||||
for shape in shapes:
|
for shape in shapes:
|
||||||
shading_engines = cmds.listConnections(shape,
|
shading_engines = cmds.listConnections(shape,
|
||||||
destination=True,
|
destination=True,
|
||||||
|
|
@ -60,19 +61,18 @@ class ValidateShaderName(pyblish.api.InstancePlugin):
|
||||||
)
|
)
|
||||||
|
|
||||||
for shader in shaders:
|
for shader in shaders:
|
||||||
m = r.match(cls.regex, shader)
|
m = regex_compile.match(shader)
|
||||||
if m is None:
|
if m is None:
|
||||||
invalid.append(shape)
|
invalid.append(shape)
|
||||||
cls.log.error(
|
cls.log.error(error_message.format(shape, shader))
|
||||||
"object {0} has invalid shader name {1}".format(shape,
|
|
||||||
shader)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
if 'asset' in r.groupindex:
|
if 'asset' in regex_compile.groupindex:
|
||||||
if m.group('asset') != asset_name:
|
if m.group('asset') != asset_name:
|
||||||
invalid.append(shape)
|
invalid.append(shape)
|
||||||
cls.log.error(("object {0} has invalid "
|
message = error_message
|
||||||
"shader name {1}").format(shape,
|
message += " with missing asset name \"{2}\""
|
||||||
shader))
|
cls.log.error(
|
||||||
|
message.format(shape, shader, asset_name)
|
||||||
|
)
|
||||||
|
|
||||||
return invalid
|
return invalid
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ from . import (
|
||||||
register_inventory_action_path,
|
register_inventory_action_path,
|
||||||
register_creator_plugin_path,
|
register_creator_plugin_path,
|
||||||
deregister_loader_plugin_path,
|
deregister_loader_plugin_path,
|
||||||
|
deregister_inventory_action_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -54,6 +55,7 @@ PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins")
|
||||||
# Global plugin paths
|
# Global plugin paths
|
||||||
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
|
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
|
||||||
LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
|
LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
|
||||||
|
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
|
||||||
|
|
||||||
|
|
||||||
def _get_modules_manager():
|
def _get_modules_manager():
|
||||||
|
|
@ -158,6 +160,7 @@ def install_openpype_plugins(project_name=None, host_name=None):
|
||||||
pyblish.api.register_plugin_path(PUBLISH_PATH)
|
pyblish.api.register_plugin_path(PUBLISH_PATH)
|
||||||
pyblish.api.register_discovery_filter(filter_pyblish_plugins)
|
pyblish.api.register_discovery_filter(filter_pyblish_plugins)
|
||||||
register_loader_plugin_path(LOAD_PATH)
|
register_loader_plugin_path(LOAD_PATH)
|
||||||
|
register_inventory_action_path(INVENTORY_PATH)
|
||||||
|
|
||||||
if host_name is None:
|
if host_name is None:
|
||||||
host_name = os.environ.get("AVALON_APP")
|
host_name = os.environ.get("AVALON_APP")
|
||||||
|
|
@ -223,6 +226,7 @@ def uninstall_host():
|
||||||
pyblish.api.deregister_plugin_path(PUBLISH_PATH)
|
pyblish.api.deregister_plugin_path(PUBLISH_PATH)
|
||||||
pyblish.api.deregister_discovery_filter(filter_pyblish_plugins)
|
pyblish.api.deregister_discovery_filter(filter_pyblish_plugins)
|
||||||
deregister_loader_plugin_path(LOAD_PATH)
|
deregister_loader_plugin_path(LOAD_PATH)
|
||||||
|
deregister_inventory_action_path(INVENTORY_PATH)
|
||||||
log.info("Global plug-ins unregistred")
|
log.info("Global plug-ins unregistred")
|
||||||
|
|
||||||
deregister_host()
|
deregister_host()
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ from openpype.lib.attribute_definitions import (
|
||||||
get_default_values,
|
get_default_values,
|
||||||
)
|
)
|
||||||
from openpype.host import IPublishHost, IWorkfileHost
|
from openpype.host import IPublishHost, IWorkfileHost
|
||||||
from openpype.pipeline import legacy_io
|
from openpype.pipeline import legacy_io, Anatomy
|
||||||
from openpype.pipeline.plugin_discover import DiscoverResult
|
from openpype.pipeline.plugin_discover import DiscoverResult
|
||||||
|
|
||||||
from .creator_plugins import (
|
from .creator_plugins import (
|
||||||
|
|
@ -1383,6 +1383,8 @@ class CreateContext:
|
||||||
self._current_task_name = None
|
self._current_task_name = None
|
||||||
self._current_workfile_path = None
|
self._current_workfile_path = None
|
||||||
|
|
||||||
|
self._current_project_anatomy = None
|
||||||
|
|
||||||
self._host_is_valid = host_is_valid
|
self._host_is_valid = host_is_valid
|
||||||
# Currently unused variable
|
# Currently unused variable
|
||||||
self.headless = headless
|
self.headless = headless
|
||||||
|
|
@ -1546,6 +1548,18 @@ class CreateContext:
|
||||||
|
|
||||||
return self._current_workfile_path
|
return self._current_workfile_path
|
||||||
|
|
||||||
|
def get_current_project_anatomy(self):
|
||||||
|
"""Project anatomy for current project.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Anatomy: Anatomy object ready to be used.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self._current_project_anatomy is None:
|
||||||
|
self._current_project_anatomy = Anatomy(
|
||||||
|
self._current_project_name)
|
||||||
|
return self._current_project_anatomy
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def context_has_changed(self):
|
def context_has_changed(self):
|
||||||
"""Host context has changed.
|
"""Host context has changed.
|
||||||
|
|
@ -1568,6 +1582,7 @@ class CreateContext:
|
||||||
)
|
)
|
||||||
|
|
||||||
project_name = property(get_current_project_name)
|
project_name = property(get_current_project_name)
|
||||||
|
project_anatomy = property(get_current_project_anatomy)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def log(self):
|
def log(self):
|
||||||
|
|
@ -1680,6 +1695,8 @@ class CreateContext:
|
||||||
self._current_task_name = task_name
|
self._current_task_name = task_name
|
||||||
self._current_workfile_path = workfile_path
|
self._current_workfile_path = workfile_path
|
||||||
|
|
||||||
|
self._current_project_anatomy = None
|
||||||
|
|
||||||
def reset_plugins(self, discover_publish_plugins=True):
|
def reset_plugins(self, discover_publish_plugins=True):
|
||||||
"""Reload plugins.
|
"""Reload plugins.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -231,10 +231,24 @@ class BaseCreator:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def project_name(self):
|
def project_name(self):
|
||||||
"""Family that plugin represents."""
|
"""Current project name.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Name of a project.
|
||||||
|
"""
|
||||||
|
|
||||||
return self.create_context.project_name
|
return self.create_context.project_name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def project_anatomy(self):
|
||||||
|
"""Current project anatomy.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Anatomy: Project anatomy object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.create_context.project_anatomy
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def host(self):
|
def host(self):
|
||||||
return self.create_context.host
|
return self.create_context.host
|
||||||
|
|
|
||||||
51
openpype/plugins/inventory/remove_and_load.py
Normal file
51
openpype/plugins/inventory/remove_and_load.py
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
from openpype.pipeline import InventoryAction
|
||||||
|
from openpype.pipeline import get_current_project_name
|
||||||
|
from openpype.pipeline.load.plugins import discover_loader_plugins
|
||||||
|
from openpype.pipeline.load.utils import (
|
||||||
|
get_loader_identifier,
|
||||||
|
remove_container,
|
||||||
|
load_container,
|
||||||
|
)
|
||||||
|
from openpype.client import get_representation_by_id
|
||||||
|
|
||||||
|
|
||||||
|
class RemoveAndLoad(InventoryAction):
|
||||||
|
"""Delete inventory item and reload it."""
|
||||||
|
|
||||||
|
label = "Remove and load"
|
||||||
|
icon = "refresh"
|
||||||
|
|
||||||
|
def process(self, containers):
|
||||||
|
project_name = get_current_project_name()
|
||||||
|
loaders_by_name = {
|
||||||
|
get_loader_identifier(plugin): plugin
|
||||||
|
for plugin in discover_loader_plugins(project_name=project_name)
|
||||||
|
}
|
||||||
|
for container in containers:
|
||||||
|
# Get loader
|
||||||
|
loader_name = container["loader"]
|
||||||
|
loader = loaders_by_name.get(loader_name, None)
|
||||||
|
if not loader:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Failed to get loader '{}', can't remove "
|
||||||
|
"and load container".format(loader_name)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get representation
|
||||||
|
representation = get_representation_by_id(
|
||||||
|
project_name, container["representation"]
|
||||||
|
)
|
||||||
|
if not representation:
|
||||||
|
self.log.warning(
|
||||||
|
"Skipping remove and load because representation id is not"
|
||||||
|
" found in database: '{}'".format(
|
||||||
|
container["representation"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Remove container
|
||||||
|
remove_container(container)
|
||||||
|
|
||||||
|
# Load container
|
||||||
|
load_container(loader, representation)
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
import os
|
|
||||||
import re
|
|
||||||
|
|
||||||
import clique
|
|
||||||
import pyblish.api
|
|
||||||
|
|
||||||
|
|
||||||
class ValidateSequenceFrames(pyblish.api.InstancePlugin):
|
|
||||||
"""Ensure the sequence of frames is complete
|
|
||||||
|
|
||||||
The files found in the folder are checked against the startFrame and
|
|
||||||
endFrame of the instance. If the first or last file is not
|
|
||||||
corresponding with the first or last frame it is flagged as invalid.
|
|
||||||
|
|
||||||
Used regular expression pattern handles numbers in the file names
|
|
||||||
(eg "Main_beauty.v001.1001.exr", "Main_beauty_v001.1001.exr",
|
|
||||||
"Main_beauty.1001.1001.exr") but not numbers behind frames (eg.
|
|
||||||
"Main_beauty.1001.v001.exr")
|
|
||||||
"""
|
|
||||||
|
|
||||||
order = pyblish.api.ValidatorOrder
|
|
||||||
label = "Validate Sequence Frames"
|
|
||||||
families = ["imagesequence", "render"]
|
|
||||||
hosts = ["shell", "unreal"]
|
|
||||||
|
|
||||||
def process(self, instance):
|
|
||||||
representations = instance.data.get("representations")
|
|
||||||
if not representations:
|
|
||||||
return
|
|
||||||
for repr in representations:
|
|
||||||
repr_files = repr["files"]
|
|
||||||
if isinstance(repr_files, str):
|
|
||||||
continue
|
|
||||||
|
|
||||||
ext = repr.get("ext")
|
|
||||||
if not ext:
|
|
||||||
_, ext = os.path.splitext(repr_files[0])
|
|
||||||
elif not ext.startswith("."):
|
|
||||||
ext = ".{}".format(ext)
|
|
||||||
pattern = r"\D?(?P<index>(?P<padding>0*)\d+){}$".format(
|
|
||||||
re.escape(ext))
|
|
||||||
patterns = [pattern]
|
|
||||||
|
|
||||||
collections, remainder = clique.assemble(
|
|
||||||
repr_files, minimum_items=1, patterns=patterns)
|
|
||||||
|
|
||||||
assert not remainder, "Must not have remainder"
|
|
||||||
assert len(collections) == 1, "Must detect single collection"
|
|
||||||
collection = collections[0]
|
|
||||||
frames = list(collection.indexes)
|
|
||||||
|
|
||||||
if instance.data.get("slate"):
|
|
||||||
# Slate is not part of the frame range
|
|
||||||
frames = frames[1:]
|
|
||||||
|
|
||||||
current_range = (frames[0], frames[-1])
|
|
||||||
|
|
||||||
required_range = (instance.data["frameStart"],
|
|
||||||
instance.data["frameEnd"])
|
|
||||||
|
|
||||||
if current_range != required_range:
|
|
||||||
raise ValueError(f"Invalid frame range: {current_range} - "
|
|
||||||
f"expected: {required_range}")
|
|
||||||
|
|
||||||
missing = collection.holes().indexes
|
|
||||||
assert not missing, "Missing frames: %s" % (missing,)
|
|
||||||
|
|
@ -21,5 +21,18 @@
|
||||||
"copy_path": "~/.openpype/hosts/fusion/profiles",
|
"copy_path": "~/.openpype/hosts/fusion/profiles",
|
||||||
"copy_status": false,
|
"copy_status": false,
|
||||||
"force_sync": false
|
"force_sync": false
|
||||||
|
},
|
||||||
|
"create": {
|
||||||
|
"CreateSaver": {
|
||||||
|
"temp_rendering_path_template": "{workdir}/renders/fusion/{subset}/{subset}.{frame}.{ext}",
|
||||||
|
"default_variants": [
|
||||||
|
"Main",
|
||||||
|
"Mask"
|
||||||
|
],
|
||||||
|
"instance_attributes": [
|
||||||
|
"reviewable",
|
||||||
|
"farm_rendering"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,5 +19,12 @@
|
||||||
"custFloats": "custFloats",
|
"custFloats": "custFloats",
|
||||||
"custVecs": "custVecs"
|
"custVecs": "custVecs"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"publish": {
|
||||||
|
"ValidateFrameRange": {
|
||||||
|
"enabled": true,
|
||||||
|
"optional": true,
|
||||||
|
"active": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -734,6 +734,7 @@
|
||||||
"ValidateShaderName": {
|
"ValidateShaderName": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
|
"active": true,
|
||||||
"regex": "(?P<asset>.*)_(.*)_SHD"
|
"regex": "(?P<asset>.*)_(.*)_SHD"
|
||||||
},
|
},
|
||||||
"ValidateShadingEngine": {
|
"ValidateShadingEngine": {
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,50 @@
|
||||||
"label": "Resync profile on each launch"
|
"label": "Resync profile on each launch"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "dict",
|
||||||
|
"collapsible": true,
|
||||||
|
"key": "create",
|
||||||
|
"label": "Creator plugins",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "dict",
|
||||||
|
"collapsible": true,
|
||||||
|
"key": "CreateSaver",
|
||||||
|
"label": "Create Saver",
|
||||||
|
"is_group": true,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"key": "temp_rendering_path_template",
|
||||||
|
"label": "Temporary rendering path template"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "list",
|
||||||
|
"key": "default_variants",
|
||||||
|
"label": "Default variants",
|
||||||
|
"object_type": {
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "instance_attributes",
|
||||||
|
"label": "Instance attributes",
|
||||||
|
"type": "enum",
|
||||||
|
"multiselection": true,
|
||||||
|
"enum_items": [
|
||||||
|
{
|
||||||
|
"reviewable": "Reviewable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"farm_rendering": "Farm rendering"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "schema",
|
||||||
|
"name": "schema_max_publish"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"type": "dict",
|
||||||
|
"collapsible": true,
|
||||||
|
"key": "publish",
|
||||||
|
"label": "Publish plugins",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "dict",
|
||||||
|
"collapsible": true,
|
||||||
|
"checkbox_key": "enabled",
|
||||||
|
"key": "ValidateFrameRange",
|
||||||
|
"label": "Validate Frame Range",
|
||||||
|
"is_group": true,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"key": "enabled",
|
||||||
|
"label": "Enabled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"key": "optional",
|
||||||
|
"label": "Optional"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"key": "active",
|
||||||
|
"label": "Active"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -126,6 +126,11 @@
|
||||||
"key": "optional",
|
"key": "optional",
|
||||||
"label": "Optional"
|
"label": "Optional"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"key": "active",
|
||||||
|
"label": "Active"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "label",
|
"type": "label",
|
||||||
"label": "Shader name regex can use named capture group <b>asset</b> to validate against current asset name.<p><b>Example:</b><br/><code>^.*(?P=<asset>.+)_SHD</code></p>"
|
"label": "Shader name regex can use named capture group <b>asset</b> to validate against current asset name.<p><b>Example:</b><br/><code>^.*(?P=<asset>.+)_SHD</code></p>"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue