Merge branch 'develop' into feature/OP-5207_Global-host-color-management-activation

This commit is contained in:
Ondřej Samohel 2023-06-09 14:02:25 +02:00 committed by GitHub
commit 869cb5ef15
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
73 changed files with 2054 additions and 400 deletions

View file

@ -855,12 +855,13 @@ def get_output_link_versions(project_name, version_id, fields=None):
return conn.find(query_filter, _prepare_fields(fields))
def get_last_versions(project_name, subset_ids, fields=None):
def get_last_versions(project_name, subset_ids, active=None, fields=None):
"""Latest versions for entered subset_ids.
Args:
project_name (str): Name of project where to look for queried entities.
subset_ids (Iterable[Union[str, ObjectId]]): List of subset ids.
active (Optional[bool]): If True only active versions are returned.
fields (Optional[Iterable[str]]): Fields that should be returned. All
fields are returned if 'None' is passed.
@ -899,12 +900,21 @@ def get_last_versions(project_name, subset_ids, fields=None):
if name_needed:
group_item["name"] = {"$last": "$name"}
aggregate_filter = {
"type": "version",
"parent": {"$in": subset_ids}
}
if active is False:
aggregate_filter["data.active"] = active
elif active is True:
aggregate_filter["$or"] = [
{"data.active": {"$exists": 0}},
{"data.active": active},
]
aggregation_pipeline = [
# Find all versions of those subsets
{"$match": {
"type": "version",
"parent": {"$in": subset_ids}
}},
{"$match": aggregate_filter},
# Sorting versions all together
{"$sort": {"name": 1}},
# Group them by "parent", but only take the last

View file

@ -0,0 +1,55 @@
from pathlib import Path
from openpype.lib import PreLaunchHook
class AddPythonScriptToLaunchArgs(PreLaunchHook):
"""Add python script to be executed before Blender launch."""
# Append after file argument
order = 15
app_groups = [
"blender",
]
def execute(self):
if not self.launch_context.data.get("python_scripts"):
return
# Add path to workfile to arguments
for python_script_path in self.launch_context.data["python_scripts"]:
self.log.info(
f"Adding python script {python_script_path} to launch"
)
# Test script path exists
python_script_path = Path(python_script_path)
if not python_script_path.exists():
self.log.warning(
f"Python script {python_script_path} doesn't exist. "
"Skipped..."
)
continue
if "--" in self.launch_context.launch_args:
# Insert before separator
separator_index = self.launch_context.launch_args.index("--")
self.launch_context.launch_args.insert(
separator_index,
"-P",
)
self.launch_context.launch_args.insert(
separator_index + 1,
python_script_path.as_posix(),
)
else:
self.launch_context.launch_args.extend(
["-P", python_script_path.as_posix()]
)
# Ensure separator
if "--" not in self.launch_context.launch_args:
self.launch_context.launch_args.append("--")
self.launch_context.launch_args.extend(
[*self.launch_context.data.get("script_args", [])]
)

View file

@ -0,0 +1,56 @@
import attr
import hou
from openpype.hosts.houdini.api.lib import get_color_management_preferences
@attr.s
class LayerMetadata(object):
"""Data class for Render Layer metadata."""
frameStart = attr.ib()
frameEnd = attr.ib()
@attr.s
class RenderProduct(object):
"""Getting Colorspace as
Specific Render Product Parameter for submitting
publish job.
"""
colorspace = attr.ib() # colorspace
view = attr.ib()
productName = attr.ib(default=None)
class ARenderProduct(object):
def __init__(self):
"""Constructor."""
# Initialize
self.layer_data = self._get_layer_data()
self.layer_data.products = self.get_colorspace_data()
def _get_layer_data(self):
return LayerMetadata(
frameStart=int(hou.playbar.frameRange()[0]),
frameEnd=int(hou.playbar.frameRange()[1]),
)
def get_colorspace_data(self):
"""To be implemented by renderer class.
This should return a list of RenderProducts.
Returns:
list: List of RenderProduct
"""
data = get_color_management_preferences()
colorspace_data = [
RenderProduct(
colorspace=data["display"],
view=data["view"],
productName=""
)
]
return colorspace_data

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import sys
import os
import re
import uuid
import logging
from contextlib import contextmanager
@ -581,3 +582,74 @@ def splitext(name, allowed_multidot_extensions):
return name[:-len(ext)], ext
return os.path.splitext(name)
def get_top_referenced_parm(parm):
processed = set() # disallow infinite loop
while True:
if parm.path() in processed:
raise RuntimeError("Parameter references result in cycle.")
processed.add(parm.path())
ref = parm.getReferencedParm()
if ref.path() == parm.path():
# It returns itself when it doesn't reference
# another parameter
return ref
else:
parm = ref
def evalParmNoFrame(node, parm, pad_character="#"):
parameter = node.parm(parm)
assert parameter, "Parameter does not exist: %s.%s" % (node, parm)
# If the parameter has a parameter reference, then get that
# parameter instead as otherwise `unexpandedString()` fails.
parameter = get_top_referenced_parm(parameter)
# Substitute out the frame numbering with padded characters
try:
raw = parameter.unexpandedString()
except hou.Error as exc:
print("Failed: %s" % parameter)
raise RuntimeError(exc)
def replace(match):
padding = 1
n = match.group(2)
if n and int(n):
padding = int(n)
return pad_character * padding
expression = re.sub(r"(\$F([0-9]*))", replace, raw)
with hou.ScriptEvalContext(parameter):
return hou.expandStringAtFrame(expression, 0)
def get_color_management_preferences():
"""Get default OCIO preferences"""
data = {
"config": hou.Color.ocio_configPath()
}
# Get default display and view from OCIO
display = hou.Color.ocio_defaultDisplay()
disp_regex = re.compile(r"^(?P<name>.+-)(?P<display>.+)$")
disp_match = disp_regex.match(display)
view = hou.Color.ocio_defaultView()
view_regex = re.compile(r"^(?P<name>.+- )(?P<view>.+)$")
view_match = view_regex.match(view)
data.update({
"display": disp_match.group("display"),
"view": view_match.group("view")
})
return data

View file

@ -0,0 +1,71 @@
from openpype.hosts.houdini.api import plugin
from openpype.lib import EnumDef
class CreateArnoldRop(plugin.HoudiniCreator):
"""Arnold ROP"""
identifier = "io.openpype.creators.houdini.arnold_rop"
label = "Arnold ROP"
family = "arnold_rop"
icon = "magic"
defaults = ["master"]
# Default extension
ext = "exr"
def create(self, subset_name, instance_data, pre_create_data):
import hou
# Remove the active, we are checking the bypass flag of the nodes
instance_data.pop("active", None)
instance_data.update({"node_type": "arnold"})
# Add chunk size attribute
instance_data["chunkSize"] = 1
# Submit for job publishing
instance_data["farm"] = True
instance = super(CreateArnoldRop, self).create(
subset_name,
instance_data,
pre_create_data) # type: plugin.CreatedInstance
instance_node = hou.node(instance.get("instance_node"))
ext = pre_create_data.get("image_format")
filepath = "{renders_dir}{subset_name}/{subset_name}.$F4.{ext}".format(
renders_dir=hou.text.expandString("$HIP/pyblish/renders/"),
subset_name=subset_name,
ext=ext,
)
parms = {
# Render frame range
"trange": 1,
# Arnold ROP settings
"ar_picture": filepath,
"ar_exr_half_precision": 1 # half precision
}
instance_node.setParms(parms)
# Lock any parameters in this list
to_lock = ["family", "id"]
self.lock_parameters(instance_node, to_lock)
def get_pre_create_attr_defs(self):
attrs = super(CreateArnoldRop, self).get_pre_create_attr_defs()
image_format_enum = [
"bmp", "cin", "exr", "jpg", "pic", "pic.gz", "png",
"rad", "rat", "rta", "sgi", "tga", "tif",
]
return attrs + [
EnumDef("image_format",
image_format_enum,
default=self.ext,
label="Image Format Options")
]

View file

@ -0,0 +1,114 @@
# -*- coding: utf-8 -*-
"""Creator plugin to create Karma ROP."""
from openpype.hosts.houdini.api import plugin
from openpype.pipeline import CreatedInstance
from openpype.lib import BoolDef, EnumDef, NumberDef
class CreateKarmaROP(plugin.HoudiniCreator):
"""Karma ROP"""
identifier = "io.openpype.creators.houdini.karma_rop"
label = "Karma ROP"
family = "karma_rop"
icon = "magic"
defaults = ["master"]
def create(self, subset_name, instance_data, pre_create_data):
import hou # noqa
instance_data.pop("active", None)
instance_data.update({"node_type": "karma"})
# Add chunk size attribute
instance_data["chunkSize"] = 10
# Submit for job publishing
instance_data["farm"] = True
instance = super(CreateKarmaROP, self).create(
subset_name,
instance_data,
pre_create_data) # type: CreatedInstance
instance_node = hou.node(instance.get("instance_node"))
ext = pre_create_data.get("image_format")
filepath = "{renders_dir}{subset_name}/{subset_name}.$F4.{ext}".format(
renders_dir=hou.text.expandString("$HIP/pyblish/renders/"),
subset_name=subset_name,
ext=ext,
)
checkpoint = "{cp_dir}{subset_name}.$F4.checkpoint".format(
cp_dir=hou.text.expandString("$HIP/pyblish/"),
subset_name=subset_name
)
usd_directory = "{usd_dir}{subset_name}_$RENDERID".format(
usd_dir=hou.text.expandString("$HIP/pyblish/renders/usd_renders/"), # noqa
subset_name=subset_name
)
parms = {
# Render Frame Range
"trange": 1,
# Karma ROP Setting
"picture": filepath,
# Karma Checkpoint Setting
"productName": checkpoint,
# USD Output Directory
"savetodirectory": usd_directory,
}
res_x = pre_create_data.get("res_x")
res_y = pre_create_data.get("res_y")
if self.selected_nodes:
# If camera found in selection
# we will use as render camera
camera = None
for node in self.selected_nodes:
if node.type().name() == "cam":
has_camera = pre_create_data.get("cam_res")
if has_camera:
res_x = node.evalParm("resx")
res_y = node.evalParm("resy")
if not camera:
self.log.warning("No render camera found in selection")
parms.update({
"camera": camera or "",
"resolutionx": res_x,
"resolutiony": res_y,
})
instance_node.setParms(parms)
# Lock some Avalon attributes
to_lock = ["family", "id"]
self.lock_parameters(instance_node, to_lock)
def get_pre_create_attr_defs(self):
attrs = super(CreateKarmaROP, self).get_pre_create_attr_defs()
image_format_enum = [
"bmp", "cin", "exr", "jpg", "pic", "pic.gz", "png",
"rad", "rat", "rta", "sgi", "tga", "tif",
]
return attrs + [
EnumDef("image_format",
image_format_enum,
default="exr",
label="Image Format Options"),
NumberDef("res_x",
label="width",
default=1920,
decimals=0),
NumberDef("res_y",
label="height",
default=720,
decimals=0),
BoolDef("cam_res",
label="Camera Resolution",
default=False)
]

View file

@ -0,0 +1,88 @@
# -*- coding: utf-8 -*-
"""Creator plugin to create Mantra ROP."""
from openpype.hosts.houdini.api import plugin
from openpype.pipeline import CreatedInstance
from openpype.lib import EnumDef, BoolDef
class CreateMantraROP(plugin.HoudiniCreator):
"""Mantra ROP"""
identifier = "io.openpype.creators.houdini.mantra_rop"
label = "Mantra ROP"
family = "mantra_rop"
icon = "magic"
defaults = ["master"]
def create(self, subset_name, instance_data, pre_create_data):
import hou # noqa
instance_data.pop("active", None)
instance_data.update({"node_type": "ifd"})
# Add chunk size attribute
instance_data["chunkSize"] = 10
# Submit for job publishing
instance_data["farm"] = True
instance = super(CreateMantraROP, self).create(
subset_name,
instance_data,
pre_create_data) # type: CreatedInstance
instance_node = hou.node(instance.get("instance_node"))
ext = pre_create_data.get("image_format")
filepath = "{renders_dir}{subset_name}/{subset_name}.$F4.{ext}".format(
renders_dir=hou.text.expandString("$HIP/pyblish/renders/"),
subset_name=subset_name,
ext=ext,
)
parms = {
# Render Frame Range
"trange": 1,
# Mantra ROP Setting
"vm_picture": filepath,
}
if self.selected_nodes:
# If camera found in selection
# we will use as render camera
camera = None
for node in self.selected_nodes:
if node.type().name() == "cam":
camera = node.path()
if not camera:
self.log.warning("No render camera found in selection")
parms.update({"camera": camera or ""})
custom_res = pre_create_data.get("override_resolution")
if custom_res:
parms.update({"override_camerares": 1})
instance_node.setParms(parms)
# Lock some Avalon attributes
to_lock = ["family", "id"]
self.lock_parameters(instance_node, to_lock)
def get_pre_create_attr_defs(self):
attrs = super(CreateMantraROP, self).get_pre_create_attr_defs()
image_format_enum = [
"bmp", "cin", "exr", "jpg", "pic", "pic.gz", "png",
"rad", "rat", "rta", "sgi", "tga", "tif",
]
return attrs + [
EnumDef("image_format",
image_format_enum,
default="exr",
label="Image Format Options"),
BoolDef("override_resolution",
label="Override Camera Resolution",
tooltip="Override the current camera "
"resolution, recommended for IPR.",
default=False)
]

View file

@ -1,7 +1,10 @@
# -*- coding: utf-8 -*-
"""Creator plugin to create Redshift ROP."""
import hou # noqa
from openpype.hosts.houdini.api import plugin
from openpype.pipeline import CreatedInstance
from openpype.lib import EnumDef
class CreateRedshiftROP(plugin.HoudiniCreator):
@ -11,20 +14,16 @@ class CreateRedshiftROP(plugin.HoudiniCreator):
family = "redshift_rop"
icon = "magic"
defaults = ["master"]
ext = "exr"
def create(self, subset_name, instance_data, pre_create_data):
import hou # noqa
instance_data.pop("active", None)
instance_data.update({"node_type": "Redshift_ROP"})
# Add chunk size attribute
instance_data["chunkSize"] = 10
# Clear the family prefix from the subset
subset = subset_name
subset_no_prefix = subset[len(self.family):]
subset_no_prefix = subset_no_prefix[0].lower() + subset_no_prefix[1:]
subset_name = subset_no_prefix
# Submit for job publishing
instance_data["farm"] = True
instance = super(CreateRedshiftROP, self).create(
subset_name,
@ -34,11 +33,10 @@ class CreateRedshiftROP(plugin.HoudiniCreator):
instance_node = hou.node(instance.get("instance_node"))
basename = instance_node.name()
instance_node.setName(basename + "_ROP", unique_name=True)
# Also create the linked Redshift IPR Rop
try:
ipr_rop = self.parent.createNode(
ipr_rop = instance_node.parent().createNode(
"Redshift_IPR", node_name=basename + "_IPR"
)
except hou.OperationFailed:
@ -50,19 +48,58 @@ class CreateRedshiftROP(plugin.HoudiniCreator):
ipr_rop.setPosition(instance_node.position() + hou.Vector2(0, -1))
# Set the linked rop to the Redshift ROP
ipr_rop.parm("linked_rop").set(ipr_rop.relativePathTo(instance))
ipr_rop.parm("linked_rop").set(instance_node.path())
ext = pre_create_data.get("image_format")
filepath = "{renders_dir}{subset_name}/{subset_name}.{fmt}".format(
renders_dir=hou.text.expandString("$HIP/pyblish/renders/"),
subset_name=subset_name,
fmt="${aov}.$F4.{ext}".format(aov="AOV", ext=ext)
)
prefix = '${HIP}/render/${HIPNAME}/`chs("subset")`.${AOV}.$F4.exr'
parms = {
# Render frame range
"trange": 1,
# Redshift ROP settings
"RS_outputFileNamePrefix": prefix,
"RS_outputMultilayerMode": 0, # no multi-layered exr
"RS_outputFileNamePrefix": filepath,
"RS_outputMultilayerMode": "1", # no multi-layered exr
"RS_outputBeautyAOVSuffix": "beauty",
}
if self.selected_nodes:
# set up the render camera from the selected node
camera = None
for node in self.selected_nodes:
if node.type().name() == "cam":
camera = node.path()
parms.update({
"RS_renderCamera": camera or ""})
instance_node.setParms(parms)
# Lock some Avalon attributes
to_lock = ["family", "id"]
self.lock_parameters(instance_node, to_lock)
def remove_instances(self, instances):
for instance in instances:
node = instance.data.get("instance_node")
ipr_node = hou.node(f"{node}_IPR")
if ipr_node:
ipr_node.destroy()
return super(CreateRedshiftROP, self).remove_instances(instances)
def get_pre_create_attr_defs(self):
attrs = super(CreateRedshiftROP, self).get_pre_create_attr_defs()
image_format_enum = [
"bmp", "cin", "exr", "jpg", "pic", "pic.gz", "png",
"rad", "rat", "rta", "sgi", "tga", "tif",
]
return attrs + [
EnumDef("image_format",
image_format_enum,
default=self.ext,
label="Image Format Options")
]

View file

@ -0,0 +1,156 @@
# -*- coding: utf-8 -*-
"""Creator plugin to create VRay ROP."""
import hou
from openpype.hosts.houdini.api import plugin
from openpype.pipeline import CreatedInstance
from openpype.lib import EnumDef, BoolDef
class CreateVrayROP(plugin.HoudiniCreator):
"""VRay ROP"""
identifier = "io.openpype.creators.houdini.vray_rop"
label = "VRay ROP"
family = "vray_rop"
icon = "magic"
defaults = ["master"]
ext = "exr"
def create(self, subset_name, instance_data, pre_create_data):
instance_data.pop("active", None)
instance_data.update({"node_type": "vray_renderer"})
# Add chunk size attribute
instance_data["chunkSize"] = 10
# Submit for job publishing
instance_data["farm"] = True
instance = super(CreateVrayROP, self).create(
subset_name,
instance_data,
pre_create_data) # type: CreatedInstance
instance_node = hou.node(instance.get("instance_node"))
# Add IPR for Vray
basename = instance_node.name()
try:
ipr_rop = instance_node.parent().createNode(
"vray", node_name=basename + "_IPR"
)
except hou.OperationFailed:
raise plugin.OpenPypeCreatorError(
"Cannot create Vray render node. "
"Make sure Vray installed and enabled!"
)
ipr_rop.setPosition(instance_node.position() + hou.Vector2(0, -1))
ipr_rop.parm("rop").set(instance_node.path())
parms = {
"trange": 1,
"SettingsEXR_bits_per_channel": "16" # half precision
}
if self.selected_nodes:
# set up the render camera from the selected node
camera = None
for node in self.selected_nodes:
if node.type().name() == "cam":
camera = node.path()
parms.update({
"render_camera": camera or ""
})
# Enable render element
ext = pre_create_data.get("image_format")
instance_data["RenderElement"] = pre_create_data.get("render_element_enabled") # noqa
if pre_create_data.get("render_element_enabled", True):
# Vray has its own tag for AOV file output
filepath = "{renders_dir}{subset_name}/{subset_name}.{fmt}".format(
renders_dir=hou.text.expandString("$HIP/pyblish/renders/"),
subset_name=subset_name,
fmt="${aov}.$F4.{ext}".format(aov="AOV",
ext=ext)
)
filepath = "{}{}".format(
hou.text.expandString("$HIP/pyblish/renders/"),
"{}/{}.${}.$F4.{}".format(subset_name,
subset_name,
"AOV",
ext)
)
re_rop = instance_node.parent().createNode(
"vray_render_channels",
node_name=basename + "_render_element"
)
# move the render element node next to the vray renderer node
re_rop.setPosition(instance_node.position() + hou.Vector2(0, 1))
re_path = re_rop.path()
parms.update({
"use_render_channels": 1,
"SettingsOutput_img_file_path": filepath,
"render_network_render_channels": re_path
})
else:
filepath = "{renders_dir}{subset_name}/{subset_name}.{fmt}".format(
renders_dir=hou.text.expandString("$HIP/pyblish/renders/"),
subset_name=subset_name,
fmt="$F4.{ext}".format(ext=ext)
)
parms.update({
"use_render_channels": 0,
"SettingsOutput_img_file_path": filepath
})
custom_res = pre_create_data.get("override_resolution")
if custom_res:
parms.update({"override_camerares": 1})
instance_node.setParms(parms)
# lock parameters from AVALON
to_lock = ["family", "id"]
self.lock_parameters(instance_node, to_lock)
def remove_instances(self, instances):
for instance in instances:
node = instance.data.get("instance_node")
# for the extra render node from the plugins
# such as vray and redshift
ipr_node = hou.node("{}{}".format(node, "_IPR"))
if ipr_node:
ipr_node.destroy()
re_node = hou.node("{}{}".format(node,
"_render_element"))
if re_node:
re_node.destroy()
return super(CreateVrayROP, self).remove_instances(instances)
def get_pre_create_attr_defs(self):
attrs = super(CreateVrayROP, self).get_pre_create_attr_defs()
image_format_enum = [
"bmp", "cin", "exr", "jpg", "pic", "pic.gz", "png",
"rad", "rat", "rta", "sgi", "tga", "tif",
]
return attrs + [
EnumDef("image_format",
image_format_enum,
default=self.ext,
label="Image Format Options"),
BoolDef("override_resolution",
label="Override Camera Resolution",
tooltip="Override the current camera "
"resolution, recommended for IPR.",
default=False),
BoolDef("render_element_enabled",
label="Render Element",
tooltip="Create Render Element Node "
"if enabled",
default=False)
]

View file

@ -0,0 +1,135 @@
import os
import re
import hou
import pyblish.api
from openpype.hosts.houdini.api import colorspace
from openpype.hosts.houdini.api.lib import (
evalParmNoFrame, get_color_management_preferences)
class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin):
"""Collect Arnold ROP Render Products
Collects the instance.data["files"] for the render products.
Provides:
instance -> files
"""
label = "Arnold ROP Render Products"
order = pyblish.api.CollectorOrder + 0.4
hosts = ["houdini"]
families = ["arnold_rop"]
def process(self, instance):
rop = hou.node(instance.data.get("instance_node"))
# Collect chunkSize
chunk_size_parm = rop.parm("chunkSize")
if chunk_size_parm:
chunk_size = int(chunk_size_parm.eval())
instance.data["chunkSize"] = chunk_size
self.log.debug("Chunk Size: %s" % chunk_size)
default_prefix = evalParmNoFrame(rop, "ar_picture")
render_products = []
# Default beauty AOV
beauty_product = self.get_render_product_name(prefix=default_prefix,
suffix=None)
render_products.append(beauty_product)
files_by_aov = {
"": self.generate_expected_files(instance, beauty_product)
}
num_aovs = rop.evalParm("ar_aovs")
for index in range(1, num_aovs + 1):
# Skip disabled AOVs
if not rop.evalParm("ar_enable_aovP{}".format(index)):
continue
if rop.evalParm("ar_aov_exr_enable_layer_name{}".format(index)):
label = rop.evalParm("ar_aov_exr_layer_name{}".format(index))
else:
label = evalParmNoFrame(rop, "ar_aov_label{}".format(index))
aov_product = self.get_render_product_name(default_prefix,
suffix=label)
render_products.append(aov_product)
files_by_aov[label] = self.generate_expected_files(instance,
aov_product)
for product in render_products:
self.log.debug("Found render product: {}".format(product))
instance.data["files"] = list(render_products)
instance.data["renderProducts"] = colorspace.ARenderProduct()
# For now by default do NOT try to publish the rendered output
instance.data["publishJobState"] = "Suspended"
instance.data["attachTo"] = [] # stub required data
if "expectedFiles" not in instance.data:
instance.data["expectedFiles"] = list()
instance.data["expectedFiles"].append(files_by_aov)
# update the colorspace data
colorspace_data = get_color_management_preferences()
instance.data["colorspaceConfig"] = colorspace_data["config"]
instance.data["colorspaceDisplay"] = colorspace_data["display"]
instance.data["colorspaceView"] = colorspace_data["view"]
def get_render_product_name(self, prefix, suffix):
"""Return the output filename using the AOV prefix and suffix"""
# When AOV is explicitly defined in prefix we just swap it out
# directly with the AOV suffix to embed it.
# Note: ${AOV} seems to be evaluated in the parameter as %AOV%
if "%AOV%" in prefix:
# It seems that when some special separator characters are present
# before the %AOV% token that Redshift will secretly remove it if
# there is no suffix for the current product, for example:
# foo_%AOV% -> foo.exr
pattern = "%AOV%" if suffix else "[._-]?%AOV%"
product_name = re.sub(pattern,
suffix,
prefix,
flags=re.IGNORECASE)
else:
if suffix:
# Add ".{suffix}" before the extension
prefix_base, ext = os.path.splitext(prefix)
product_name = prefix_base + "." + suffix + ext
else:
product_name = prefix
return product_name
def generate_expected_files(self, instance, path):
"""Create expected files in instance data"""
dir = os.path.dirname(path)
file = os.path.basename(path)
if "#" in file:
def replace(match):
return "%0{}d".format(len(match.group()))
file = re.sub("#+", replace, file)
if "%" not in file:
return path
expected_files = []
start = instance.data["frameStart"]
end = instance.data["frameEnd"]
for i in range(int(start), (int(end) + 1)):
expected_files.append(
os.path.join(dir, (file % i)).replace("\\", "/"))
return expected_files

View file

@ -11,15 +11,13 @@ from openpype.hosts.houdini.api import lib
class CollectFrames(pyblish.api.InstancePlugin):
"""Collect all frames which would be saved from the ROP nodes"""
order = pyblish.api.CollectorOrder
order = pyblish.api.CollectorOrder + 0.01
label = "Collect Frames"
families = ["vdbcache", "imagesequence", "ass", "redshiftproxy", "review"]
def process(self, instance):
ropnode = hou.node(instance.data["instance_node"])
frame_data = lib.get_frame_data(ropnode)
instance.data.update(frame_data)
start_frame = instance.data.get("frameStart", None)
end_frame = instance.data.get("frameEnd", None)

View file

@ -0,0 +1,56 @@
import hou
import pyblish.api
class CollectInstanceNodeFrameRange(pyblish.api.InstancePlugin):
"""Collect time range frame data for the instance node."""
order = pyblish.api.CollectorOrder + 0.001
label = "Instance Node Frame Range"
hosts = ["houdini"]
def process(self, instance):
node_path = instance.data.get("instance_node")
node = hou.node(node_path) if node_path else None
if not node_path or not node:
self.log.debug("No instance node found for instance: "
"{}".format(instance))
return
frame_data = self.get_frame_data(node)
if not frame_data:
return
self.log.info("Collected time data: {}".format(frame_data))
instance.data.update(frame_data)
def get_frame_data(self, node):
"""Get the frame data: start frame, end frame and steps
Args:
node(hou.Node)
Returns:
dict
"""
data = {}
if node.parm("trange") is None:
self.log.debug("Node has no 'trange' parameter: "
"{}".format(node.path()))
return data
if node.evalParm("trange") == 0:
# Ignore 'render current frame'
self.log.debug("Node '{}' has 'Render current frame' set. "
"Time range data ignored.".format(node.path()))
return data
data["frameStart"] = node.evalParm("f1")
data["frameEnd"] = node.evalParm("f2")
data["byFrameStep"] = node.evalParm("f3")
return data

View file

@ -70,16 +70,10 @@ class CollectInstances(pyblish.api.ContextPlugin):
if "active" in data:
data["publish"] = data["active"]
data.update(self.get_frame_data(node))
# Create nice name if the instance has a frame range.
label = data.get("name", node.name())
label += " (%s)" % data["asset"] # include asset in name
if "frameStart" in data and "frameEnd" in data:
frames = "[{frameStart} - {frameEnd}]".format(**data)
label = "{} {}".format(label, frames)
instance = context.create_instance(label)
# Include `families` using `family` data
@ -118,6 +112,6 @@ class CollectInstances(pyblish.api.ContextPlugin):
data["frameStart"] = node.evalParm("f1")
data["frameEnd"] = node.evalParm("f2")
data["steps"] = node.evalParm("f3")
data["byFrameStep"] = node.evalParm("f3")
return data

View file

@ -0,0 +1,104 @@
import re
import os
import hou
import pyblish.api
from openpype.hosts.houdini.api.lib import (
evalParmNoFrame,
get_color_management_preferences
)
from openpype.hosts.houdini.api import (
colorspace
)
class CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin):
"""Collect Karma Render Products
Collects the instance.data["files"] for the multipart render product.
Provides:
instance -> files
"""
label = "Karma ROP Render Products"
order = pyblish.api.CollectorOrder + 0.4
hosts = ["houdini"]
families = ["karma_rop"]
def process(self, instance):
rop = hou.node(instance.data.get("instance_node"))
# Collect chunkSize
chunk_size_parm = rop.parm("chunkSize")
if chunk_size_parm:
chunk_size = int(chunk_size_parm.eval())
instance.data["chunkSize"] = chunk_size
self.log.debug("Chunk Size: %s" % chunk_size)
default_prefix = evalParmNoFrame(rop, "picture")
render_products = []
# Default beauty AOV
beauty_product = self.get_render_product_name(
prefix=default_prefix, suffix=None
)
render_products.append(beauty_product)
files_by_aov = {
"beauty": self.generate_expected_files(instance,
beauty_product)
}
filenames = list(render_products)
instance.data["files"] = filenames
instance.data["renderProducts"] = colorspace.ARenderProduct()
for product in render_products:
self.log.debug("Found render product: %s" % product)
if "expectedFiles" not in instance.data:
instance.data["expectedFiles"] = list()
instance.data["expectedFiles"].append(files_by_aov)
# update the colorspace data
colorspace_data = get_color_management_preferences()
instance.data["colorspaceConfig"] = colorspace_data["config"]
instance.data["colorspaceDisplay"] = colorspace_data["display"]
instance.data["colorspaceView"] = colorspace_data["view"]
def get_render_product_name(self, prefix, suffix):
product_name = prefix
if suffix:
# Add ".{suffix}" before the extension
prefix_base, ext = os.path.splitext(prefix)
product_name = "{}.{}{}".format(prefix_base, suffix, ext)
return product_name
def generate_expected_files(self, instance, path):
"""Create expected files in instance data"""
dir = os.path.dirname(path)
file = os.path.basename(path)
if "#" in file:
def replace(match):
return "%0{}d".format(len(match.group()))
file = re.sub("#+", replace, file)
if "%" not in file:
return path
expected_files = []
start = instance.data["frameStart"]
end = instance.data["frameEnd"]
for i in range(int(start), (int(end) + 1)):
expected_files.append(
os.path.join(dir, (file % i)).replace("\\", "/"))
return expected_files

View file

@ -0,0 +1,127 @@
import re
import os
import hou
import pyblish.api
from openpype.hosts.houdini.api.lib import (
evalParmNoFrame,
get_color_management_preferences
)
from openpype.hosts.houdini.api import (
colorspace
)
class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin):
"""Collect Mantra Render Products
Collects the instance.data["files"] for the render products.
Provides:
instance -> files
"""
label = "Mantra ROP Render Products"
order = pyblish.api.CollectorOrder + 0.4
hosts = ["houdini"]
families = ["mantra_rop"]
def process(self, instance):
rop = hou.node(instance.data.get("instance_node"))
# Collect chunkSize
chunk_size_parm = rop.parm("chunkSize")
if chunk_size_parm:
chunk_size = int(chunk_size_parm.eval())
instance.data["chunkSize"] = chunk_size
self.log.debug("Chunk Size: %s" % chunk_size)
default_prefix = evalParmNoFrame(rop, "vm_picture")
render_products = []
# Default beauty AOV
beauty_product = self.get_render_product_name(
prefix=default_prefix, suffix=None
)
render_products.append(beauty_product)
files_by_aov = {
"beauty": self.generate_expected_files(instance,
beauty_product)
}
aov_numbers = rop.evalParm("vm_numaux")
if aov_numbers > 0:
# get the filenames of the AOVs
for i in range(1, aov_numbers + 1):
var = rop.evalParm("vm_variable_plane%d" % i)
if var:
aov_name = "vm_filename_plane%d" % i
aov_boolean = "vm_usefile_plane%d" % i
aov_enabled = rop.evalParm(aov_boolean)
has_aov_path = rop.evalParm(aov_name)
if has_aov_path and aov_enabled == 1:
aov_prefix = evalParmNoFrame(rop, aov_name)
aov_product = self.get_render_product_name(
prefix=aov_prefix, suffix=None
)
render_products.append(aov_product)
files_by_aov[var] = self.generate_expected_files(instance, aov_product) # noqa
for product in render_products:
self.log.debug("Found render product: %s" % product)
filenames = list(render_products)
instance.data["files"] = filenames
instance.data["renderProducts"] = colorspace.ARenderProduct()
# For now by default do NOT try to publish the rendered output
instance.data["publishJobState"] = "Suspended"
instance.data["attachTo"] = [] # stub required data
if "expectedFiles" not in instance.data:
instance.data["expectedFiles"] = list()
instance.data["expectedFiles"].append(files_by_aov)
# update the colorspace data
colorspace_data = get_color_management_preferences()
instance.data["colorspaceConfig"] = colorspace_data["config"]
instance.data["colorspaceDisplay"] = colorspace_data["display"]
instance.data["colorspaceView"] = colorspace_data["view"]
def get_render_product_name(self, prefix, suffix):
product_name = prefix
if suffix:
# Add ".{suffix}" before the extension
prefix_base, ext = os.path.splitext(prefix)
product_name = prefix_base + "." + suffix + ext
return product_name
def generate_expected_files(self, instance, path):
"""Create expected files in instance data"""
dir = os.path.dirname(path)
file = os.path.basename(path)
if "#" in file:
def replace(match):
return "%0{}d".format(len(match.group()))
file = re.sub("#+", replace, file)
if "%" not in file:
return path
expected_files = []
start = instance.data["frameStart"]
end = instance.data["frameEnd"]
for i in range(int(start), (int(end) + 1)):
expected_files.append(
os.path.join(dir, (file % i)).replace("\\", "/"))
return expected_files

View file

@ -4,52 +4,13 @@ import os
import hou
import pyblish.api
def get_top_referenced_parm(parm):
processed = set() # disallow infinite loop
while True:
if parm.path() in processed:
raise RuntimeError("Parameter references result in cycle.")
processed.add(parm.path())
ref = parm.getReferencedParm()
if ref.path() == parm.path():
# It returns itself when it doesn't reference
# another parameter
return ref
else:
parm = ref
def evalParmNoFrame(node, parm, pad_character="#"):
parameter = node.parm(parm)
assert parameter, "Parameter does not exist: %s.%s" % (node, parm)
# If the parameter has a parameter reference, then get that
# parameter instead as otherwise `unexpandedString()` fails.
parameter = get_top_referenced_parm(parameter)
# Substitute out the frame numbering with padded characters
try:
raw = parameter.unexpandedString()
except hou.Error as exc:
print("Failed: %s" % parameter)
raise RuntimeError(exc)
def replace(match):
padding = 1
n = match.group(2)
if n and int(n):
padding = int(n)
return pad_character * padding
expression = re.sub(r"(\$F([0-9]*))", replace, raw)
with hou.ScriptEvalContext(parameter):
return hou.expandStringAtFrame(expression, 0)
from openpype.hosts.houdini.api.lib import (
evalParmNoFrame,
get_color_management_preferences
)
from openpype.hosts.houdini.api import (
colorspace
)
class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin):
@ -87,6 +48,9 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin):
prefix=default_prefix, suffix=beauty_suffix
)
render_products.append(beauty_product)
files_by_aov = {
"_": self.generate_expected_files(instance,
beauty_product)}
num_aovs = rop.evalParm("RS_aov")
for index in range(num_aovs):
@ -104,11 +68,29 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin):
aov_product = self.get_render_product_name(aov_prefix, aov_suffix)
render_products.append(aov_product)
files_by_aov[aov_suffix] = self.generate_expected_files(instance,
aov_product) # noqa
for product in render_products:
self.log.debug("Found render product: %s" % product)
filenames = list(render_products)
instance.data["files"] = filenames
instance.data["renderProducts"] = colorspace.ARenderProduct()
# For now by default do NOT try to publish the rendered output
instance.data["publishJobState"] = "Suspended"
instance.data["attachTo"] = [] # stub required data
if "expectedFiles" not in instance.data:
instance.data["expectedFiles"] = list()
instance.data["expectedFiles"].append(files_by_aov)
# update the colorspace data
colorspace_data = get_color_management_preferences()
instance.data["colorspaceConfig"] = colorspace_data["config"]
instance.data["colorspaceDisplay"] = colorspace_data["display"]
instance.data["colorspaceView"] = colorspace_data["view"]
def get_render_product_name(self, prefix, suffix):
"""Return the output filename using the AOV prefix and suffix"""
@ -133,3 +115,27 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin):
product_name = prefix
return product_name
def generate_expected_files(self, instance, path):
"""Create expected files in instance data"""
dir = os.path.dirname(path)
file = os.path.basename(path)
if "#" in file:
def replace(match):
return "%0{}d".format(len(match.group()))
file = re.sub("#+", replace, file)
if "%" not in file:
return path
expected_files = []
start = instance.data["frameStart"]
end = instance.data["frameEnd"]
for i in range(int(start), (int(end) + 1)):
expected_files.append(
os.path.join(dir, (file % i)).replace("\\", "/"))
return expected_files

View file

@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
"""Collector plugin for frames data on ROP instances."""
import hou # noqa
import pyblish.api
from openpype.hosts.houdini.api import lib
class CollectRopFrameRange(pyblish.api.InstancePlugin):
"""Collect all frames which would be saved from the ROP nodes"""
order = pyblish.api.CollectorOrder
label = "Collect RopNode Frame Range"
def process(self, instance):
node_path = instance.data.get("instance_node")
if node_path is None:
# Instance without instance node like a workfile instance
return
ropnode = hou.node(node_path)
frame_data = lib.get_frame_data(ropnode)
if "frameStart" in frame_data and "frameEnd" in frame_data:
# Log artist friendly message about the collected frame range
message = (
"Frame range {0[frameStart]} - {0[frameEnd]}"
).format(frame_data)
if frame_data.get("step", 1.0) != 1.0:
message += " with step {0[step]}".format(frame_data)
self.log.info(message)
instance.data.update(frame_data)
# Add frame range to label if the instance has a frame range.
label = instance.data.get("label", instance.data["name"])
instance.data["label"] = (
"{0} [{1[frameStart]} - {1[frameEnd]}]".format(label,
frame_data)
)

View file

@ -0,0 +1,129 @@
import re
import os
import hou
import pyblish.api
from openpype.hosts.houdini.api.lib import (
evalParmNoFrame,
get_color_management_preferences
)
from openpype.hosts.houdini.api import (
colorspace
)
class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin):
"""Collect Vray Render Products
Collects the instance.data["files"] for the render products.
Provides:
instance -> files
"""
label = "VRay ROP Render Products"
order = pyblish.api.CollectorOrder + 0.4
hosts = ["houdini"]
families = ["vray_rop"]
def process(self, instance):
rop = hou.node(instance.data.get("instance_node"))
# Collect chunkSize
chunk_size_parm = rop.parm("chunkSize")
if chunk_size_parm:
chunk_size = int(chunk_size_parm.eval())
instance.data["chunkSize"] = chunk_size
self.log.debug("Chunk Size: %s" % chunk_size)
default_prefix = evalParmNoFrame(rop, "SettingsOutput_img_file_path")
render_products = []
# TODO: add render elements if render element
beauty_product = self.get_beauty_render_product(default_prefix)
render_products.append(beauty_product)
files_by_aov = {
"RGB Color": self.generate_expected_files(instance,
beauty_product)}
if instance.data.get("RenderElement", True):
render_element = self.get_render_element_name(rop, default_prefix)
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
for product in render_products:
self.log.debug("Found render product: %s" % product)
filenames = list(render_products)
instance.data["files"] = filenames
instance.data["renderProducts"] = colorspace.ARenderProduct()
# For now by default do NOT try to publish the rendered output
instance.data["publishJobState"] = "Suspended"
instance.data["attachTo"] = [] # stub required data
if "expectedFiles" not in instance.data:
instance.data["expectedFiles"] = list()
instance.data["expectedFiles"].append(files_by_aov)
self.log.debug("expectedFiles:{}".format(files_by_aov))
# update the colorspace data
colorspace_data = get_color_management_preferences()
instance.data["colorspaceConfig"] = colorspace_data["config"]
instance.data["colorspaceDisplay"] = colorspace_data["display"]
instance.data["colorspaceView"] = colorspace_data["view"]
def get_beauty_render_product(self, prefix, suffix="<reName>"):
"""Return the beauty output filename if render element enabled
"""
aov_parm = ".{}".format(suffix)
beauty_product = None
if aov_parm in prefix:
beauty_product = prefix.replace(aov_parm, "")
else:
beauty_product = prefix
return beauty_product
def get_render_element_name(self, node, prefix, suffix="<reName>"):
"""Return the output filename using the AOV prefix and suffix
"""
render_element_dict = {}
# need a rewrite
re_path = node.evalParm("render_network_render_channels")
if re_path:
node_children = hou.node(re_path).children()
for element in node_children:
if element.shaderName() != "vray:SettingsRenderChannels":
aov = str(element)
render_product = prefix.replace(suffix, aov)
render_element_dict[aov] = render_product
return render_element_dict
def generate_expected_files(self, instance, path):
"""Create expected files in instance data"""
dir = os.path.dirname(path)
file = os.path.basename(path)
if "#" in file:
def replace(match):
return "%0{}d".format(len(match.group()))
file = re.sub("#+", replace, file)
if "%" not in file:
return path
expected_files = []
start = instance.data["frameStart"]
end = instance.data["frameEnd"]
for i in range(int(start), (int(end) + 1)):
expected_files.append(
os.path.join(dir, (file % i)).replace("\\", "/"))
return expected_files

View file

@ -2,7 +2,10 @@ import pyblish.api
from openpype.lib import version_up
from openpype.pipeline import registered_host
from openpype.action import get_errored_plugins_from_data
from openpype.hosts.houdini.api import HoudiniHost
from openpype.pipeline.publish import KnownPublishError
class IncrementCurrentFile(pyblish.api.ContextPlugin):
"""Increment the current file.
@ -14,17 +17,32 @@ class IncrementCurrentFile(pyblish.api.ContextPlugin):
label = "Increment current file"
order = pyblish.api.IntegratorOrder + 9.0
hosts = ["houdini"]
families = ["workfile"]
families = ["workfile",
"redshift_rop",
"arnold_rop",
"mantra_rop",
"karma_rop",
"usdrender"]
optional = True
def process(self, context):
errored_plugins = get_errored_plugins_from_data(context)
if any(
plugin.__name__ == "HoudiniSubmitPublishDeadline"
for plugin in errored_plugins
):
raise KnownPublishError(
"Skipping incrementing current file because "
"submission to deadline failed."
)
# Filename must not have changed since collecting
host = registered_host() # type: HoudiniHost
current_file = host.current_file()
assert (
context.data["currentFile"] == current_file
), "Collected filename from current scene name."
), "Collected filename mismatches from current scene name."
new_filepath = version_up(current_file)
host.save_workfile(new_filepath)

