Merge branch 'develop' into bugfix/OP-6416_3dsmax-container-tab

This commit is contained in:
Kayla Man 2023-09-04 18:03:06 +08:00
commit edbecc50fb
98 changed files with 2186 additions and 615 deletions

View file

@ -138,7 +138,6 @@ class CollectAERender(publish.AbstractCollectRender):
fam = "render.farm"
if fam not in instance.families:
instance.families.append(fam)
instance.toBeRenderedOn = "deadline"
instance.renderer = "aerender"
instance.farm = True # to skip integrate
if "review" in instance.families:

View file

@ -108,7 +108,6 @@ class CollectFusionRender(
fam = "render.farm"
if fam not in instance.families:
instance.families.append(fam)
instance.toBeRenderedOn = "deadline"
instance.farm = True # to skip integrate
if "review" in instance.families:
# to skip ExtractReview locally

View file

@ -147,13 +147,13 @@ class CollectFarmRender(publish.AbstractCollectRender):
attachTo=False,
setMembers=[node],
publish=info[4],
review=False,
renderer=None,
priority=50,
name=node.split("/")[1],
family="render.farm",
families=["render.farm"],
farm=True,
resolutionWidth=context.data["resolutionWidth"],
resolutionHeight=context.data["resolutionHeight"],
@ -174,7 +174,6 @@ class CollectFarmRender(publish.AbstractCollectRender):
outputFormat=info[1],
outputStartFrame=info[3],
leadingZeros=info[2],
toBeRenderedOn='deadline',
ignoreFrameHandleCheck=True
)

View file

@ -22,9 +22,12 @@ log = logging.getLogger(__name__)
JSON_PREFIX = "JSON:::"
def get_asset_fps():
def get_asset_fps(asset_doc=None):
"""Return current asset fps."""
return get_current_project_asset()["data"].get("fps")
if asset_doc is None:
asset_doc = get_current_project_asset(fields=["data.fps"])
return asset_doc["data"]["fps"]
def set_id(node, unique_id, overwrite=False):
@ -472,14 +475,19 @@ def maintained_selection():
def reset_framerange():
"""Set frame range to current asset"""
"""Set frame range and FPS to current asset"""
# Get asset data
project_name = get_current_project_name()
asset_name = get_current_asset_name()
# Get the asset ID from the database for the asset of current context
asset_doc = get_asset_by_name(project_name, asset_name)
asset_data = asset_doc["data"]
# Get FPS
fps = get_asset_fps(asset_doc)
# Get Start and End Frames
frame_start = asset_data.get("frameStart")
frame_end = asset_data.get("frameEnd")
@ -493,6 +501,9 @@ def reset_framerange():
frame_start -= int(handle_start)
frame_end += int(handle_end)
# Set frame range and FPS
print("Setting scene FPS to {}".format(int(fps)))
set_scene_fps(fps)
hou.playbar.setFrameRange(frame_start, frame_end)
hou.playbar.setPlaybackRange(frame_start, frame_end)
hou.setFrame(frame_start)

View file