View file

@ -6,7 +6,7 @@ from operator import attrgetter
import json
from openpype.host import HostBase, IWorkfileHost, ILoadHost, INewPublisher
from openpype.host import HostBase, IWorkfileHost, ILoadHost, IPublishHost
import pyblish.api
from openpype.pipeline import (
register_creator_plugin_path,
@ -28,7 +28,7 @@ CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
class MaxHost(HostBase, IWorkfileHost, ILoadHost, INewPublisher):
class MaxHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
name = "max"
menu = None

View file

@ -1,8 +1,5 @@
import os
from openpype.pipeline import (
load, get_representation_path
)
from openpype.pipeline import load, get_representation_path
from openpype.hosts.max.api.pipeline import containerise
from openpype.hosts.max.api import lib
from openpype.hosts.max.api.lib import maintained_selection
@ -24,24 +21,20 @@ class ModelAbcLoader(load.LoaderPlugin):
file_path = os.path.normpath(self.fname)
abc_before = {
c for c in rt.rootNode.Children
c
for c in rt.rootNode.Children
if rt.classOf(c) == rt.AlembicContainer
}
abc_import_cmd = (f"""
AlembicImport.ImportToRoot = false
AlembicImport.CustomAttributes = true
AlembicImport.UVs = true
AlembicImport.VertexColors = true
importFile @"{file_path}" #noPrompt
""")
self.log.debug(f"Executing command: {abc_import_cmd}")
rt.execute(abc_import_cmd)
rt.AlembicImport.ImportToRoot = False
rt.AlembicImport.CustomAttributes = True
rt.AlembicImport.UVs = True
rt.AlembicImport.VertexColors = True
rt.importFile(file_path, rt.name("noPrompt"))
abc_after = {
c for c in rt.rootNode.Children
c
for c in rt.rootNode.Children
if rt.classOf(c) == rt.AlembicContainer
}
@ -54,10 +47,12 @@ importFile @"{file_path}" #noPrompt
abc_container = abc_containers.pop()
return containerise(
name, [abc_container], context, loader=self.__class__.__name__)
name, [abc_container], context, loader=self.__class__.__name__
)
def update(self, container, representation):
from pymxs import runtime as rt
path = get_representation_path(representation)
node = rt.getNodeByName(container["instance_node"])
rt.select(node.Children)
@ -76,9 +71,10 @@ importFile @"{file_path}" #noPrompt
with maintained_selection():
rt.select(node)
lib.imprint(container["instance_node"], {
"representation": str(representation["_id"])
})
lib.imprint(
container["instance_node"],
{"representation": str(representation["_id"])},
)
def switch(self, container, representation):
self.update(container, representation)