@ -25,7 +25,6 @@ from openpype.lib import (
emit_event,
)
from .lib import get_asset_fps
log = logging.getLogger("openpype.hosts.houdini")
@ -385,11 +384,6 @@ def _set_context_settings():
None
"""
# Set new scene fps
fps = get_asset_fps()
print("Setting scene FPS to %i" % fps)
lib.set_scene_fps(fps)
lib.reset_framerange()

View file

@ -33,7 +33,7 @@ class CreateVDBCache(plugin.HoudiniCreator):
}
if self.selected_nodes:
parms["soppath"] = self.selected_nodes[0].path()
parms["soppath"] = self.get_sop_node_path(self.selected_nodes[0])
instance_node.setParms(parms)
@ -42,3 +42,63 @@ class CreateVDBCache(plugin.HoudiniCreator):
hou.ropNodeTypeCategory(),
hou.sopNodeTypeCategory()
]
def get_sop_node_path(self, selected_node):
"""Get Sop Path of the selected node.
Although Houdini allows ObjNode path on `sop_path` for the
the ROP node, we prefer it set to the SopNode path explicitly.
"""
# Allow sop level paths (e.g. /obj/geo1/box1)
if isinstance(selected_node, hou.SopNode):
self.log.debug(
"Valid SopNode selection, 'SOP Path' in ROP will"
" be set to '%s'.", selected_node.path()
)
return selected_node.path()
# Allow object level paths to Geometry nodes (e.g. /obj/geo1)
# but do not allow other object level nodes types like cameras, etc.
elif isinstance(selected_node, hou.ObjNode) and \
selected_node.type().name() == "geo":
# Try to find output node.
sop_node = self.get_obj_output(selected_node)
if sop_node:
self.log.debug(
"Valid ObjNode selection, 'SOP Path' in ROP will "
"be set to the child path '%s'.", sop_node.path()
)
return sop_node.path()
self.log.debug(
"Selection isn't valid. 'SOP Path' in ROP will be empty."
)
return ""
def get_obj_output(self, obj_node):
"""Try to find output node.
If any output nodes are present, return the output node with
the minimum 'outputidx'
If no output nodes are present, return the node with display flag
If no nodes are present at all, return None
"""
outputs = obj_node.subnetOutputs()
# if obj_node is empty
if not outputs:
return
# if obj_node has one output child whether its
# sop output node or a node with the render flag
elif len(outputs) == 1:
return outputs[0]
# if there are more than one, then it has multiple output nodes
# return the one with the minimum 'outputidx'
else:
return min(outputs,
key=lambda node: node.evalParm('outputidx'))

View file

@ -59,6 +59,9 @@ class HdaLoader(load.LoaderPlugin):
def_paths = [d.libraryFilePath() for d in defs]
new = def_paths.index(file_path)
defs[new].setIsPreferred(True)
hda_node.setParms({
"representation": str(representation["_id"])
})
def remove(self, container):
node = container["node"]

View file

@ -2,7 +2,19 @@
<mainMenu>
<menuBar>
<subMenu id="openpype_menu">
<label>OpenPype</label>
<labelExpression><![CDATA[
import os
return os.environ.get("AVALON_LABEL") or "OpenPype"
]]></labelExpression>
<actionItem id="asset_name">
<labelExpression><![CDATA[
from openpype.pipeline import get_current_asset_name, get_current_task_name
label = "{}, {}".format(get_current_asset_name(), get_current_task_name())
return label
]]></labelExpression>
</actionItem>
<separatorItem/>
<scriptItem id="openpype_create">
<label>Create...</label>

View file

@ -43,7 +43,7 @@ class RenderSettings(object):
rt.viewport.setCamera(sel)
break
if not found:
raise RuntimeError("Camera not found")
raise RuntimeError("Active Camera not found")
def render_output(self, container):
folder = rt.maxFilePath
@ -113,7 +113,8 @@ class RenderSettings(object):
# for setting up renderable camera
arv = rt.MAXToAOps.ArnoldRenderView()
render_camera = rt.viewport.GetCamera()
arv.setOption("Camera", str(render_camera))
if render_camera:
arv.setOption("Camera", str(render_camera))
# TODO: add AOVs and extension
img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa

View file

@ -34,6 +34,9 @@ class CollectRender(pyblish.api.InstancePlugin):
aovs = RenderProducts().get_aovs(instance.name)
files_by_aov.update(aovs)
camera = rt.viewport.GetCamera()
instance.data["cameras"] = [camera.name] if camera else None # noqa
if "expectedFiles" not in instance.data:
instance.data["expectedFiles"] = list()
instance.data["files"] = list()

View file

@ -13,7 +13,6 @@ class ValidateMaxContents(pyblish.api.InstancePlugin):
order = pyblish.api.ValidatorOrder
families = ["camera",
"maxScene",
"maxrender",
"review"]
hosts = ["max"]
label = "Max Scene Contents"

View file

@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
import pyblish.api
from openpype.pipeline import (
PublishValidationError,
OptionalPyblishPluginMixin)
from openpype.pipeline.publish import RepairAction
from openpype.hosts.max.api.lib import get_current_renderer
from pymxs import runtime as rt
class ValidateRenderableCamera(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin):
"""Validates Renderable Camera
Check if the renderable camera used for rendering
"""
order = pyblish.api.ValidatorOrder
families = ["maxrender"]
hosts = ["max"]
label = "Renderable Camera"
optional = True
actions = [RepairAction]
def process(self, instance):
if not self.is_active(instance.data):
return
if not instance.data["cameras"]:
raise PublishValidationError(
"No renderable Camera found in scene."
)
@classmethod
def repair(cls, instance):
rt.viewport.setType(rt.Name("view_camera"))
camera = rt.viewport.GetCamera()
cls.log.info(f"Camera {camera} set as renderable camera")
renderer_class = get_current_renderer()
renderer = str(renderer_class).split(":")[0]
if renderer == "Arnold":
arv = rt.MAXToAOps.ArnoldRenderView()
arv.setOption("Camera", str(camera))
arv.close()
instance.data["cameras"] = [camera.name]

View file

@ -10,7 +10,6 @@ class CollectCurrentFile(pyblish.api.ContextPlugin):
order = pyblish.api.CollectorOrder - 0.4
label = "Maya Current File"
hosts = ['maya']
families = ["workfile"]
def process(self, context):
"""Inject the current working file"""

View file

@ -249,7 +249,6 @@ class MayaSubmitMuster(pyblish.api.InstancePlugin):
Authenticate with Muster, collect all data, prepare path for post
render publish job and submit job to farm.
"""
instance.data["toBeRenderedOn"] = "muster"
# setup muster environment
self.MUSTER_REST_URL = os.environ.get("MUSTER_REST_URL")

View file

@ -3,94 +3,19 @@
from __future__ import absolute_import
import pyblish.api
import openpype.hosts.maya.api.action
from openpype.pipeline.publish import (
ValidateContentsOrder, PublishValidationError
RepairAction,
ValidateContentsOrder,
PublishValidationError,
OptionalPyblishPluginMixin
)
from maya import cmds
class SelectInvalidInstances(pyblish.api.Action):
"""Select invalid instances in Outliner."""
label = "Select Instances"
icon = "briefcase"
on = "failed"
def process(self, context, plugin):
"""Process invalid validators and select invalid instances."""
# Get the errored instances
failed = []
for result in context.data["results"]:
if (
result["error"] is None
or result["instance"] is None
or result["instance"] in failed
or result["plugin"] != plugin
):
continue
failed.append(result["instance"])
# Apply pyblish.logic to get the instances for the plug-in
instances = pyblish.api.instances_by_plugin(failed, plugin)
if instances:
self.log.info(
"Selecting invalid nodes: %s" % ", ".join(
[str(x) for x in instances]
)
)
self.select(instances)
else:
self.log.info("No invalid nodes found.")
self.deselect()
def select(self, instances):
cmds.select(instances, replace=True, noExpand=True)
def deselect(self):
cmds.select(deselect=True)
class RepairSelectInvalidInstances(pyblish.api.Action):
"""Repair the instance asset."""
label = "Repair"
icon = "wrench"
on = "failed"
def process(self, context, plugin):
# Get the errored instances
failed = []
for result in context.data["results"]:
if result["error"] is None:
continue
if result["instance"] is None:
continue
if result["instance"] in failed:
continue
if result["plugin"] != plugin:
continue
failed.append(result["instance"])
# Apply pyblish.logic to get the instances for the plug-in
instances = pyblish.api.instances_by_plugin(failed, plugin)
context_asset = context.data["assetEntity"]["name"]
for instance in instances:
self.set_attribute(instance, context_asset)
def set_attribute(self, instance, context_asset):
cmds.setAttr(
instance.data.get("name") + ".asset",
context_asset,
type="string"
)
class ValidateInstanceInContext(pyblish.api.InstancePlugin):
class ValidateInstanceInContext(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin):
"""Validator to check if instance asset match context asset.
When working in per-shot style you always publish data in context of
@ -104,11 +29,49 @@ class ValidateInstanceInContext(pyblish.api.InstancePlugin):
label = "Instance in same Context"
optional = True
hosts = ["maya"]
actions = [SelectInvalidInstances, RepairSelectInvalidInstances]
actions = [
openpype.hosts.maya.api.action.SelectInvalidAction, RepairAction
]
def process(self, instance):
if not self.is_active(instance.data):
return
asset = instance.data.get("asset")
context_asset = instance.context.data["assetEntity"]["name"]
msg = "{} has asset {}".format(instance.name, asset)
context_asset = self.get_context_asset(instance)
if asset != context_asset:
raise PublishValidationError(msg)
raise PublishValidationError(
message=(
"Instance '{}' publishes to different asset than current "
"context: {}. Current context: {}".format(
instance.name, asset, context_asset
)
),
description=(
"## Publishing to a different asset\n"
"There are publish instances present which are publishing "
"into a different asset than your current context.\n\n"
"Usually this is not what you want but there can be cases "
"where you might want to publish into another asset or "
"shot. If that's the case you can disable the validation "
"on the instance to ignore it."
)
)
@classmethod
def get_invalid(cls, instance):
return [instance.data["instance_node"]]
@classmethod
def repair(cls, instance):
context_asset = cls.get_context_asset(instance)
instance_node = instance.data["instance_node"]
cmds.setAttr(
"{}.asset".format(instance_node),
context_asset,
type="string"
)
@staticmethod
def get_context_asset(instance):
return instance.context.data["assetEntity"]["name"]

View file

@ -4,6 +4,8 @@ from maya import cmds
import pyblish.api
from openpype.hosts.maya.api.lib import pairwise
from openpype.hosts.maya.api.action import SelectInvalidAction
from openpype.pipeline.publish import (
ValidateContentsOrder,
PublishValidationError
@ -19,31 +21,33 @@ class ValidatePluginPathAttributes(pyblish.api.InstancePlugin):
hosts = ['maya']
families = ["workfile"]
label = "Plug-in Path Attributes"
actions = [SelectInvalidAction]
def get_invalid(self, instance):
# Attributes are defined in project settings
attribute = []
@classmethod
def get_invalid(cls, instance):
invalid = list()
# get the project setting
validate_path = (
instance.context.data["project_settings"]["maya"]["publish"]
)
file_attr = validate_path["ValidatePluginPathAttributes"]["attribute"]
file_attr = cls.attribute
if not file_attr:
return invalid
# get the nodes and file attributes
for node, attr in file_attr.items():
# check the related nodes
targets = cmds.ls(type=node)
# Consider only valid node types to avoid "Unknown object type" warning
all_node_types = set(cmds.allNodeTypes())
node_types = [key for key in file_attr.keys() if key in all_node_types]
for target in targets:
# get the filepath
file_attr = "{}.{}".format(target, attr)
filepath = cmds.getAttr(file_attr)
for node, node_type in pairwise(cmds.ls(type=node_types,
showType=True)):
# get the filepath
file_attr = "{}.{}".format(node, file_attr[node_type])
filepath = cmds.getAttr(file_attr)
if filepath and not os.path.exists(filepath):
self.log.error("File {0} not exists".format(filepath)) # noqa
invalid.append(target)
if filepath and not os.path.exists(filepath):
cls.log.error("{} '{}' uses non-existing filepath: {}"
.format(node_type, node, filepath))
invalid.append(node)
return invalid
@ -51,5 +55,16 @@ class ValidatePluginPathAttributes(pyblish.api.InstancePlugin):
"""Process all directories Set as Filenames in Non-Maya Nodes"""
invalid = self.get_invalid(instance)
if invalid:
raise PublishValidationError("Non-existent Path "
"found: {0}".format(invalid))
raise PublishValidationError(
title="Plug-in Path Attributes",
message="Non-existent filepath found on nodes: {}".format(
", ".join(invalid)
),
description=(
"## Plug-in nodes use invalid filepaths\n"
"The workfile contains nodes from plug-ins that use "
"filepaths which do not exist.\n\n"
"Please make sure their filepaths are correct and the "
"files exist on disk."
)
)

View file

@ -7,6 +7,7 @@ from openpype.hosts.maya.api import lib
from openpype.pipeline.publish import (
ValidateContentsOrder,
RepairAction,
PublishValidationError
)
@ -67,5 +68,30 @@ class ValidateShapeZero(pyblish.api.Validator):
invalid = self.get_invalid(instance)
if invalid:
raise ValueError("Shapes found with non-zero component tweaks: "
"{0}".format(invalid))
raise PublishValidationError(
title="Shape Component Tweaks",
message="Shapes found with non-zero component tweaks: '{}'"
"".format(", ".join(invalid)),
description=(
"## Shapes found with component tweaks\n"
"Shapes were detected that have component tweaks on their "
"components. Please remove the component tweaks to "
"continue.\n\n"
"### Repair\n"
"The repair action will try to *freeze* the component "
"tweaks into the shapes, which is usually the correct fix "
"if the mesh has no construction history (= has its "
"history deleted)."),
detail=(
"Maya allows to store component tweaks within shape nodes "
"which are applied between its `inMesh` and `outMesh` "
"connections resulting in the output of a shape node "
"differing from the input. We usually want to avoid this "
"for published meshes (in particular for Maya scenes) as "
"it can have unintended results when using these meshes "
"as intermediate meshes since it applies positional "
"differences without being visible edits in the node "
"graph.\n\n"
"These tweaks are traditionally stored in the `.pnts` "
"attribute of shapes.")
)

View file

@ -2041,6 +2041,7 @@ class WorkfileSettings(object):
)
workfile_settings = imageio_host["workfile"]
viewer_process_settings = imageio_host["viewer"]["viewerProcess"]
if not config_data:
# TODO: backward compatibility for old projects - remove later
@ -2091,6 +2092,15 @@ class WorkfileSettings(object):
workfile_settings.pop("colorManagement", None)
workfile_settings.pop("OCIO_config", None)
# get monitor lut from settings respecting Nuke version differences
monitor_lut = workfile_settings.pop("monitorLut", None)
monitor_lut_data = self._get_monitor_settings(
viewer_process_settings, monitor_lut)
# set monitor related knobs luts (MonitorOut, Thumbnails)
for knob, value_ in monitor_lut_data.items():
workfile_settings[knob] = value_
# then set the rest
for knob, value_ in workfile_settings.items():
# skip unfilled ocio config path
@ -2107,8 +2117,9 @@ class WorkfileSettings(object):
# set ocio config path
if config_data:
config_path = config_data["path"].replace("\\", "/")
log.info("OCIO config path found: `{}`".format(
config_data["path"]))
config_path))
# check if there's a mismatch between environment and settings
correct_settings = self._is_settings_matching_environment(
@ -2118,6 +2129,40 @@ class WorkfileSettings(object):
if correct_settings:
self._set_ocio_config_path_to_workfile(config_data)
def _get_monitor_settings(self, viewer_lut, monitor_lut):
""" Get monitor settings from viewer and monitor lut
Args:
viewer_lut (str): viewer lut string
monitor_lut (str): monitor lut string
Returns:
dict: monitor settings
"""
output_data = {}
m_display, m_viewer = get_viewer_config_from_string(monitor_lut)
v_display, v_viewer = get_viewer_config_from_string(viewer_lut)
# set monitor lut differently for nuke version 14
if nuke.NUKE_VERSION_MAJOR >= 14:
output_data["monitorOutLUT"] = create_viewer_profile_string(
m_viewer, m_display, path_like=False)
# monitorLut=thumbnails - viewerProcess makes more sense
output_data["monitorLut"] = create_viewer_profile_string(
v_viewer, v_display, path_like=False)
if nuke.NUKE_VERSION_MAJOR == 13:
output_data["monitorOutLUT"] = create_viewer_profile_string(
m_viewer, m_display, path_like=False)
# monitorLut=thumbnails - viewerProcess makes more sense
output_data["monitorLut"] = create_viewer_profile_string(
v_viewer, v_display, path_like=True)
if nuke.NUKE_VERSION_MAJOR <= 12:
output_data["monitorLut"] = create_viewer_profile_string(
m_viewer, m_display, path_like=True)
return output_data
def _is_settings_matching_environment(self, config_data):
""" Check if OCIO config path is different from environment
@ -2177,6 +2222,7 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies.
"""
# replace path with env var if possible
ocio_path = self._replace_ocio_path_with_env_var(config_data)
ocio_path = ocio_path.replace("\\", "/")
log.info("Setting OCIO config path to: `{}`".format(
ocio_path))
@ -2232,7 +2278,7 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies.
Returns:
str: OCIO config path with environment variable TCL expression
"""
config_path = config_data["path"]
config_path = config_data["path"].replace("\\", "/")
config_template = config_data["template"]
included_vars = self._get_included_vars(config_template)
@ -3320,11 +3366,11 @@ def get_viewer_config_from_string(input_string):
display = split[0]
elif "(" in viewer:
pattern = r"([\w\d\s\.\-]+).*[(](.*)[)]"
result = re.findall(pattern, viewer)
result_ = re.findall(pattern, viewer)
try:
result = result.pop()
display = str(result[1]).rstrip()
viewer = str(result[0]).rstrip()
result_ = result_.pop()
display = str(result_[1]).rstrip()
viewer = str(result_[0]).rstrip()
except IndexError:
raise IndexError((
"Viewer Input string is not correct. "
@ -3332,3 +3378,22 @@ def get_viewer_config_from_string(input_string):
).format(input_string))
return (display, viewer)
def create_viewer_profile_string(viewer, display=None, path_like=False):
"""Convert viewer and display to string
Args:
viewer (str): viewer name
display (Optional[str]): display name
path_like (Optional[bool]): if True, return path like string
Returns:
str: viewer config string
"""
if not display:
return viewer
if path_like:
return "{}/{}".format(display, viewer)
return "{} ({})".format(viewer, display)