View file

@ -1,8 +1,5 @@
import os
from openpype.pipeline import (
load,
get_representation_path
)
from openpype.pipeline import load, get_representation_path
from openpype.hosts.max.api.pipeline import containerise
from openpype.hosts.max.api import lib
from openpype.hosts.max.api.lib import maintained_selection
@ -24,10 +21,7 @@ class FbxModelLoader(load.LoaderPlugin):
rt.FBXImporterSetParam("Animation", False)
rt.FBXImporterSetParam("Cameras", False)
rt.FBXImporterSetParam("Preserveinstances", True)
rt.importFile(
filepath,
rt.name("noPrompt"),
using=rt.FBXIMP)
rt.importFile(filepath, rt.name("noPrompt"), using=rt.FBXIMP)
container = rt.getNodeByName(f"{name}")
if not container:
@ -38,7 +32,8 @@ class FbxModelLoader(load.LoaderPlugin):
selection.Parent = container
return containerise(
name, [container], context, loader=self.__class__.__name__)
name, [container], context, loader=self.__class__.__name__
)
def update(self, container, representation):
from pymxs import runtime as rt
@ -46,24 +41,21 @@ class FbxModelLoader(load.LoaderPlugin):
path = get_representation_path(representation)
node = rt.getNodeByName(container["instance_node"])
rt.select(node.Children)
fbx_reimport_cmd = (
f"""
FBXImporterSetParam "Animation" false
FBXImporterSetParam "Cameras" false
FBXImporterSetParam "AxisConversionMethod" true
FbxExporterSetParam "UpAxis" "Y"
FbxExporterSetParam "Preserveinstances" true
importFile @"{path}" #noPrompt using:FBXIMP
""")
rt.execute(fbx_reimport_cmd)
rt.FBXImporterSetParam("Animation", False)
rt.FBXImporterSetParam("Cameras", False)
rt.FBXImporterSetParam("AxisConversionMethod", True)
rt.FBXImporterSetParam("UpAxis", "Y")
rt.FBXImporterSetParam("Preserveinstances", True)
rt.importFile(path, rt.name("noPrompt"), using=rt.FBXIMP)
with maintained_selection():
rt.select(node)
lib.imprint(container["instance_node"], {
"representation": str(representation["_id"])
})
lib.imprint(
container["instance_node"],
{"representation": str(representation["_id"])},
)
def switch(self, container, representation):
self.update(container, representation)

View file

@ -5,9 +5,7 @@ Because of limited api, alembics can be only loaded, but not easily updated.
"""
import os
from openpype.pipeline import (
load, get_representation_path
)
from openpype.pipeline import load, get_representation_path
from openpype.hosts.max.api.pipeline import containerise
from openpype.hosts.max.api import lib
@ -15,9 +13,7 @@ from openpype.hosts.max.api import lib
class AbcLoader(load.LoaderPlugin):
"""Alembic loader."""
families = ["camera",
"animation",
"pointcache"]
families = ["camera", "animation", "pointcache"]
label = "Load Alembic"
representations = ["abc"]
order = -10
@ -30,21 +26,17 @@ class AbcLoader(load.LoaderPlugin):
file_path = os.path.normpath(self.fname)
abc_before = {
c for c in rt.rootNode.Children
c
for c in rt.rootNode.Children
if rt.classOf(c) == rt.AlembicContainer
}
abc_export_cmd = (f"""
AlembicImport.ImportToRoot = false
importFile @"{file_path}" #noPrompt
""")
self.log.debug(f"Executing command: {abc_export_cmd}")
rt.execute(abc_export_cmd)
rt.AlembicImport.ImportToRoot = False
rt.importFile(file_path, rt.name("noPrompt"))
abc_after = {
c for c in rt.rootNode.Children
c
for c in rt.rootNode.Children
if rt.classOf(c) == rt.AlembicContainer
}
@ -57,7 +49,8 @@ importFile @"{file_path}" #noPrompt
abc_container = abc_containers.pop()
return containerise(
name, [abc_container], context, loader=self.__class__.__name__)
name, [abc_container], context, loader=self.__class__.__name__
)
def update(self, container, representation):
from pymxs import runtime as rt
@ -69,9 +62,10 @@ importFile @"{file_path}" #noPrompt
for alembic_object in alembic_objects:
alembic_object.source = path
lib.imprint(container["instance_node"], {
"representation": str(representation["_id"])
})
lib.imprint(
container["instance_node"],
{"representation": str(representation["_id"])},
)
def switch(self, container, representation):
self.update(container, representation)

View file

@ -181,16 +181,34 @@ class CreateRender(plugin.Creator):
primary_pool = pool_setting["primary_pool"]
sorted_pools = self._set_default_pool(list(pools), primary_pool)
cmds.addAttr(self.instance, longName="primaryPool",
attributeType="enum",
enumName=":".join(sorted_pools))
cmds.addAttr(
self.instance,
longName="primaryPool",
attributeType="enum",
enumName=":".join(sorted_pools)
)
cmds.setAttr(
"{}.primaryPool".format(self.instance),
0,
keyable=False,
channelBox=True
)
pools = ["-"] + pools
secondary_pool = pool_setting["secondary_pool"]
sorted_pools = self._set_default_pool(list(pools), secondary_pool)
cmds.addAttr("{}.secondaryPool".format(self.instance),
attributeType="enum",
enumName=":".join(sorted_pools))
cmds.addAttr(
self.instance,
longName="secondaryPool",
attributeType="enum",
enumName=":".join(sorted_pools)
)
cmds.setAttr(
"{}.secondaryPool".format(self.instance),
0,
keyable=False,
channelBox=True
)
def _create_render_settings(self):
"""Create instance settings."""
@ -260,6 +278,12 @@ class CreateRender(plugin.Creator):
default_priority)
self.data["tile_priority"] = tile_priority
strict_error_checking = maya_submit_dl.get("strict_error_checking",
True)
self.data["strict_error_checking"] = strict_error_checking
# Pool attributes should be last since they will be recreated when
# the deadline server changes.
pool_setting = (self._project_settings["deadline"]
["publish"]
["CollectDeadlinePools"])
@ -272,9 +296,6 @@ class CreateRender(plugin.Creator):
secondary_pool = pool_setting["secondary_pool"]
self.data["secondaryPool"] = self._set_default_pool(pool_names,
secondary_pool)
strict_error_checking = maya_submit_dl.get("strict_error_checking",
True)
self.data["strict_error_checking"] = strict_error_checking
if muster_enabled:
self.log.info(">>> Loading Muster credentials ...")

View file

@ -33,7 +33,7 @@ def preserve_modelpanel_cameras(container, log=None):
panel_cameras = {}
for panel in cmds.getPanel(type="modelPanel"):
cam = cmds.ls(cmds.modelPanel(panel, query=True, camera=True),
long=True)
long=True)[0]
# Often but not always maya returns the transform from the
# modelPanel as opposed to the camera shape, so we convert it

View file

@ -35,13 +35,16 @@ class CollectArnoldSceneSource(pyblish.api.InstancePlugin):
# camera.
cameras = cmds.ls(type="camera", long=True)
renderable = [c for c in cameras if cmds.getAttr("%s.renderable" % c)]
camera = renderable[0]
for node in instance.data["contentMembers"]:
camera_shapes = cmds.listRelatives(
node, shapes=True, type="camera"
)
if camera_shapes:
camera = node
instance.data["camera"] = camera
if renderable:
camera = renderable[0]
for node in instance.data["contentMembers"]:
camera_shapes = cmds.listRelatives(
node, shapes=True, type="camera"
)
if camera_shapes:
camera = node
instance.data["camera"] = camera
else:
self.log.debug("No renderable cameras found.")
self.log.debug("data: {}".format(instance.data))

View file

@ -336,7 +336,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
context.data["system_settings"]["modules"]["deadline"]
)
if deadline_settings["enabled"]:
data["deadlineUrl"] = render_instance.data.get("deadlineUrl")
data["deadlineUrl"] = render_instance.data["deadlineUrl"]
if self.sync_workfile_version:
data["version"] = context.data["version"]

View file

@ -70,5 +70,5 @@ class ValidateArnoldSceneSourceCbid(pyblish.api.InstancePlugin):
@classmethod
def repair(cls, instance):
for content_node, proxy_node in cls.get_invalid_couples(cls, instance):
lib.set_id(proxy_node, lib.get_id(content_node), overwrite=False)
for content_node, proxy_node in cls.get_invalid_couples(instance):
lib.set_id(proxy_node, lib.get_id(content_node), overwrite=True)

View file

@ -364,6 +364,17 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
cmds.setAttr("defaultRenderGlobals.animation", True)
# Repair prefix
if renderer == "arnold":
multipart = cmds.getAttr("defaultArnoldDriver.mergeAOVs")
if multipart:
separator_variations = [
"_<RenderPass>",
"<RenderPass>_",
"<RenderPass>",
]
for variant in separator_variations:
default_prefix = default_prefix.replace(variant, "")
if renderer != "renderman":
node = render_attrs["node"]
prefix_attr = render_attrs["prefix"]

View file

@ -1403,8 +1403,6 @@ def create_write_node(
# adding write to read button
add_button_clear_rendered(GN, os.path.dirname(fpath))
GN.addKnob(nuke.Text_Knob('', ''))
# set tile color
tile_color = next(
iter(

View file

@ -564,6 +564,7 @@ def remove_instance(instance):
instance_node = instance.transient_data["node"]
instance_knob = instance_node.knobs()[INSTANCE_DATA_KNOB]
instance_node.removeKnob(instance_knob)
nuke.delete(instance_node)
def select_instance(instance):

View file

@ -75,20 +75,6 @@ class NukeCreator(NewCreator):
for pass_key in keys:
creator_attrs[pass_key] = pre_create_data[pass_key]
def add_info_knob(self, node):
if "OP_info" in node.knobs().keys():
return
# add info text
info_knob = nuke.Text_Knob("OP_info", "")
info_knob.setValue("""
<span style=\"color:#fc0303\">
<p>This node is maintained by <b>OpenPype Publisher</b>.</p>
<p>To remove it use Publisher gui.</p>
</span>
""")
node.addKnob(info_knob)
def check_existing_subset(self, subset_name):
"""Make sure subset name is unique.
@ -153,8 +139,6 @@ class NukeCreator(NewCreator):
created_node = nuke.createNode(node_type)
created_node["name"].setValue(node_name)
self.add_info_knob(created_node)
for key, values in node_knobs.items():
if key in created_node.knobs():
created_node["key"].setValue(values)

View file

@ -36,8 +36,6 @@ class CreateBackdrop(NukeCreator):
created_node["note_font_size"].setValue(24)
created_node["label"].setValue("[{}]".format(node_name))
self.add_info_knob(created_node)
return created_node
def create(self, subset_name, instance_data, pre_create_data):

View file

@ -39,8 +39,6 @@ class CreateCamera(NukeCreator):
created_node["name"].setValue(node_name)
self.add_info_knob(created_node)
return created_node
def create(self, subset_name, instance_data, pre_create_data):

View file

@ -40,8 +40,6 @@ class CreateGizmo(NukeCreator):
created_node["name"].setValue(node_name)
self.add_info_knob(created_node)
return created_node
def create(self, subset_name, instance_data, pre_create_data):

View file

@ -40,8 +40,6 @@ class CreateModel(NukeCreator):
created_node["name"].setValue(node_name)
self.add_info_knob(created_node)
return created_node
def create(self, subset_name, instance_data, pre_create_data):

View file

@ -32,7 +32,7 @@ class CreateSource(NukeCreator):
read_node["tile_color"].setValue(
int(self.node_color, 16))
read_node["name"].setValue(node_name)
self.add_info_knob(read_node)
return read_node
def create(self, subset_name, instance_data, pre_create_data):

View file

@ -86,7 +86,6 @@ class CreateWriteImage(napi.NukeWriteCreator):
"frame": nuke.frame()
}
)
self.add_info_knob(created_node)
self._add_frame_range_limit(created_node, instance_data)

View file

@ -74,7 +74,6 @@ class CreateWritePrerender(napi.NukeWriteCreator):
"height": height
}
)
self.add_info_knob(created_node)
self._add_frame_range_limit(created_node)

View file

@ -66,7 +66,6 @@ class CreateWriteRender(napi.NukeWriteCreator):
"height": height
}
)
self.add_info_knob(created_node)
self.integrate_links(created_node, outputs=False)

View file

View file

@ -0,0 +1,76 @@
import os
import nuke
from openpype.hosts.nuke.api.lib import set_node_knobs_from_settings
frame_padding = 5
temp_rendering_path_template = (
"{work}/renders/nuke/{subset}/{subset}.{frame}.{ext}")
knobs_setting = {
"knobs": [
{
"type": "text",
"name": "file_type",
"value": "exr"
},
{
"type": "text",
"name": "datatype",
"value": "16 bit half"
},
{
"type": "text",
"name": "compression",
"value": "Zip (1 scanline)"
},
{
"type": "bool",
"name": "autocrop",
"value": True
},
{
"type": "color_gui",
"name": "tile_color",
"value": [
186,
35,
35,
255
]
},
{
"type": "text",
"name": "channels",
"value": "rgb"
},
{
"type": "bool",
"name": "create_directories",
"value": True
}
]
}
def main():
write_selected_nodes = [
s for s in nuke.selectedNodes() if s.Class() == "Write"]
ext = None
knobs = knobs_setting["knobs"]
for knob in knobs:
if knob["name"] == "file_type":
ext = knob["value"]
for w in write_selected_nodes:
# data for mapping the path
data = {
"work": os.getenv("AVALON_WORKDIR"),
"subset": w["name"].value(),
"frame": "#" * frame_padding,
"ext": ext
}
file_path = temp_rendering_path_template.format(**data)
file_path = file_path.replace("\\", "/")
w["file"].setValue(file_path)
set_node_knobs_from_settings(w, knobs)

View file

@ -0,0 +1,47 @@
""" OpenPype custom script for resetting read nodes start frame values """
import nuke
import nukescripts
class FrameSettingsPanel(nukescripts.PythonPanel):
""" Frame Settings Panel """
def __init__(self):
nukescripts.PythonPanel.__init__(self, "Set Frame Start (Read Node)")
# create knobs
self.frame = nuke.Int_Knob(
'frame', 'Frame Number')
self.selected = nuke.Boolean_Knob("selection")
# add knobs to panel
self.addKnob(self.selected)
self.addKnob(self.frame)
# set values
self.selected.setValue(False)
self.frame.setValue(nuke.root().firstFrame())
def process(self):
""" Process the panel values. """
# get values
frame = self.frame.value()
if self.selected.value():
# selected nodes processing
if not nuke.selectedNodes():
return
for rn_ in nuke.selectedNodes():
if rn_.Class() != "Read":
continue
rn_["frame_mode"].setValue("start_at")
rn_["frame"].setValue(str(frame))
else:
# all nodes processing
for rn_ in nuke.allNodes(filter="Read"):
rn_["frame_mode"].setValue("start_at")
rn_["frame"].setValue(str(frame))
def main():
p_ = FrameSettingsPanel()
if p_.showModalDialog():
print(p_.process())

View file

@ -43,18 +43,22 @@ def open_file(filepath):
"""
Loading project
"""
from . import bmdvr
pm = get_project_manager()
page = bmdvr.GetCurrentPage()
if page is not None:
# Save current project only if Resolve has an active page, otherwise
# we consider Resolve being in a pre-launch state (no open UI yet)
project = pm.GetCurrentProject()
print(f"Saving current project: {project}")
pm.SaveProject()
file = os.path.basename(filepath)
fname, _ = os.path.splitext(file)
dname, _ = fname.split("_v")
# deal with current project
project = pm.GetCurrentProject()
log.info(f"Test `pm`: {pm}")
pm.SaveProject()
try:
log.info(f"Test `dname`: {dname}")
if not set_project_manager_to_folder_name(dname):
raise
# load project from input path
@ -72,6 +76,7 @@ def open_file(filepath):
return False
return True
def current_file():
pm = get_project_manager()
current_dir = os.getenv("AVALON_WORKDIR")

View file

@ -0,0 +1,45 @@
import os
from openpype.lib import PreLaunchHook
import openpype.hosts.resolve
class ResolveLaunchLastWorkfile(PreLaunchHook):
"""Special hook to open last workfile for Resolve.
Checks 'start_last_workfile', if set to False, it will not open last
workfile. This property is set explicitly in Launcher.
"""
# Execute after workfile template copy
order = 10
app_groups = ["resolve"]
def execute(self):
if not self.data.get("start_last_workfile"):
self.log.info("It is set to not start last workfile on start.")
return
last_workfile = self.data.get("last_workfile_path")
if not last_workfile:
self.log.warning("Last workfile was not collected.")
return
if not os.path.exists(last_workfile):
self.log.info("Current context does not have any workfile yet.")
return
# Add path to launch environment for the startup script to pick up
self.log.info(f"Setting OPENPYPE_RESOLVE_OPEN_ON_LAUNCH to launch "
f"last workfile: {last_workfile}")
key = "OPENPYPE_RESOLVE_OPEN_ON_LAUNCH"
self.launch_context.env[key] = last_workfile
# Set the openpype prelaunch startup script path for easy access
# in the LUA .scriptlib code
op_resolve_root = os.path.dirname(openpype.hosts.resolve.__file__)
script_path = os.path.join(op_resolve_root, "startup.py")
key = "OPENPYPE_RESOLVE_STARTUP_SCRIPT"
self.launch_context.env[key] = script_path
self.log.info("Setting OPENPYPE_RESOLVE_STARTUP_SCRIPT to: "
f"{script_path}")

View file

@ -0,0 +1,62 @@
"""This script is used as a startup script in Resolve through a .scriptlib file
It triggers directly after the launch of Resolve and it's recommended to keep
it optimized for fast performance since the Resolve UI is actually interactive
while this is running. As such, there's nothing ensuring the user isn't
continuing manually before any of the logic here runs. As such we also try
to delay any imports as much as possible.
This code runs in a separate process to the main Resolve process.
"""
import os
import openpype.hosts.resolve.api
def ensure_installed_host():
"""Install resolve host with openpype and return the registered host.
This function can be called multiple times without triggering an
additional install.
"""
from openpype.pipeline import install_host, registered_host
host = registered_host()
if host:
return host
install_host(openpype.hosts.resolve.api)
return registered_host()
def launch_menu():
print("Launching Resolve OpenPype menu..")
ensure_installed_host()
openpype.hosts.resolve.api.launch_pype_menu()
def open_file(path):
# Avoid the need to "install" the host
host = ensure_installed_host()
host.open_file(path)
def main():
# Open last workfile
workfile_path = os.environ.get("OPENPYPE_RESOLVE_OPEN_ON_LAUNCH")
if workfile_path:
open_file(workfile_path)
else:
print("No last workfile set to open. Skipping..")
# Launch OpenPype menu
from openpype.settings import get_project_settings
from openpype.pipeline.context_tools import get_current_project_name
project_name = get_current_project_name()
settings = get_project_settings(project_name)
if settings.get("resolve", {}).get("launch_openpype_menu_on_start", True):
launch_menu()
if __name__ == "__main__":
main()

View file

@ -0,0 +1,21 @@
-- Run OpenPype's Python launch script for resolve
function file_exists(name)
local f = io.open(name, "r")
return f ~= nil and io.close(f)
end
openpype_startup_script = os.getenv("OPENPYPE_RESOLVE_STARTUP_SCRIPT")
if openpype_startup_script ~= nil then
script = fusion:MapPath(openpype_startup_script)
if file_exists(script) then
-- We must use RunScript to ensure it runs in a separate
-- process to Resolve itself to avoid a deadlock for
-- certain imports of OpenPype libraries or Qt
print("Running launch script: " .. script)
fusion:RunScript(script)
else
print("Launch script not found at: " .. script)
end
end

View file

@ -53,6 +53,14 @@ def setup(env):
src = os.path.join(directory, script)
dst = os.path.join(util_scripts_dir, script)
# TODO: Make this a less hacky workaround
if script == "openpype_startup.scriptlib":
# Handle special case for scriptlib that needs to be a folder
# up from the Comp folder in the Fusion scripts
dst = os.path.join(os.path.dirname(util_scripts_dir),
script)
log.info("Copying `{}` to `{}`...".format(src, dst))
if os.path.isdir(src):
shutil.copytree(

View file

@ -487,7 +487,22 @@ or updating already created. Publishing will create OTIO file.
)
# get video stream data
video_stream = media_data["streams"][0]
video_streams = []
audio_streams = []
for stream in media_data["streams"]:
codec_type = stream.get("codec_type")
if codec_type == "audio":
audio_streams.append(stream)
elif codec_type == "video":
video_streams.append(stream)
if not video_streams:
raise ValueError(
"Could not find video stream in source file."
)
video_stream = video_streams[0]
return_data = {
"video": True,
"start_frame": 0,
@ -500,12 +515,7 @@ or updating already created. Publishing will create OTIO file.
}
# get audio streams data
audio_stream = [
stream for stream in media_data["streams"]
if stream["codec_type"] == "audio"
]
if audio_stream:
if audio_streams:
return_data["audio"] = True
except Exception as exc:

View file

@ -113,26 +113,29 @@ def pack_project(
project_name
))
roots = project_doc["config"]["roots"]
# Determine root directory of project
source_root = None
source_root_name = None
for root_name, root_value in roots.items():
if source_root is not None:
raise ValueError(
"Packaging is supported only for single root projects"
)
source_root = root_value
source_root_name = root_name
root_path = None
source_root = {}
project_source_path = None
if not only_documents:
roots = project_doc["config"]["roots"]
# Determine root directory of project
source_root_name = None
for root_name, root_value in roots.items():
if source_root is not None:
raise ValueError(
"Packaging is supported only for single root projects"
)
source_root = root_value
source_root_name = root_name
root_path = source_root[platform.system().lower()]
print("Using root \"{}\" with path \"{}\"".format(
source_root_name, root_path
))
root_path = source_root[platform.system().lower()]
print("Using root \"{}\" with path \"{}\"".format(
source_root_name, root_path
))
project_source_path = os.path.join(root_path, project_name)
if not os.path.exists(project_source_path):
raise ValueError("Didn't find source of project files")
project_source_path = os.path.join(root_path, project_name)
if not os.path.exists(project_source_path):
raise ValueError("Didn't find source of project files")
# Determine zip filepath where data will be stored
if not destination_dir:
@ -273,8 +276,7 @@ def unpack_project(
low_platform = platform.system().lower()
project_name = metadata["project_name"]
source_root = metadata["root"]
root_path = source_root[low_platform]
root_path = metadata["root"].get(low_platform)
# Drop existing collection
replace_project_documents(project_name, docs, database_name)

View file

@ -662,7 +662,7 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin):
# test if there is instance of workfile waiting
# to be published.
assert i.data["publish"] is True, (
assert i.data.get("publish", True) is True, (
"Workfile (scene) must be published along")
return i

View file

@ -5,23 +5,26 @@ This is resolving index of server lists stored in `deadlineServers` instance
attribute or using default server if that attribute doesn't exists.
"""
from maya import cmds
import pyblish.api
class CollectDeadlineServerFromInstance(pyblish.api.InstancePlugin):
"""Collect Deadline Webservice URL from instance."""
order = pyblish.api.CollectorOrder + 0.415
# Run before collect_render.
order = pyblish.api.CollectorOrder + 0.005
label = "Deadline Webservice from the Instance"
families = ["rendering", "renderlayer"]
hosts = ["maya"]
def process(self, instance):
instance.data["deadlineUrl"] = self._collect_deadline_url(instance)
self.log.info(
"Using {} for submission.".format(instance.data["deadlineUrl"]))
@staticmethod
def _collect_deadline_url(render_instance):
def _collect_deadline_url(self, render_instance):
# type: (pyblish.api.Instance) -> str
"""Get Deadline Webservice URL from render instance.
@ -49,8 +52,16 @@ class CollectDeadlineServerFromInstance(pyblish.api.InstancePlugin):
default_server = render_instance.context.data["defaultDeadline"]
instance_server = render_instance.data.get("deadlineServers")
if not instance_server:
self.log.debug("Using default server.")
return default_server
# Get instance server as sting.
if isinstance(instance_server, int):
instance_server = cmds.getAttr(
"{}.deadlineServers".format(render_instance.data["objset"]),
asString=True
)
default_servers = deadline_settings["deadline_urls"]
project_servers = (
render_instance.context.data
@ -58,15 +69,23 @@ class CollectDeadlineServerFromInstance(pyblish.api.InstancePlugin):
["deadline"]
["deadline_servers"]
)
deadline_servers = {
if not project_servers:
self.log.debug("Not project servers found. Using default servers.")
return default_servers[instance_server]
project_enabled_servers = {
k: default_servers[k]
for k in project_servers
if k in default_servers
}
# This is Maya specific and may not reflect real selection of deadline
# url as dictionary keys in Python 2 are not ordered
return deadline_servers[
list(deadline_servers.keys())[
int(render_instance.data.get("deadlineServers"))
]
]
msg = (
"\"{}\" server on instance is not enabled in project settings."
" Enabled project servers:\n{}".format(
instance_server, project_enabled_servers
)
)
assert instance_server in project_enabled_servers, msg
self.log.debug("Using project approved server.")
return project_enabled_servers[instance_server]

View file

@ -17,7 +17,8 @@ class CollectDefaultDeadlineServer(pyblish.api.ContextPlugin):
`CollectDeadlineServerFromInstance`.
"""
order = pyblish.api.CollectorOrder + 0.410
# Run before collect_deadline_server_instance.
order = pyblish.api.CollectorOrder + 0.0025
label = "Default Deadline Webservice"
pass_mongo_url = False

View file

@ -1,19 +1,27 @@
import hou
import os
import json
import attr
import getpass
from datetime import datetime
import requests
import pyblish.api
# import hou ???
from openpype.pipeline import legacy_io
from openpype.tests.lib import is_in_tests
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
class HoudiniSubmitRenderDeadline(pyblish.api.InstancePlugin):
@attr.s
class DeadlinePluginInfo():
SceneFile = attr.ib(default=None)
OutputDriver = attr.ib(default=None)
Version = attr.ib(default=None)
IgnoreInputs = attr.ib(default=True)
class HoudiniSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline):
"""Submit Solaris USD Render ROPs to Deadline.
Renders are submitted to a Deadline Web Service as
@ -30,83 +38,57 @@ class HoudiniSubmitRenderDeadline(pyblish.api.InstancePlugin):
order = pyblish.api.IntegratorOrder
hosts = ["houdini"]
families = ["usdrender",
"redshift_rop"]
"redshift_rop",
"arnold_rop",
"mantra_rop",
"karma_rop",
"vray_rop"]
targets = ["local"]
use_published = True
def process(self, instance):
def get_job_info(self):
job_info = DeadlineJobInfo(Plugin="Houdini")
instance = self._instance
context = instance.context
code = context.data["code"]
filepath = context.data["currentFile"]
filename = os.path.basename(filepath)
comment = context.data.get("comment", "")
deadline_user = context.data.get("deadlineUser", getpass.getuser())
jobname = "%s - %s" % (filename, instance.name)
# Support code prefix label for batch name
batch_name = filename
if code:
batch_name = "{0} - {1}".format(code, batch_name)
job_info.Name = "{} - {}".format(filename, instance.name)
job_info.BatchName = filename
job_info.Plugin = "Houdini"
job_info.UserName = context.data.get(
"deadlineUser", getpass.getuser())
if is_in_tests():
batch_name += datetime.now().strftime("%d%m%Y%H%M%S")
job_info.BatchName += datetime.now().strftime("%d%m%Y%H%M%S")
# Output driver to render
driver = instance[0]
# StartFrame to EndFrame by byFrameStep
# Deadline requires integers in frame range
frames = "{start}-{end}x{step}".format(
start=int(instance.data["frameStart"]),
end=int(instance.data["frameEnd"]),
step=int(instance.data["byFrameStep"]),
)
job_info.Frames = frames
# Documentation for keys available at:
# https://docs.thinkboxsoftware.com
# /products/deadline/8.0/1_User%20Manual/manual
# /manual-submission.html#job-info-file-options
payload = {
"JobInfo": {
# Top-level group name
"BatchName": batch_name,
job_info.Pool = instance.data.get("primaryPool")
job_info.SecondaryPool = instance.data.get("secondaryPool")
job_info.ChunkSize = instance.data.get("chunkSize", 10)
job_info.Comment = context.data.get("comment")
# Job name, as seen in Monitor
"Name": jobname,
# Arbitrary username, for visualisation in Monitor
"UserName": deadline_user,
"Plugin": "Houdini",
"Pool": instance.data.get("primaryPool"),
"secondaryPool": instance.data.get("secondaryPool"),
"Frames": frames,
"ChunkSize": instance.data.get("chunkSize", 10),
"Comment": comment
},
"PluginInfo": {
# Input
"SceneFile": filepath,
"OutputDriver": driver.path(),
# Mandatory for Deadline
# Houdini version without patch number
"Version": hou.applicationVersionString().rsplit(".", 1)[0],
"IgnoreInputs": True
},
# Mandatory for Deadline, may be empty
"AuxFiles": []
}
# Include critical environment variables with submission + api.Session
keys = [
# Submit along the current Avalon tool setup that we launched
# this application with so the Render Slave can build its own
# similar environment using it, e.g. "maya2018;vray4.x;yeti3.1.9"
"AVALON_TOOLS"
"FTRACK_API_KEY",
"FTRACK_API_USER",
"FTRACK_SERVER",
"OPENPYPE_SG_USER",
"AVALON_PROJECT",
"AVALON_ASSET",
"AVALON_TASK",
"AVALON_APP_NAME",
"OPENPYPE_DEV",
"OPENPYPE_LOG_NO_COLORS",
"OPENPYPE_VERSION"
]
# Add OpenPype version if we are running from build.
@ -114,61 +96,50 @@ class HoudiniSubmitRenderDeadline(pyblish.api.InstancePlugin):
keys.append("OPENPYPE_VERSION")
# Add mongo url if it's enabled
if context.data.get("deadlinePassMongoUrl"):
if self._instance.context.data.get("deadlinePassMongoUrl"):
keys.append("OPENPYPE_MONGO")
environment = dict({key: os.environ[key] for key in keys
if key in os.environ}, **legacy_io.Session)
for key in keys:
value = environment.get(key)
if value:
job_info.EnvironmentKeyValue[key] = value
payload["JobInfo"].update({
"EnvironmentKeyValue%d" % index: "{key}={value}".format(
key=key,
value=environment[key]
) for index, key in enumerate(environment)
})
# to recognize job from PYPE for turning Event On/Off
job_info.EnvironmentKeyValue["OPENPYPE_RENDER_JOB"] = "1"
# Include OutputFilename entries
# The first entry also enables double-click to preview rendered
# frames from Deadline Monitor
output_data = {}
for i, filepath in enumerate(instance.data["files"]):
dirname = os.path.dirname(filepath)
fname = os.path.basename(filepath)
output_data["OutputDirectory%d" % i] = dirname.replace("\\", "/")
output_data["OutputFilename%d" % i] = fname
job_info.OutputDirectory += dirname.replace("\\", "/")
job_info.OutputFilename += fname
# For now ensure destination folder exists otherwise HUSK
# will fail to render the output image. This is supposedly fixed
# in new production builds of Houdini
# TODO Remove this workaround with Houdini 18.0.391+
if not os.path.exists(dirname):
self.log.info("Ensuring output directory exists: %s" %
dirname)
os.makedirs(dirname)
return job_info
payload["JobInfo"].update(output_data)
def get_plugin_info(self):
self.submit(instance, payload)
instance = self._instance
context = instance.context
def submit(self, instance, payload):
# Output driver to render
driver = hou.node(instance.data["instance_node"])
hou_major_minor = hou.applicationVersionString().rsplit(".", 1)[0]
AVALON_DEADLINE = legacy_io.Session.get("AVALON_DEADLINE",
"http://localhost:8082")
assert AVALON_DEADLINE, "Requires AVALON_DEADLINE"
plugin_info = DeadlinePluginInfo(
SceneFile=context.data["currentFile"],
OutputDriver=driver.path(),
Version=hou_major_minor,
IgnoreInputs=True
)
plugin = payload["JobInfo"]["Plugin"]
self.log.info("Using Render Plugin : {}".format(plugin))
return attr.asdict(plugin_info)
self.log.info("Submitting..")
self.log.debug(json.dumps(payload, indent=4, sort_keys=True))
# E.g. http://192.168.0.1:8082/api/jobs
url = "{}/api/jobs".format(AVALON_DEADLINE)
response = requests.post(url, json=payload)
if not response.ok:
raise Exception(response.text)
def process(self, instance):
super(HoudiniSubmitDeadline, self).process(instance)
# TODO: Avoid the need for this logic here, needed for submit publish
# Store output dir for unified publisher (filesequence)
output_dir = os.path.dirname(instance.data["files"][0])
instance.data["outputDir"] = output_dir
instance.data["deadlineSubmissionJob"] = response.json()
instance.data["toBeRenderedOn"] = "deadline"

View file

@ -118,11 +118,15 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
deadline_plugin = "OpenPype"
targets = ["local"]
hosts = ["fusion", "max", "maya", "nuke",
hosts = ["fusion", "max", "maya", "nuke", "houdini",
"celaction", "aftereffects", "harmony"]
families = ["render.farm", "prerender.farm",
"renderlayer", "imagesequence", "maxrender", "vrayscene"]
"renderlayer", "imagesequence",
"vrayscene", "maxrender",
"arnold_rop", "mantra_rop",
"karma_rop", "vray_rop",
"redshift_rop"]
aov_filter = {"maya": [r".*([Bb]eauty).*"],
"aftereffects": [r".*"], # for everything from AE
@ -140,7 +144,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
"FTRACK_SERVER",
"AVALON_APP_NAME",
"OPENPYPE_USERNAME",
"OPENPYPE_SG_USER",
"OPENPYPE_VERSION",
"OPENPYPE_SG_USER"
]
# Add OpenPype version if we are running from build.
@ -820,7 +825,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
).format(source))
family = "render"
if "prerender" in instance.data["families"]:
if ("prerender" in instance.data["families"] or
"prerender.farm" in instance.data["families"]):
family = "prerender"
families = [family]
@ -1089,6 +1095,10 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
deadline_publish_job_id = \
self._submit_deadline_post_job(instance, render_job, instances)
# Inject deadline url to instances.
for inst in instances:
inst["deadlineUrl"] = self.deadline_url
# publish job file
publish_job = {
"asset": asset,

View file

@ -35,41 +35,47 @@ class CollectFramesFixDef(
rewrite_version = attribute_values.get("rewrite_version")
if frames_to_fix:
instance.data["frames_to_fix"] = frames_to_fix
if not frames_to_fix:
return
subset_name = instance.data["subset"]
asset_name = instance.data["asset"]
instance.data["frames_to_fix"] = frames_to_fix
project_entity = instance.data["projectEntity"]
project_name = project_entity["name"]
subset_name = instance.data["subset"]
asset_name = instance.data["asset"]
version = get_last_version_by_subset_name(project_name,
subset_name,
asset_name=asset_name)
if not version:
self.log.warning("No last version found, "
"re-render not possible")
return
project_entity = instance.data["projectEntity"]
project_name = project_entity["name"]
representations = get_representations(project_name,
version_ids=[version["_id"]])
published_files = []
for repre in representations:
if repre["context"]["family"] not in self.families:
continue
version = get_last_version_by_subset_name(
project_name,
subset_name,
asset_name=asset_name
)
if not version:
self.log.warning(
"No last version found, re-render not possible"
)
return
for file_info in repre.get("files"):
published_files.append(file_info["path"])
representations = get_representations(
project_name, version_ids=[version["_id"]]
)
published_files = []
for repre in representations:
if repre["context"]["family"] not in self.families:
continue
instance.data["last_version_published_files"] = published_files
self.log.debug("last_version_published_files::{}".format(
instance.data["last_version_published_files"]))
for file_info in repre.get("files"):
published_files.append(file_info["path"])
if rewrite_version:
instance.data["version"] = version["name"]
# limits triggering version validator
instance.data.pop("latestVersion")
instance.data["last_version_published_files"] = published_files
self.log.debug("last_version_published_files::{}".format(
instance.data["last_version_published_files"]))
if self.rewrite_version_enable and rewrite_version:
instance.data["version"] = version["name"]
# limits triggering version validator
instance.data.pop("latestVersion")
@classmethod
def get_attribute_defs(cls):

View file

@ -218,6 +218,20 @@
"title": "OpenPype Docs",
"command": "import webbrowser;webbrowser.open(url='https://openpype.io/docs/artist_hosts_nuke_tut')",
"tooltip": "Open the OpenPype Nuke user doc page"
},
{
"type": "action",
"sourcetype": "python",
"title": "Set Frame Start (Read Node)",
"command": "from openpype.hosts.nuke.startup.frame_setting_for_read_nodes import main;main();",
"tooltip": "Set frame start for read node(s)"
},
{
"type": "action",
"sourcetype": "python",
"title": "Set non publish output for Write Node",
"command": "from openpype.hosts.nuke.startup.custom_write_node import main;main();",
"tooltip": "Open the OpenPype Nuke user doc page"
}
]
},

View file

@ -1,4 +1,5 @@
{
"launch_openpype_menu_on_start": false,
"imageio": {
"activate_host_color_management": true,
"remapping": {

View file

@ -5,6 +5,11 @@
"label": "DaVinci Resolve",
"is_file": true,
"children": [
{
"type": "boolean",
"key": "launch_openpype_menu_on_start",
"label": "Launch OpenPype menu on start of Resolve"
},
{
"key": "imageio",
"type": "dict",

View file

@ -273,7 +273,7 @@ class ActionModel(QtGui.QStandardItemModel):
# Sort by order and name
return sorted(
compatible,
key=lambda action: (action.order, action.name)
key=lambda action: (action.order, lib.get_action_label(action))
)
def update_force_not_open_workfile_settings(self, is_checked, action_id):

View file

@ -446,6 +446,7 @@ class SubsetsModel(BaseRepresentationModel, TreeModel):
last_versions_by_subset_id = get_last_versions(
project_name,
subset_ids,
active=True,
fields=["_id", "parent", "name", "type", "data", "schema"]
)

View file

@ -66,8 +66,7 @@ class PublisherWindow(QtWidgets.QDialog):
on_top_flag = QtCore.Qt.Dialog
self.setWindowFlags(
self.windowFlags()
| QtCore.Qt.WindowTitleHint
QtCore.Qt.WindowTitleHint
| QtCore.Qt.WindowMaximizeButtonHint
| QtCore.Qt.WindowMinimizeButtonHint
| QtCore.Qt.WindowCloseButtonHint

View file

@ -633,10 +633,10 @@ class TrayManager:
# Create a copy of sys.argv
additional_args = list(sys.argv)
# Check last argument from `get_openpype_execute_args`
# - when running from code it is the same as first from sys.argv
if args[-1] == additional_args[0]:
additional_args.pop(0)
# Remove first argument from 'sys.argv'
# - when running from code the first argument is 'start.py'
# - when running from build the first argument is executable
additional_args.pop(0)
cleanup_additional_args = False
if use_expected_version:
@ -663,7 +663,6 @@ class TrayManager:
additional_args = _additional_args
args.extend(additional_args)
run_detached_process(args, env=envs)
self.exit()

View file

@ -123,10 +123,14 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
project_name = self.dbcon.active_project()
# Add all available versions to the editor
parent_id = item["version_document"]["parent"]
version_docs = list(sorted(
get_versions(project_name, subset_ids=[parent_id]),
key=lambda item: item["name"]
))
version_docs = [
version_doc
for version_doc in sorted(
get_versions(project_name, subset_ids=[parent_id]),
key=lambda item: item["name"]
)
if version_doc["data"].get("active", True)
]
hero_versions = list(
get_hero_versions(

View file

@ -1,6 +1,7 @@
import os
import datetime
import copy
import platform
from qtpy import QtCore, QtWidgets, QtGui
from openpype.client import (
@ -94,6 +95,19 @@ class SidePanelWidget(QtWidgets.QWidget):
self._on_note_change()
self.save_clicked.emit()
def get_user_name(self, file):
"""Get user name from file path"""
# Only run on Unix because pwd module is not available on Windows.
# NOTE: we tried adding "win32security" module but it was not working
# on all hosts so we decided to just support Linux until migration
# to Ayon
if platform.system().lower() == "windows":
return None
import pwd
filestat = os.stat(file)
return pwd.getpwuid(filestat.st_uid).pw_name
def set_context(self, asset_id, task_name, filepath, workfile_doc):
# Check if asset, task and file are selected
# NOTE workfile document is not requirement
@ -134,8 +148,14 @@ class SidePanelWidget(QtWidgets.QWidget):
"<b>Created:</b>",
creation_time.strftime(datetime_format),
"<b>Modified:</b>",
modification_time.strftime(datetime_format)
modification_time.strftime(datetime_format),
)
username = self.get_user_name(filepath)
if username:
lines += (
"<b>User:</b>",
username,
)
self._details_input.appendHtml("<br>".join(lines))
def get_workfile_data(self):

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring Pype version."""
__version__ = "3.15.9"
__version__ = "3.15.10-nightly.2"