View file

@ -543,6 +543,9 @@ def list_instances(creator_id=None):
For SubsetManager
Args:
creator_id (Optional[str]): creator identifier
Returns:
(list) of dictionaries matching instances format
"""
@ -575,10 +578,13 @@ def list_instances(creator_id=None):
if creator_id and instance_data["creator_identifier"] != creator_id:
continue
if instance_data["instance_id"] in instance_ids:
instance_id = instance_data.get("instance_id")
if not instance_id:
pass
elif instance_id in instance_ids:
instance_data.pop("instance_id")
else:
instance_ids.add(instance_data["instance_id"])
instance_ids.add(instance_id)
# node name could change, so update subset name data
_update_subset_name_data(instance_data, node)

View file

@ -327,6 +327,7 @@ class NukeWriteCreator(NukeCreator):
"frames": "Use existing frames"
}
if ("farm_rendering" in self.instance_attributes):
rendering_targets["frames_farm"] = "Use existing frames - farm"
rendering_targets["farm"] = "Farm rendering"
return EnumDef(

View file

@ -2,11 +2,13 @@ import nuke
import pyblish.api
class CollectInstanceData(pyblish.api.InstancePlugin):
"""Collect all nodes with Avalon knob."""
class CollectNukeInstanceData(pyblish.api.InstancePlugin):
"""Collect Nuke instance data
"""
order = pyblish.api.CollectorOrder - 0.49
label = "Collect Instance Data"
label = "Collect Nuke Instance Data"
hosts = ["nuke", "nukeassist"]
# presets
@ -40,5 +42,14 @@ class CollectInstanceData(pyblish.api.InstancePlugin):
"pixelAspect": pixel_aspect
})
# add creator attributes to instance
creator_attributes = instance.data["creator_attributes"]
instance.data.update(creator_attributes)
# add review family if review activated on instance
if instance.data.get("review"):
instance.data["families"].append("review")
self.log.debug("Collected instance: {}".format(
instance.data))

View file

@ -5,7 +5,7 @@ import nuke
class CollectSlate(pyblish.api.InstancePlugin):
"""Check if SLATE node is in scene and connected to rendering tree"""
order = pyblish.api.CollectorOrder + 0.09
order = pyblish.api.CollectorOrder + 0.002
label = "Collect Slate Node"
hosts = ["nuke"]
families = ["render"]
@ -13,10 +13,14 @@ class CollectSlate(pyblish.api.InstancePlugin):
def process(self, instance):
node = instance.data["transientData"]["node"]
slate = next((n for n in nuke.allNodes()
if "slate" in n.name().lower()
if not n["disable"].getValue()),
None)
slate = next(
(
n_ for n_ in nuke.allNodes()
if "slate" in n_.name().lower()
if not n_["disable"].getValue()
),
None
)
if slate:
# check if slate node is connected to write node tree

View file

@ -1,5 +1,4 @@
import os
from pprint import pformat
import nuke
import pyblish.api
from openpype.hosts.nuke import api as napi
@ -15,30 +14,16 @@ class CollectNukeWrites(pyblish.api.InstancePlugin,
hosts = ["nuke", "nukeassist"]
families = ["render", "prerender", "image"]
# cache
_write_nodes = {}
_frame_ranges = {}
def process(self, instance):
self.log.debug(pformat(instance.data))
creator_attributes = instance.data["creator_attributes"]
instance.data.update(creator_attributes)
group_node = instance.data["transientData"]["node"]
render_target = instance.data["render_target"]
family = instance.data["family"]
families = instance.data["families"]
# add targeted family to families
instance.data["families"].append(
"{}.{}".format(family, render_target)
)
if instance.data.get("review"):
instance.data["families"].append("review")
child_nodes = napi.get_instance_group_node_childs(instance)
instance.data["transientData"]["childNodes"] = child_nodes
write_node = None
for x in child_nodes:
if x.Class() == "Write":
write_node = x
write_node = self._write_node_helper(instance)
if write_node is None:
self.log.warning(
@ -48,113 +33,134 @@ class CollectNukeWrites(pyblish.api.InstancePlugin,
)
return
instance.data["writeNode"] = write_node
self.log.debug("checking instance: {}".format(instance))
# get colorspace and add to version data
colorspace = napi.get_colorspace_from_node(write_node)
# Determine defined file type
ext = write_node["file_type"].value()
if render_target == "frames":
self._set_existing_files_data(instance, colorspace)
# Get frame range
handle_start = instance.context.data["handleStart"]
handle_end = instance.context.data["handleEnd"]
first_frame = int(nuke.root()["first_frame"].getValue())
last_frame = int(nuke.root()["last_frame"].getValue())
frame_length = int(last_frame - first_frame + 1)
elif render_target == "frames_farm":
collected_frames = self._set_existing_files_data(
instance, colorspace)
if write_node["use_limit"].getValue():
first_frame = int(write_node["first"].getValue())
last_frame = int(write_node["last"].getValue())
self._set_expected_files(instance, collected_frames)
self._add_farm_instance_data(instance)
elif render_target == "farm":
self._add_farm_instance_data(instance)
# set additional instance data
self._set_additional_instance_data(instance, render_target, colorspace)
def _set_existing_files_data(self, instance, colorspace):
"""Set existing files data to instance data.
Args:
instance (pyblish.api.Instance): pyblish instance
colorspace (str): colorspace
Returns:
list: collected frames
"""
collected_frames = self._get_collected_frames(instance)
representation = self._get_existing_frames_representation(
instance, collected_frames
)
# inject colorspace data
self.set_representation_colorspace(
representation, instance.context,
colorspace=colorspace
)
instance.data["representations"].append(representation)
return collected_frames
def _set_expected_files(self, instance, collected_frames):
"""Set expected files to instance data.
Args:
instance (pyblish.api.Instance): pyblish instance
collected_frames (list): collected frames
"""
write_node = self._write_node_helper(instance)
write_file_path = nuke.filename(write_node)
output_dir = os.path.dirname(write_file_path)
# get colorspace and add to version data
colorspace = napi.get_colorspace_from_node(write_node)
instance.data["expectedFiles"] = [
os.path.join(output_dir, source_file)
for source_file in collected_frames
]
self.log.debug('output dir: {}'.format(output_dir))
def _get_frame_range_data(self, instance):
"""Get frame range data from instance.
if render_target == "frames":
representation = {
'name': ext,
'ext': ext,
"stagingDir": output_dir,
"tags": []
}
Args:
instance (pyblish.api.Instance): pyblish instance
# get file path knob
node_file_knob = write_node["file"]
# list file paths based on input frames
expected_paths = list(sorted({
node_file_knob.evaluate(frame)
for frame in range(first_frame, last_frame + 1)
}))
Returns:
tuple: first_frame, last_frame
"""
# convert only to base names
expected_filenames = [
os.path.basename(filepath)
for filepath in expected_paths
]
instance_name = instance.data["name"]
# make sure files are existing at folder
collected_frames = [
filename
for filename in os.listdir(output_dir)
if filename in expected_filenames
]
if self._frame_ranges.get(instance_name):
# return cashed write node
return self._frame_ranges[instance_name]
if collected_frames:
collected_frames_len = len(collected_frames)
frame_start_str = "%0{}d".format(
len(str(last_frame))) % first_frame
representation['frameStart'] = frame_start_str
write_node = self._write_node_helper(instance)
# in case slate is expected and not yet rendered
self.log.debug("_ frame_length: {}".format(frame_length))
self.log.debug("_ collected_frames_len: {}".format(
collected_frames_len))
# Get frame range from workfile
first_frame = int(nuke.root()["first_frame"].getValue())
last_frame = int(nuke.root()["last_frame"].getValue())
# this will only run if slate frame is not already
# rendered from previews publishes
if (
"slate" in families
and frame_length == collected_frames_len
and family == "render"
):
frame_slate_str = (
"{{:0{}d}}".format(len(str(last_frame)))
).format(first_frame - 1)
# Get frame range from write node if activated
if write_node["use_limit"].getValue():
first_frame = int(write_node["first"].getValue())
last_frame = int(write_node["last"].getValue())
slate_frame = collected_frames[0].replace(
frame_start_str, frame_slate_str)
collected_frames.insert(0, slate_frame)
# add to cache
self._frame_ranges[instance_name] = (first_frame, last_frame)
if collected_frames_len == 1:
representation['files'] = collected_frames.pop()
else:
representation['files'] = collected_frames
return first_frame, last_frame
# inject colorspace data
self.set_representation_colorspace(
representation, instance.context,
colorspace=colorspace
)
def _set_additional_instance_data(
self, instance, render_target, colorspace
):
"""Set additional instance data.
instance.data["representations"].append(representation)
self.log.info("Publishing rendered frames ...")
Args:
instance (pyblish.api.Instance): pyblish instance
render_target (str): render target
colorspace (str): colorspace
"""
family = instance.data["family"]
elif render_target == "farm":
farm_keys = ["farm_chunk", "farm_priority", "farm_concurrency"]
for key in farm_keys:
# Skip if key is not in creator attributes
if key not in creator_attributes:
continue
# Add farm attributes to instance
instance.data[key] = creator_attributes[key]
# add targeted family to families
instance.data["families"].append(
"{}.{}".format(family, render_target)
)
self.log.debug("Appending render target to families: {}.{}".format(
family, render_target)
)
# Farm rendering
instance.data["transfer"] = False
instance.data["farm"] = True
self.log.info("Farm rendering ON ...")
write_node = self._write_node_helper(instance)
# Determine defined file type
ext = write_node["file_type"].value()
# get frame range data
handle_start = instance.context.data["handleStart"]
handle_end = instance.context.data["handleEnd"]
first_frame, last_frame = self._get_frame_range_data(instance)
# get output paths
write_file_path = nuke.filename(write_node)
output_dir = os.path.dirname(write_file_path)
# TODO: remove this when we have proper colorspace support
version_data = {
@ -188,10 +194,6 @@ class CollectNukeWrites(pyblish.api.InstancePlugin,
"frameEndHandle": last_frame,
})
# make sure rendered sequence on farm will
# be used for extract review
if not instance.data.get("review"):
instance.data["useSequenceForReview"] = False
# TODO temporarily set stagingDir as persistent for backward
# compatibility. This is mainly focused on `renders`folders which
@ -199,4 +201,201 @@ class CollectNukeWrites(pyblish.api.InstancePlugin,
# this logic should be removed and replaced with custom staging dir
instance.data["stagingDir_persistent"] = True
self.log.debug("instance.data: {}".format(pformat(instance.data)))
def _write_node_helper(self, instance):
"""Helper function to get write node from instance.
Also sets instance transient data with child nodes.
Args:
instance (pyblish.api.Instance): pyblish instance
Returns:
nuke.Node: write node
"""
instance_name = instance.data["name"]
if self._write_nodes.get(instance_name):
# return cashed write node
return self._write_nodes[instance_name]
# get all child nodes from group node
child_nodes = napi.get_instance_group_node_childs(instance)
# set child nodes to instance transient data
instance.data["transientData"]["childNodes"] = child_nodes
write_node = None
for node_ in child_nodes:
if node_.Class() == "Write":
write_node = node_
if write_node:
# for slate frame extraction
instance.data["transientData"]["writeNode"] = write_node
# add to cache
self._write_nodes[instance_name] = write_node
return self._write_nodes[instance_name]
def _get_existing_frames_representation(
self,
instance,
collected_frames
):
"""Get existing frames representation.
Args:
instance (pyblish.api.Instance): pyblish instance
collected_frames (list): collected frames
Returns:
dict: representation
"""
first_frame, last_frame = self._get_frame_range_data(instance)
write_node = self._write_node_helper(instance)
write_file_path = nuke.filename(write_node)
output_dir = os.path.dirname(write_file_path)
# Determine defined file type
ext = write_node["file_type"].value()
representation = {
"name": ext,
"ext": ext,
"stagingDir": output_dir,
"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,
collected_frames,
first_frame,
last_frame
)
if len(collected_frames) == 1:
representation['files'] = collected_frames.pop()
else:
representation['files'] = collected_frames
return representation
def _get_frame_start_str(self, first_frame, last_frame):
"""Get frame start string.
Args:
first_frame (int): first frame
last_frame (int): last frame
Returns:
str: frame start string
"""
# convert first frame to string with padding
return (
"{{:0{}d}}".format(len(str(last_frame)))
).format(first_frame)
def _add_slate_frame_to_collected_frames(
self,
instance,
collected_frames,
first_frame,
last_frame
):
"""Add slate frame to collected frames.
Args:
instance (pyblish.api.Instance): pyblish instance
collected_frames (list): collected frames
first_frame (int): first frame
last_frame (int): last frame
Returns:
list: collected frames
"""
frame_start_str = self._get_frame_start_str(first_frame, last_frame)
frame_length = int(last_frame - first_frame + 1)
# this will only run if slate frame is not already
# rendered from previews publishes
if (
"slate" in instance.data["families"]
and frame_length == len(collected_frames)
):
frame_slate_str = self._get_frame_start_str(
first_frame - 1,
last_frame
)
slate_frame = collected_frames[0].replace(
frame_start_str, frame_slate_str)
collected_frames.insert(0, slate_frame)
return collected_frames
def _add_farm_instance_data(self, instance):
"""Add farm publishing related instance data.
Args:
instance (pyblish.api.Instance): pyblish instance
"""
# make sure rendered sequence on farm will
# be used for extract review
if not instance.data.get("review"):
instance.data["useSequenceForReview"] = False
# Farm rendering
instance.data.update({
"transfer": False,
"farm": True # to skip integrate
})
self.log.info("Farm rendering ON ...")
def _get_collected_frames(self, instance):
"""Get collected frames.
Args:
instance (pyblish.api.Instance): pyblish instance
Returns:
list: collected frames
"""
first_frame, last_frame = self._get_frame_range_data(instance)
write_node = self._write_node_helper(instance)
write_file_path = nuke.filename(write_node)
output_dir = os.path.dirname(write_file_path)
# get file path knob
node_file_knob = write_node["file"]
# list file paths based on input frames
expected_paths = list(sorted({
node_file_knob.evaluate(frame)
for frame in range(first_frame, last_frame + 1)
}))
# convert only to base names
expected_filenames = {
os.path.basename(filepath)
for filepath in expected_paths
}
# make sure files are existing at folder
collected_frames = [
filename
for filename in os.listdir(output_dir)
if filename in expected_filenames
]
return collected_frames

View file

@ -11,9 +11,9 @@ from openpype.hosts.nuke.api.lib import maintained_selection
class ExtractCamera(publish.Extractor):
""" 3D camera exctractor
""" 3D camera extractor
"""
label = 'Exctract Camera'
label = 'Extract Camera'
order = pyblish.api.ExtractorOrder
families = ["camera"]
hosts = ["nuke"]

View file

@ -11,9 +11,9 @@ from openpype.hosts.nuke.api.lib import (
class ExtractModel(publish.Extractor):
""" 3D model exctractor
""" 3D model extractor
"""
label = 'Exctract Model'
label = 'Extract Model'
order = pyblish.api.ExtractorOrder
families = ["model"]
hosts = ["nuke"]

View file

@ -249,7 +249,7 @@ class ExtractSlateFrame(publish.Extractor):
# Add file to representation files
# - get write node
write_node = instance.data["writeNode"]
write_node = instance.data["transientData"]["writeNode"]
# - evaluate filepaths for first frame and slate frame
first_filename = os.path.basename(
write_node["file"].evaluate(first_frame))

View file

@ -54,6 +54,7 @@ class ExtractThumbnail(publish.Extractor):
def render_thumbnail(self, instance, output_name=None, **kwargs):
first_frame = instance.data["frameStartHandle"]
last_frame = instance.data["frameEndHandle"]
colorspace = instance.data["colorspace"]
# find frame range and define middle thumb frame
mid_frame = int((last_frame - first_frame) / 2)
@ -112,8 +113,8 @@ class ExtractThumbnail(publish.Extractor):
if self.use_rendered and os.path.isfile(path_render):
# check if file exist otherwise connect to write node
rnode = nuke.createNode("Read")
rnode["file"].setValue(path_render)
rnode["colorspace"].setValue(colorspace)
# turn it raw if none of baking is ON
if all([

View file

@ -14,27 +14,26 @@ class RepairActionBase(pyblish.api.Action):
# Get the errored instances
return get_errored_instances_from_context(context, plugin=plugin)
def repair_knob(self, instances, state):
def repair_knob(self, context, instances, state):
create_context = context.data["create_context"]
for instance in instances:
node = instance.data["transientData"]["node"]
files_remove = [os.path.join(instance.data["outputDir"], f)
for r in instance.data.get("representations", [])
for f in r.get("files", [])
]
self.log.info("Files to be removed: {}".format(files_remove))
for f in files_remove:
os.remove(f)
self.log.debug("removing file: {}".format(f))
node["render"].setValue(state)
# Reset the render knob
instance_id = instance.data.get("instance_id")
created_instance = create_context.get_instance_by_id(
instance_id
)
created_instance.creator_attributes["render_target"] = state
self.log.info("Rendering toggled to `{}`".format(state))
create_context.save_changes()
class RepairCollectionActionToLocal(RepairActionBase):
label = "Repair - rerender with \"Local\""
def process(self, context, plugin):
instances = self.get_instance(context, plugin)
self.repair_knob(instances, "Local")
self.repair_knob(context, instances, "local")
class RepairCollectionActionToFarm(RepairActionBase):
@ -42,7 +41,7 @@ class RepairCollectionActionToFarm(RepairActionBase):
def process(self, context, plugin):
instances = self.get_instance(context, plugin)
self.repair_knob(instances, "On farm")
self.repair_knob(context, instances, "farm")
class ValidateRenderedFrames(pyblish.api.InstancePlugin):

View file

@ -1,3 +1,5 @@
from collections import defaultdict
import pyblish.api
from openpype.pipeline.publish import get_errored_instances_from_context
from openpype.hosts.nuke.api.lib import (
@ -87,6 +89,11 @@ class ValidateNukeWriteNode(
correct_data
))
# Collect key values of same type in a list.
values_by_name = defaultdict(list)
for knob_data in correct_data["knobs"]:
values_by_name[knob_data["name"]].append(knob_data["value"])
for knob_data in correct_data["knobs"]:
knob_type = knob_data["type"]
self.log.debug("__ knob_type: {}".format(
@ -105,28 +112,33 @@ class ValidateNukeWriteNode(
)
key = knob_data["name"]
value = knob_data["value"]
values = values_by_name[key]
node_value = write_node[key].value()
# fix type differences
if type(node_value) in (int, float):
try:
if isinstance(value, list):
value = color_gui_to_int(value)
else:
value = float(value)
node_value = float(node_value)
except ValueError:
value = str(value)
else:
value = str(value)
node_value = str(node_value)
fixed_values = []
for value in values:
if type(node_value) in (int, float):
try:
self.log.debug("__ key: {} | value: {}".format(
key, value
if isinstance(value, list):
value = color_gui_to_int(value)
else:
value = float(value)
node_value = float(node_value)
except ValueError:
value = str(value)
else:
value = str(value)
node_value = str(node_value)
fixed_values.append(value)
self.log.debug("__ key: {} | values: {}".format(
key, fixed_values
))
if (
node_value != value
node_value not in fixed_values
and key != "file"
and key != "tile_color"
):

View file

@ -76,11 +76,16 @@ class AnimationAlembicLoader(plugin.Loader):
asset_name = "{}_{}".format(asset, name)
else:
asset_name = "{}".format(name)
version = context.get('version').get('name')
version = context.get('version')
# Check if version is hero version and use different name
if not version.get("name") and version.get('type') == "hero_version":
name_version = f"{name}_hero"
else:
name_version = f"{name}_v{version.get('name'):03d}"
tools = unreal.AssetToolsHelpers().get_asset_tools()
asset_dir, container_name = tools.create_unique_asset_name(
f"{root}/{asset}/{name}_v{version:03d}", suffix="")
f"{root}/{asset}/{name_version}", suffix="")
container_name += suffix

View file

@ -78,11 +78,16 @@ class SkeletalMeshAlembicLoader(plugin.Loader):
asset_name = "{}_{}".format(asset, name)
else:
asset_name = "{}".format(name)
version = context.get('version').get('name')
version = context.get('version')
# Check if version is hero version and use different name
if not version.get("name") and version.get('type') == "hero_version":
name_version = f"{name}_hero"
else:
name_version = f"{name}_v{version.get('name'):03d}"
tools = unreal.AssetToolsHelpers().get_asset_tools()
asset_dir, container_name = tools.create_unique_asset_name(
f"{root}/{asset}/{name}_v{version:03d}", suffix="")
f"{root}/{asset}/{name_version}", suffix="")
container_name += suffix

View file

@ -52,11 +52,16 @@ class SkeletalMeshFBXLoader(plugin.Loader):
asset_name = "{}_{}".format(asset, name)
else:
asset_name = "{}".format(name)
version = context.get('version').get('name')
version = context.get('version')
# Check if version is hero version and use different name
if not version.get("name") and version.get('type') == "hero_version":
name_version = f"{name}_hero"
else:
name_version = f"{name}_v{version.get('name'):03d}"
tools = unreal.AssetToolsHelpers().get_asset_tools()
asset_dir, container_name = tools.create_unique_asset_name(
f"{root}/{asset}/{name}_v{version:03d}", suffix="")
f"{root}/{asset}/{name_version}", suffix="")
container_name += suffix

View file

@ -79,11 +79,13 @@ class StaticMeshAlembicLoader(plugin.Loader):
root = "/Game/Ayon/Assets"
asset = context.get('asset').get('name')
suffix = "_CON"
if asset:
asset_name = "{}_{}".format(asset, name)
asset_name = f"{asset}_{name}" if asset else f"{name}"
version = context.get('version')
# Check if version is hero version and use different name
if not version.get("name") and version.get('type') == "hero_version":
name_version = f"{name}_hero"
else:
asset_name = "{}".format(name)
version = context.get('version').get('name')
name_version = f"{name}_v{version.get('name'):03d}"
default_conversion = False
if options.get("default_conversion"):
@ -91,7 +93,7 @@ class StaticMeshAlembicLoader(plugin.Loader):
tools = unreal.AssetToolsHelpers().get_asset_tools()
asset_dir, container_name = tools.create_unique_asset_name(
f"{root}/{asset}/{name}_v{version:03d}", suffix="")
f"{root}/{asset}/{name_version}", suffix="")
container_name += suffix

View file

@ -78,10 +78,16 @@ class StaticMeshFBXLoader(plugin.Loader):
asset_name = "{}_{}".format(asset, name)
else:
asset_name = "{}".format(name)
version = context.get('version')
# Check if version is hero version and use different name
if not version.get("name") and version.get('type') == "hero_version":
name_version = f"{name}_hero"
else:
name_version = f"{name}_v{version.get('name'):03d}"
tools = unreal.AssetToolsHelpers().get_asset_tools()
asset_dir, container_name = tools.create_unique_asset_name(
f"{root}/{asset}/{name}", suffix=""
f"{root}/{asset}/{name_version}", suffix=""
)
container_name += suffix

View file

@ -1,4 +1,6 @@
import clique
import os
import re
import pyblish.api
@ -21,7 +23,19 @@ class ValidateSequenceFrames(pyblish.api.InstancePlugin):
representations = instance.data.get("representations")
for repr in representations:
data = instance.data.get("assetEntity", {}).get("data", {})
patterns = [clique.PATTERNS["frames"]]
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)
@ -30,6 +44,10 @@ class ValidateSequenceFrames(pyblish.api.InstancePlugin):
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 = (data["clipIn"],
data["clipOut"])

View file

@ -280,13 +280,14 @@ class BatchPublishEndpoint(WebpublishApiEndpoint):
for key, value in add_args.items():
# Skip key values where value is None
if value is not None:
args.append("--{}".format(key))
# Extend list into arguments (targets can be a list)
if isinstance(value, (tuple, list)):
args.extend(value)
else:
args.append(value)
if value is None:
continue
arg_key = "--{}".format(key)
if not isinstance(value, (tuple, list)):
value = [value]
for item in value:
args += [arg_key, item]
log.info("args:: {}".format(args))
if add_to_queue: