Merge branch 'develop' into enhancement/AY-1675_Nuke---baking-inherits-channels-configuration

This commit is contained in:
Kayla Man 2024-05-16 21:54:45 +08:00 committed by GitHub
commit de59582b98
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
63 changed files with 2487 additions and 1145 deletions

View file

@ -1,7 +1,7 @@
from ayon_applications import PreLaunchHook
from ayon_core.pipeline.colorspace import get_imageio_config
from ayon_core.pipeline.template_data import get_template_data_with_names
from ayon_core.pipeline.colorspace import get_imageio_config_preset
from ayon_core.pipeline.template_data import get_template_data
class OCIOEnvHook(PreLaunchHook):
@ -26,32 +26,38 @@ class OCIOEnvHook(PreLaunchHook):
def execute(self):
"""Hook entry method."""
template_data = get_template_data_with_names(
project_name=self.data["project_name"],
folder_path=self.data["folder_path"],
task_name=self.data["task_name"],
folder_entity = self.data["folder_entity"]
template_data = get_template_data(
self.data["project_entity"],
folder_entity=folder_entity,
task_entity=self.data["task_entity"],
host_name=self.host_name,
settings=self.data["project_settings"]
settings=self.data["project_settings"],
)
config_data = get_imageio_config(
project_name=self.data["project_name"],
host_name=self.host_name,
project_settings=self.data["project_settings"],
anatomy_data=template_data,
config_data = get_imageio_config_preset(
self.data["project_name"],
self.data["folder_path"],
self.data["task_name"],
self.host_name,
anatomy=self.data["anatomy"],
project_settings=self.data["project_settings"],
template_data=template_data,
env=self.launch_context.env,
folder_id=folder_entity["id"],
)
if config_data:
ocio_path = config_data["path"]
if self.host_name in ["nuke", "hiero"]:
ocio_path = ocio_path.replace("\\", "/")
self.log.info(
f"Setting OCIO environment to config path: {ocio_path}")
self.launch_context.env["OCIO"] = ocio_path
else:
if not config_data:
self.log.debug("OCIO not set or enabled")
return
ocio_path = config_data["path"]
if self.host_name in ["nuke", "hiero"]:
ocio_path = ocio_path.replace("\\", "/")
self.log.info(
f"Setting OCIO environment to config path: {ocio_path}")
self.launch_context.env["OCIO"] = ocio_path

View file

@ -60,7 +60,7 @@ def main(*subprocess_args):
)
)
elif os.environ.get("AVALON_PHOTOSHOP_WORKFILES_ON_LAUNCH", True):
elif os.environ.get("AVALON_AFTEREFFECTS_WORKFILES_ON_LAUNCH", True):
save = False
if os.getenv("WORKFILES_SAVE_AS"):
save = True

View file

@ -43,7 +43,10 @@ class AbcCameraLoader(plugin.AssetLoader):
def _process(self, libpath, asset_group, group_name):
plugin.deselect_all()
bpy.ops.wm.alembic_import(filepath=libpath)
# Force the creation of the transform cache even if the camera
# doesn't have an animation. We use the cache to update the camera.
bpy.ops.wm.alembic_import(
filepath=libpath, always_add_cache_reader=True)
objects = lib.get_selection()
@ -178,12 +181,33 @@ class AbcCameraLoader(plugin.AssetLoader):
self.log.info("Library already loaded, not updating...")
return
mat = asset_group.matrix_basis.copy()
for obj in asset_group.children:
found = False
for constraint in obj.constraints:
if constraint.type == "TRANSFORM_CACHE":
constraint.cache_file.filepath = libpath.as_posix()
found = True
break
if not found:
# This is to keep compatibility with cameras loaded with
# the old loader
# Create a new constraint for the cache file
constraint = obj.constraints.new("TRANSFORM_CACHE")
bpy.ops.cachefile.open(filepath=libpath.as_posix())
constraint.cache_file = bpy.data.cache_files[-1]
constraint.cache_file.scale = 1.0
self._remove(asset_group)
self._process(str(libpath), asset_group, object_name)
# This is a workaround to set the object path. Blender doesn't
# load the list of object paths until the object is evaluated.
# This is a hack to force the object to be evaluated.
# The modifier doesn't need to be removed because camera
# objects don't have modifiers.
obj.modifiers.new(
name='MeshSequenceCache', type='MESH_SEQUENCE_CACHE')
bpy.context.evaluated_depsgraph_get()
asset_group.matrix_basis = mat
constraint.object_path = (
constraint.cache_file.object_paths[0].path)
metadata["libpath"] = str(libpath)
metadata["representation"] = repre_entity["id"]

View file

@ -1110,10 +1110,7 @@ def apply_colorspace_project():
'''
# backward compatibility layer
# TODO: remove this after some time
config_data = get_imageio_config(
project_name=get_current_project_name(),
host_name="hiero"
)
config_data = get_current_context_imageio_config_preset()
if config_data:
presets.update({

View file

@ -13,11 +13,17 @@ class CreateArnoldRop(plugin.HoudiniCreator):
# Default extension
ext = "exr"
# Default to split export and render jobs
export_job = True
# Default render target
render_target = "farm_split"
def create(self, product_name, instance_data, pre_create_data):
import hou
# Transfer settings from pre create to instance
creator_attributes = instance_data.setdefault(
"creator_attributes", dict())
for key in ["render_target", "review"]:
if key in pre_create_data:
creator_attributes[key] = pre_create_data[key]
# Remove the active, we are checking the bypass flag of the nodes
instance_data.pop("active", None)
@ -25,8 +31,6 @@ class CreateArnoldRop(plugin.HoudiniCreator):
# Add chunk size attribute
instance_data["chunkSize"] = 1
# Submit for job publishing
instance_data["farm"] = pre_create_data.get("farm")
instance = super(CreateArnoldRop, self).create(
product_name,
@ -51,7 +55,7 @@ class CreateArnoldRop(plugin.HoudiniCreator):
"ar_exr_half_precision": 1 # half precision
}
if pre_create_data.get("export_job"):
if pre_create_data.get("render_target") == "farm_split":
ass_filepath = \
"{export_dir}{product_name}/{product_name}.$F4.ass".format(
export_dir=hou.text.expandString("$HIP/pyblish/ass/"),
@ -66,23 +70,41 @@ class CreateArnoldRop(plugin.HoudiniCreator):
to_lock = ["productType", "id"]
self.lock_parameters(instance_node, to_lock)
def get_pre_create_attr_defs(self):
attrs = super(CreateArnoldRop, self).get_pre_create_attr_defs()
def get_instance_attr_defs(self):
"""get instance attribute definitions.
Attributes defined in this method are exposed in
publish tab in the publisher UI.
"""
render_target_items = {
"local": "Local machine rendering",
"local_no_render": "Use existing frames (local)",
"farm": "Farm Rendering",
"farm_split": "Farm Rendering - Split export & render jobs",
}
return [
BoolDef("review",
label="Review",
tooltip="Mark as reviewable",
default=True),
EnumDef("render_target",
items=render_target_items,
label="Render target",
default=self.render_target),
]
def get_pre_create_attr_defs(self):
image_format_enum = [
"bmp", "cin", "exr", "jpg", "pic", "pic.gz", "png",
"rad", "rat", "rta", "sgi", "tga", "tif",
]
return attrs + [
BoolDef("farm",
label="Submitting to Farm",
default=True),
BoolDef("export_job",
label="Split export and render jobs",
default=self.export_job),
attrs = [
EnumDef("image_format",
image_format_enum,
default=self.ext,
label="Image Format Options")
label="Image Format Options"),
]
return attrs + self.get_instance_attr_defs()

View file

@ -11,15 +11,23 @@ class CreateKarmaROP(plugin.HoudiniCreator):
product_type = "karma_rop"
icon = "magic"
# Default render target
render_target = "farm"
def create(self, product_name, instance_data, pre_create_data):
import hou # noqa
# Transfer settings from pre create to instance
creator_attributes = instance_data.setdefault(
"creator_attributes", dict())
for key in ["render_target", "review"]:
if key in pre_create_data:
creator_attributes[key] = pre_create_data[key]
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"] = pre_create_data.get("farm")
instance = super(CreateKarmaROP, self).create(
product_name,
@ -86,18 +94,40 @@ class CreateKarmaROP(plugin.HoudiniCreator):
to_lock = ["productType", "id"]
self.lock_parameters(instance_node, to_lock)
def get_pre_create_attr_defs(self):
attrs = super(CreateKarmaROP, self).get_pre_create_attr_defs()
def get_instance_attr_defs(self):
"""get instance attribute definitions.
Attributes defined in this method are exposed in
publish tab in the publisher UI.
"""
render_target_items = {
"local": "Local machine rendering",
"local_no_render": "Use existing frames (local)",
"farm": "Farm Rendering",
}
return [
BoolDef("review",
label="Review",
tooltip="Mark as reviewable",
default=True),
EnumDef("render_target",
items=render_target_items,
label="Render target",
default=self.render_target)
]
def get_pre_create_attr_defs(self):
image_format_enum = [
"bmp", "cin", "exr", "jpg", "pic", "pic.gz", "png",
"rad", "rat", "rta", "sgi", "tga", "tif",
]
return attrs + [
BoolDef("farm",
label="Submitting to Farm",
default=True),
attrs = super(CreateKarmaROP, self).get_pre_create_attr_defs()
attrs += [
EnumDef("image_format",
image_format_enum,
default="exr",
@ -112,5 +142,6 @@ class CreateKarmaROP(plugin.HoudiniCreator):
decimals=0),
BoolDef("cam_res",
label="Camera Resolution",
default=False)
default=False),
]
return attrs + self.get_instance_attr_defs()

View file

@ -11,18 +11,22 @@ class CreateMantraROP(plugin.HoudiniCreator):
product_type = "mantra_rop"
icon = "magic"
# Default to split export and render jobs
export_job = True
# Default render target
render_target = "farm_split"
def create(self, product_name, instance_data, pre_create_data):
import hou # noqa
# Transfer settings from pre create to instance
creator_attributes = instance_data.setdefault(
"creator_attributes", dict())
for key in ["render_target", "review"]:
if key in pre_create_data:
creator_attributes[key] = pre_create_data[key]
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"] = pre_create_data.get("farm")
instance = super(CreateMantraROP, self).create(
product_name,
@ -46,7 +50,7 @@ class CreateMantraROP(plugin.HoudiniCreator):
"vm_picture": filepath,
}
if pre_create_data.get("export_job"):
if pre_create_data.get("render_target") == "farm_split":
ifd_filepath = \
"{export_dir}{product_name}/{product_name}.$F4.ifd".format(
export_dir=hou.text.expandString("$HIP/pyblish/ifd/"),
@ -77,21 +81,40 @@ class CreateMantraROP(plugin.HoudiniCreator):
to_lock = ["productType", "id"]
self.lock_parameters(instance_node, to_lock)
def get_pre_create_attr_defs(self):
attrs = super(CreateMantraROP, self).get_pre_create_attr_defs()
def get_instance_attr_defs(self):
"""get instance attribute definitions.
Attributes defined in this method are exposed in
publish tab in the publisher UI.
"""
render_target_items = {
"local": "Local machine rendering",
"local_no_render": "Use existing frames (local)",
"farm": "Farm Rendering",
"farm_split": "Farm Rendering - Split export & render jobs",
}
return [
BoolDef("review",
label="Review",
tooltip="Mark as reviewable",
default=True),
EnumDef("render_target",
items=render_target_items,
label="Render target",
default=self.render_target)
]
def get_pre_create_attr_defs(self):
image_format_enum = [
"bmp", "cin", "exr", "jpg", "pic", "pic.gz", "png",
"rad", "rat", "rta", "sgi", "tga", "tif",
]
return attrs + [
BoolDef("farm",
label="Submitting to Farm",
default=True),
BoolDef("export_job",
label="Split export and render jobs",
default=self.export_job),
attrs = super(CreateMantraROP, self).get_pre_create_attr_defs()
attrs += [
EnumDef("image_format",
image_format_enum,
default="exr",
@ -100,5 +123,6 @@ class CreateMantraROP(plugin.HoudiniCreator):
label="Override Camera Resolution",
tooltip="Override the current camera "
"resolution, recommended for IPR.",
default=False)
default=False),
]
return attrs + self.get_instance_attr_defs()

View file

@ -17,17 +17,21 @@ class CreateRedshiftROP(plugin.HoudiniCreator):
ext = "exr"
multi_layered_mode = "No Multi-Layered EXR File"
# Default to split export and render jobs
split_render = True
# Default render target
render_target = "farm_split"
def create(self, product_name, instance_data, pre_create_data):
# Transfer settings from pre create to instance
creator_attributes = instance_data.setdefault(
"creator_attributes", dict())
for key in ["render_target", "review"]:
if key in pre_create_data:
creator_attributes[key] = pre_create_data[key]
instance_data.pop("active", None)
instance_data.update({"node_type": "Redshift_ROP"})
# Add chunk size attribute
instance_data["chunkSize"] = 10
# Submit for job publishing
instance_data["farm"] = pre_create_data.get("farm")
instance = super(CreateRedshiftROP, self).create(
product_name,
@ -99,7 +103,7 @@ class CreateRedshiftROP(plugin.HoudiniCreator):
rs_filepath = f"{export_dir}{product_name}/{product_name}.$F4.rs"
parms["RS_archive_file"] = rs_filepath
if pre_create_data.get("split_render", self.split_render):
if pre_create_data.get("render_target") == "farm_split":
parms["RS_archive_enable"] = 1
instance_node.setParms(parms)
@ -118,24 +122,44 @@ class CreateRedshiftROP(plugin.HoudiniCreator):
return super(CreateRedshiftROP, self).remove_instances(instances)
def get_instance_attr_defs(self):
"""get instance attribute definitions.
Attributes defined in this method are exposed in
publish tab in the publisher UI.
"""
render_target_items = {
"local": "Local machine rendering",
"local_no_render": "Use existing frames (local)",
"farm": "Farm Rendering",
"farm_split": "Farm Rendering - Split export & render jobs",
}
return [
BoolDef("review",
label="Review",
tooltip="Mark as reviewable",
default=True),
EnumDef("render_target",
items=render_target_items,
label="Render target",
default=self.render_target)
]
def get_pre_create_attr_defs(self):
attrs = super(CreateRedshiftROP, self).get_pre_create_attr_defs()
image_format_enum = [
"exr", "tif", "jpg", "png",
]
multi_layered_mode = [
"No Multi-Layered EXR File",
"Full Multi-Layered EXR File"
]
return attrs + [
BoolDef("farm",
label="Submitting to Farm",
default=True),
BoolDef("split_render",
label="Split export and render jobs",
default=self.split_render),
attrs = super(CreateRedshiftROP, self).get_pre_create_attr_defs()
attrs += [
EnumDef("image_format",
image_format_enum,
default=self.ext,
@ -143,5 +167,6 @@ class CreateRedshiftROP(plugin.HoudiniCreator):
EnumDef("multi_layered_mode",
multi_layered_mode,
default=self.multi_layered_mode,
label="Multi-Layered EXR")
label="Multi-Layered EXR"),
]
return attrs + self.get_instance_attr_defs()

View file

@ -16,17 +16,21 @@ class CreateVrayROP(plugin.HoudiniCreator):
icon = "magic"
ext = "exr"
# Default to split export and render jobs
export_job = True
# Default render target
render_target = "farm_split"
def create(self, product_name, instance_data, pre_create_data):
# Transfer settings from pre create to instance
creator_attributes = instance_data.setdefault(
"creator_attributes", dict())
for key in ["render_target", "review"]:
if key in pre_create_data:
creator_attributes[key] = pre_create_data[key]
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"] = pre_create_data.get("farm")
instance = super(CreateVrayROP, self).create(
product_name,
@ -55,7 +59,7 @@ class CreateVrayROP(plugin.HoudiniCreator):
"SettingsEXR_bits_per_channel": "16" # half precision
}
if pre_create_data.get("export_job"):
if pre_create_data.get("render_target") == "farm_split":
scene_filepath = \
"{export_dir}{product_name}/{product_name}.$F4.vrscene".format(
export_dir=hou.text.expandString("$HIP/pyblish/vrscene/"),
@ -143,20 +147,41 @@ class CreateVrayROP(plugin.HoudiniCreator):
return super(CreateVrayROP, self).remove_instances(instances)
def get_instance_attr_defs(self):
"""get instance attribute definitions.
Attributes defined in this method are exposed in
publish tab in the publisher UI.
"""
render_target_items = {
"local": "Local machine rendering",
"local_no_render": "Use existing frames (local)",
"farm": "Farm Rendering",
"farm_split": "Farm Rendering - Split export & render jobs",
}
return [
BoolDef("review",
label="Review",
tooltip="Mark as reviewable",
default=True),
EnumDef("render_target",
items=render_target_items,
label="Render target",
default=self.render_target)
]
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 + [
BoolDef("farm",
label="Submitting to Farm",
default=True),
BoolDef("export_job",
label="Split export and render jobs",
default=self.export_job),
attrs = super(CreateVrayROP, self).get_pre_create_attr_defs()
attrs += [
EnumDef("image_format",
image_format_enum,
default=self.ext,
@ -172,3 +197,4 @@ class CreateVrayROP(plugin.HoudiniCreator):
"if enabled",
default=False)
]
return attrs + self.get_instance_attr_defs()

View file

@ -95,7 +95,7 @@ class CreateWorkfile(plugin.HoudiniCreatorBase, AutoCreator):
# write workfile information to context container.
op_ctx = hou.node(CONTEXT_CONTAINER)
if not op_ctx:
op_ctx = self.create_context_node()
op_ctx = self.host.create_context_node()
workfile_data = {"workfile": current_instance.data_to_store()}
imprint(op_ctx, workfile_data)

View file

@ -40,12 +40,9 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin):
default_prefix = evalParmNoFrame(rop, "ar_picture")
render_products = []
# Store whether we are splitting the render job (export + render)
split_render = bool(rop.parm("ar_ass_export_enable").eval())
instance.data["splitRender"] = split_render
export_prefix = None
export_products = []
if split_render:
if instance.data["splitRender"]:
export_prefix = evalParmNoFrame(
rop, "ar_ass_file", pad_character="0"
)
@ -68,7 +65,12 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin):
"": self.generate_expected_files(instance, beauty_product)
}
# Assume it's a multipartExr Render.
multipartExr = True
num_aovs = rop.evalParm("ar_aovs")
# TODO: Check the following logic.
# as it always assumes that all AOV are not merged.
for index in range(1, num_aovs + 1):
# Skip disabled AOVs
if not rop.evalParm("ar_enable_aov{}".format(index)):
@ -85,6 +87,14 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin):
files_by_aov[label] = self.generate_expected_files(instance,
aov_product)
# Set to False as soon as we have a separated aov.
multipartExr = False
# Review Logic expects this key to exist and be True
# if render is a multipart Exr.
# As long as we have one AOV then multipartExr should be True.
instance.data["multipartExr"] = multipartExr
for product in render_products:
self.log.debug("Found render product: {}".format(product))

View file

@ -0,0 +1,35 @@
import pyblish.api
class CollectFarmInstances(pyblish.api.InstancePlugin):
"""Collect instances for farm render."""
order = pyblish.api.CollectorOrder
families = ["mantra_rop",
"karma_rop",
"redshift_rop",
"arnold_rop",
"vray_rop"]
hosts = ["houdini"]
targets = ["local", "remote"]
label = "Collect farm instances"
def process(self, instance):
creator_attribute = instance.data["creator_attributes"]
# Collect Render Target
if creator_attribute.get("render_target") not in {
"farm_split", "farm"
}:
instance.data["farm"] = False
instance.data["splitRender"] = False
self.log.debug("Render on farm is disabled. "
"Skipping farm collecting.")
return
instance.data["farm"] = True
instance.data["splitRender"] = (
creator_attribute.get("render_target") == "farm_split"
)

View file

@ -55,6 +55,12 @@ class CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin):
beauty_product)
}
# Review Logic expects this key to exist and be True
# if render is a multipart Exr.
# As long as we have one AOV then multipartExr should be True.
# By default karma render is a multipart Exr.
instance.data["multipartExr"] = True
filenames = list(render_products)
instance.data["files"] = filenames
instance.data["renderProducts"] = colorspace.ARenderProduct()

View file

@ -0,0 +1,137 @@
import os
import pyblish.api
from ayon_core.pipeline.create import get_product_name
from ayon_core.pipeline.farm.patterning import match_aov_pattern
from ayon_core.pipeline.publish import (
get_plugin_settings,
apply_plugin_settings_automatically
)
class CollectLocalRenderInstances(pyblish.api.InstancePlugin):
"""Collect instances for local render.
Agnostic Local Render Collector.
"""
# this plugin runs after Collect Render Products
order = pyblish.api.CollectorOrder + 0.12
families = ["mantra_rop",
"karma_rop",
"redshift_rop",
"arnold_rop",
"vray_rop"]
hosts = ["houdini"]
label = "Collect local render instances"
use_deadline_aov_filter = False
aov_filter = {"host_name": "houdini",
"value": [".*([Bb]eauty).*"]}
@classmethod
def apply_settings(cls, project_settings):
# Preserve automatic settings applying logic
settings = get_plugin_settings(plugin=cls,
project_settings=project_settings,
log=cls.log,
category="houdini")
apply_plugin_settings_automatically(cls, settings, logger=cls.log)
if not cls.use_deadline_aov_filter:
# get aov_filter from collector settings
# and restructure it as match_aov_pattern requires.
cls.aov_filter = {
cls.aov_filter["host_name"]: cls.aov_filter["value"]
}
else:
# get aov_filter from deadline settings
cls.aov_filter = project_settings["deadline"]["publish"]["ProcessSubmittedJobOnFarm"]["aov_filter"]
cls.aov_filter = {
item["name"]: item["value"]
for item in cls.aov_filter
}
def process(self, instance):
if instance.data["farm"]:
self.log.debug("Render on farm is enabled. "
"Skipping local render collecting.")
return
# Create Instance for each AOV.
context = instance.context
expectedFiles = next(iter(instance.data["expectedFiles"]), {})
product_type = "render" # is always render
product_group = get_product_name(
context.data["projectName"],
context.data["taskEntity"]["name"],
context.data["taskEntity"]["taskType"],
context.data["hostName"],
product_type,
instance.data["productName"]
)
for aov_name, aov_filepaths in expectedFiles.items():
product_name = product_group
if aov_name:
product_name = "{}_{}".format(product_name, aov_name)
# Create instance for each AOV
aov_instance = context.create_instance(product_name)
# Prepare Representation for each AOV
aov_filenames = [os.path.basename(path) for path in aov_filepaths]
staging_dir = os.path.dirname(aov_filepaths[0])
ext = aov_filepaths[0].split(".")[-1]
# Decide if instance is reviewable
preview = False
if instance.data.get("multipartExr", False):
# Add preview tag because its multipartExr.
preview = True
else:
# Add Preview tag if the AOV matches the filter.
preview = match_aov_pattern(
"houdini", self.aov_filter, aov_filenames[0]
)
preview = preview and instance.data.get("review", False)
# Support Single frame.
# The integrator wants single files to be a single
# filename instead of a list.
# More info: https://github.com/ynput/ayon-core/issues/238
if len(aov_filenames) == 1:
aov_filenames = aov_filenames[0]
aov_instance.data.update({
# 'label': label,
"task": instance.data["task"],
"folderPath": instance.data["folderPath"],
"frameStart": instance.data["frameStartHandle"],
"frameEnd": instance.data["frameEndHandle"],
"productType": product_type,
"family": product_type,
"productName": product_name,
"productGroup": product_group,
"families": ["render.local.hou", "review"],
"instance_node": instance.data["instance_node"],
"representations": [
{
"stagingDir": staging_dir,
"ext": ext,
"name": ext,
"tags": ["review"] if preview else [],
"files": aov_filenames,
"frameStart": instance.data["frameStartHandle"],
"frameEnd": instance.data["frameEndHandle"]
}
]
})
# Remove original render instance
# I can't remove it here as I still need it to trigger the render.
# context.remove(instance)

View file

@ -44,12 +44,9 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin):
default_prefix = evalParmNoFrame(rop, "vm_picture")
render_products = []
# Store whether we are splitting the render job (export + render)
split_render = bool(rop.parm("soho_outputmode").eval())
instance.data["splitRender"] = split_render
export_prefix = None
export_products = []
if split_render:
if instance.data["splitRender"]:
export_prefix = evalParmNoFrame(
rop, "soho_diskfile", pad_character="0"
)
@ -74,6 +71,11 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin):
beauty_product)
}
# Assume it's a multipartExr Render.
multipartExr = True
# TODO: This logic doesn't take into considerations
# cryptomatte defined in 'Images > Cryptomatte'
aov_numbers = rop.evalParm("vm_numaux")
if aov_numbers > 0:
# get the filenames of the AOVs
@ -93,6 +95,14 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin):
files_by_aov[var] = self.generate_expected_files(instance, aov_product) # noqa
# Set to False as soon as we have a separated aov.
multipartExr = False
# Review Logic expects this key to exist and be True
# if render is a multipart Exr.
# As long as we have one AOV then multipartExr should be True.
instance.data["multipartExr"] = multipartExr
for product in render_products:
self.log.debug("Found render product: %s" % product)

View file

@ -42,11 +42,9 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin):
default_prefix = evalParmNoFrame(rop, "RS_outputFileNamePrefix")
beauty_suffix = rop.evalParm("RS_outputBeautyAOVSuffix")
# Store whether we are splitting the render job (export + render)
split_render = bool(rop.parm("RS_archive_enable").eval())
instance.data["splitRender"] = split_render
export_products = []
if split_render:
if instance.data["splitRender"]:
export_prefix = evalParmNoFrame(
rop, "RS_archive_file", pad_character="0"
)
@ -63,9 +61,12 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin):
full_exr_mode = (rop.evalParm("RS_outputMultilayerMode") == "2")
if full_exr_mode:
# Ignore beauty suffix if full mode is enabled
# As this is what the rop does.
# As this is what the rop does.
beauty_suffix = ""
# Assume it's a multipartExr Render.
multipartExr = True
# Default beauty/main layer AOV
beauty_product = self.get_render_product_name(
prefix=default_prefix, suffix=beauty_suffix
@ -75,7 +76,7 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin):
beauty_suffix: self.generate_expected_files(instance,
beauty_product)
}
aovs_rop = rop.parm("RS_aovGetFromNode").evalAsNode()
if aovs_rop:
rop = aovs_rop
@ -98,13 +99,21 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin):
if rop.parm(f"RS_aovID_{i}").evalAsString() == "CRYPTOMATTE" or \
not full_exr_mode:
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
# Set to False as soon as we have a separated aov.
multipartExr = False
# Review Logic expects this key to exist and be True
# if render is a multipart Exr.
# As long as we have one AOV then multipartExr should be True.
instance.data["multipartExr"] = multipartExr
for product in render_products:
self.log.debug("Found render product: %s" % product)

View file

@ -8,7 +8,8 @@ class CollectHoudiniReviewData(pyblish.api.InstancePlugin):
label = "Collect Review Data"
# This specific order value is used so that
# this plugin runs after CollectRopFrameRange
order = pyblish.api.CollectorOrder + 0.1
# Also after CollectLocalRenderInstances
order = pyblish.api.CollectorOrder + 0.13
hosts = ["houdini"]
families = ["review"]
@ -28,7 +29,8 @@ class CollectHoudiniReviewData(pyblish.api.InstancePlugin):
ropnode_path = instance.data["instance_node"]
ropnode = hou.node(ropnode_path)
camera_path = ropnode.parm("camera").eval()
# Get camera based on the instance_node type.
camera_path = self._get_camera_path(ropnode)
camera_node = hou.node(camera_path)
if not camera_node:
self.log.warning("No valid camera node found on review node: "
@ -55,3 +57,29 @@ class CollectHoudiniReviewData(pyblish.api.InstancePlugin):
# Store focal length in `burninDataMembers`
burnin_members = instance.data.setdefault("burninDataMembers", {})
burnin_members["focalLength"] = focal_length
def _get_camera_path(self, ropnode):
"""Get the camera path associated with the given rop node.
This function evaluates the camera parameter according to the
type of the given rop node.
Returns:
Union[str, None]: Camera path or None.
This function can return empty string if the camera
path is empty i.e. no camera path.
"""
if ropnode.type().name() in {
"opengl", "karma", "ifd", "arnold"
}:
return ropnode.parm("camera").eval()
elif ropnode.type().name() == "Redshift_ROP":
return ropnode.parm("RS_renderCamera").eval()
elif ropnode.type().name() == "vray_renderer":
return ropnode.parm("render_camera").eval()
return None

View file

@ -0,0 +1,22 @@
import pyblish.api
class CollectReviewableInstances(pyblish.api.InstancePlugin):
"""Collect Reviewable Instances.
Basically, all instances of the specified families
with creator_attribure["review"]
"""
order = pyblish.api.CollectorOrder
label = "Collect Reviewable Instances"
families = ["mantra_rop",
"karma_rop",
"redshift_rop",
"arnold_rop",
"vray_rop"]
def process(self, instance):
creator_attribute = instance.data["creator_attributes"]
instance.data["review"] = creator_attribute.get("review", False)

View file

@ -45,12 +45,9 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin):
render_products = []
# TODO: add render elements if render element
# Store whether we are splitting the render job in an export + render
split_render = rop.parm("render_export_mode").eval() == "2"
instance.data["splitRender"] = split_render
export_prefix = None
export_products = []
if split_render:
if instance.data["splitRender"]:
export_prefix = evalParmNoFrame(
rop, "render_export_filepath", pad_character="0"
)
@ -70,6 +67,9 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin):
"": self.generate_expected_files(instance,
beauty_product)}
# Assume it's a multipartExr Render.
multipartExr = True
if instance.data.get("RenderElement", True):
render_element = self.get_render_element_name(rop, default_prefix)
if render_element:
@ -77,7 +77,13 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin):
render_products.append(renderpass)
files_by_aov[aov] = self.generate_expected_files(
instance, renderpass)
# Set to False as soon as we have a separated aov.
multipartExr = False
# Review Logic expects this key to exist and be True
# if render is a multipart Exr.
# As long as we have one AOV then multipartExr should be True.
instance.data["multipartExr"] = multipartExr
for product in render_products:
self.log.debug("Found render product: %s" % product)

View file

@ -19,6 +19,16 @@ class ExtractOpenGL(publish.Extractor,
def process(self, instance):
ropnode = hou.node(instance.data.get("instance_node"))
# This plugin is triggered when marking render as reviewable.
# Therefore, this plugin will run on over wrong instances.
# TODO: Don't run this plugin on wrong instances.
# This plugin should run only on review product type
# with instance node of opengl type.
if ropnode.type().name() != "opengl":
self.log.debug("Skipping OpenGl extraction. Rop node {} "
"is not an OpenGl node.".format(ropnode.path()))
return
output = ropnode.evalParm("picture")
staging_dir = os.path.normpath(os.path.dirname(output))
instance.data["stagingDir"] = staging_dir

View file

@ -0,0 +1,74 @@
import pyblish.api
from ayon_core.pipeline import publish
from ayon_core.hosts.houdini.api.lib import render_rop
import hou
import os
class ExtractRender(publish.Extractor):
order = pyblish.api.ExtractorOrder
label = "Extract Render"
hosts = ["houdini"]
families = ["mantra_rop",
"karma_rop",
"redshift_rop",
"arnold_rop",
"vray_rop"]
def process(self, instance):
creator_attribute = instance.data["creator_attributes"]
product_type = instance.data["productType"]
rop_node = hou.node(instance.data.get("instance_node"))
# Align split parameter value on rop node to the render target.
if instance.data["splitRender"]:
if product_type == "arnold_rop":
rop_node.setParms({"ar_ass_export_enable": 1})
elif product_type == "mantra_rop":
rop_node.setParms({"soho_outputmode": 1})
elif product_type == "redshift_rop":
rop_node.setParms({"RS_archive_enable": 1})
elif product_type == "vray_rop":
rop_node.setParms({"render_export_mode": "2"})
else:
if product_type == "arnold_rop":
rop_node.setParms({"ar_ass_export_enable": 0})
elif product_type == "mantra_rop":
rop_node.setParms({"soho_outputmode": 0})
elif product_type == "redshift_rop":
rop_node.setParms({"RS_archive_enable": 0})
elif product_type == "vray_rop":
rop_node.setParms({"render_export_mode": "1"})
if instance.data.get("farm"):
self.log.debug("Render should be processed on farm, skipping local render.")
return
if creator_attribute.get("render_target") == "local":
ropnode = hou.node(instance.data.get("instance_node"))
render_rop(ropnode)
# `ExpectedFiles` is a list that includes one dict.
expected_files = instance.data["expectedFiles"][0]
# Each key in that dict is a list of files.
# Combine lists of files into one big list.
all_frames = []
for value in expected_files.values():
if isinstance(value, str):
all_frames.append(value)
elif isinstance(value, list):
all_frames.extend(value)
# Check missing frames.
# Frames won't exist if user cancels the render.
missing_frames = [
frame
for frame in all_frames
if not os.path.exists(frame)
]
if missing_frames:
# TODO: Use user friendly error reporting.
raise RuntimeError("Failed to complete render extraction. "
"Missing output files: {}".format(
missing_frames))

View file

@ -17,11 +17,13 @@ class IncrementCurrentFile(pyblish.api.ContextPlugin):
order = pyblish.api.IntegratorOrder + 9.0
hosts = ["houdini"]
families = ["workfile",
"redshift_rop",
"arnold_rop",
"usdrender",
"mantra_rop",
"karma_rop",
"usdrender",
"redshift_rop",
"arnold_rop",
"vray_rop",
"render.local.hou",
"publish.hou"]
optional = True

View file

@ -56,6 +56,18 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin,
def process(self, instance):
rop_node = hou.node(instance.data["instance_node"])
# This plugin is triggered when marking render as reviewable.
# Therefore, this plugin will run on over wrong instances.
# TODO: Don't run this plugin on wrong instances.
# This plugin should run only on review product type
# with instance node of opengl type.
if rop_node.type().name() != "opengl":
self.log.debug("Skipping Validation. Rop node {} "
"is not an OpenGl node.".format(rop_node.path()))
return
if not self.is_active(instance.data):
return
@ -66,7 +78,6 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin,
)
return
rop_node = hou.node(instance.data["instance_node"])
if rop_node.evalParm("colorcorrect") != 2:
# any colorspace settings other than default requires
# 'Color Correct' parm to be set to 'OpenColorIO'

View file

@ -20,6 +20,16 @@ class ValidateSceneReview(pyblish.api.InstancePlugin):
report = []
instance_node = hou.node(instance.data.get("instance_node"))
# This plugin is triggered when marking render as reviewable.
# Therefore, this plugin will run on over wrong instances.
# TODO: Don't run this plugin on wrong instances.
# This plugin should run only on review product type
# with instance node of opengl type.
if instance_node.type().name() != "opengl":
self.log.debug("Skipping Validation. Rop node {} "
"is not an OpenGl node.".format(instance_node.path()))
return
invalid = self.get_invalid_scene_path(instance_node)
if invalid:
report.append(invalid)

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- OPMenu Stencil.
It's used to extend the OPMenu.
-->
<menuDocument>
<menu>
<!-- Operator type and asset options. -->
<subMenu id="opmenu.vhda_options_create">
<insertBefore>opmenu.unsynchronize</insertBefore>
<scriptItem id="opmenu.vhda_create_ayon">
<insertAfter>opmenu.vhda_create</insertAfter>
<label>Create New (AYON)...</label>
<context>
</context>
<scriptCode>
<![CDATA[
from ayon_core.hosts.houdini.api.creator_node_shelves import create_interactive
node = kwargs["node"]
if node not in hou.selectedNodes():
node.setSelected(True)
create_interactive("io.openpype.creators.houdini.hda", **kwargs)
]]>
</scriptCode>
</scriptItem>
</subMenu>
</menu>
</menuDocument>

View file

@ -369,12 +369,8 @@ def reset_colorspace():
"""
if int(get_max_version()) < 2024:
return
project_name = get_current_project_name()
colorspace_mgr = rt.ColorPipelineMgr
project_settings = get_project_settings(project_name)
max_config_data = colorspace.get_imageio_config(
project_name, "max", project_settings)
max_config_data = colorspace.get_current_context_imageio_config_preset()
if max_config_data:
ocio_config_path = max_config_data["path"]
colorspace_mgr = rt.ColorPipelineMgr
@ -389,10 +385,7 @@ def check_colorspace():
"because Max main window can't be found.")
if int(get_max_version()) >= 2024:
color_mgr = rt.ColorPipelineMgr
project_name = get_current_project_name()
project_settings = get_project_settings(project_name)
max_config_data = colorspace.get_imageio_config(
project_name, "max", project_settings)
max_config_data = colorspace.get_current_context_imageio_config_preset()
if max_config_data and color_mgr.Mode != rt.Name("OCIO_Custom"):
if not is_headless():
from ayon_core.tools.utils import SimplePopup

View file

@ -52,11 +52,7 @@ class MaxHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
self._has_been_setup = True
def context_setting():
return lib.set_context_setting()
rt.callbacks.addScript(rt.Name('systemPostNew'),
context_setting)
rt.callbacks.addScript(rt.Name('systemPostNew'), on_new)
rt.callbacks.addScript(rt.Name('filePostOpen'),
lib.check_colorspace)
@ -163,6 +159,14 @@ def ls() -> list:
yield lib.read(container)
def on_new():
lib.set_context_setting()
if rt.checkForSave():
rt.resetMaxFile(rt.Name("noPrompt"))
rt.clearUndoBuffer()
rt.redrawViews()
def containerise(name: str, nodes: list, context,
namespace=None, loader=None, suffix="_CON"):
data = {

View file

@ -47,7 +47,7 @@ class FBXExtractor:
"smoothMesh": bool,
"instances": bool,
# "referencedContainersContent": bool, # deprecated in Maya 2016+
"bakeComplexAnimation": int,
"bakeComplexAnimation": bool,
"bakeComplexStart": int,
"bakeComplexEnd": int,
"bakeComplexStep": int,
@ -59,6 +59,7 @@ class FBXExtractor:
"constraints": bool,
"lights": bool,
"embeddedTextures": bool,
"includeChildren": bool,
"inputConnections": bool,
"upAxis": str, # x, y or z,
"triangulate": bool,
@ -102,6 +103,7 @@ class FBXExtractor:
"constraints": False,
"lights": True,
"embeddedTextures": False,
"includeChildren": True,
"inputConnections": True,
"upAxis": "y",
"triangulate": False,

View file

@ -8,7 +8,7 @@ from ayon_core.pipeline import (
from ayon_core.pipeline.load.utils import get_representation_path_from_context
from ayon_core.pipeline.colorspace import (
get_imageio_file_rules_colorspace_from_filepath,
get_imageio_config,
get_current_context_imageio_config_preset,
get_imageio_file_rules
)
from ayon_core.settings import get_project_settings
@ -270,8 +270,7 @@ class FileNodeLoader(load.LoaderPlugin):
host_name = get_current_host_name()
project_settings = get_project_settings(project_name)
config_data = get_imageio_config(
project_name, host_name,
config_data = get_current_context_imageio_config_preset(
project_settings=project_settings
)

View file

@ -35,7 +35,8 @@ class ExtractFBXAnimation(publish.Extractor):
fbx_exporter = fbx.FBXExtractor(log=self.log)
out_members = instance.data.get("animated_skeleton", [])
# Export
instance.data["constraints"] = True
# TODO: need to set up the options for users to set up
# the flags they intended to export
instance.data["skeletonDefinitions"] = True
instance.data["referencedAssetsContent"] = True
fbx_exporter.set_options_from_instance(instance)

View file

@ -43,7 +43,9 @@ from ayon_core.pipeline import (
from ayon_core.pipeline.context_tools import (
get_current_context_custom_workfile_template
)
from ayon_core.pipeline.colorspace import get_imageio_config
from ayon_core.pipeline.colorspace import (
get_current_context_imageio_config_preset
)
from ayon_core.pipeline.workfile import BuildWorkfile
from . import gizmo_menu
from .constants import ASSIST
@ -1552,10 +1554,7 @@ class WorkfileSettings(object):
imageio_host (dict): host colorspace configurations
'''
config_data = get_imageio_config(
project_name=get_current_project_name(),
host_name="nuke"
)
config_data = get_current_context_imageio_config_preset()
workfile_settings = imageio_host["workfile"]
color_management = workfile_settings["color_management"]

View file

@ -62,7 +62,7 @@ class LoadBackdropNodes(load.LoaderPlugin):
}
# add attributes from the version to imprint to metadata knob
for k in ["source", "author", "fps"]:
for k in ["source", "fps"]:
data_imprint[k] = version_attributes[k]
# getting file path
@ -206,7 +206,7 @@ class LoadBackdropNodes(load.LoaderPlugin):
"colorspaceInput": colorspace,
}
for k in ["source", "author", "fps"]:
for k in ["source", "fps"]:
data_imprint[k] = version_attributes[k]
# adding nodes to node graph

View file

@ -48,7 +48,7 @@ class AlembicCameraLoader(load.LoaderPlugin):
"frameEnd": last,
"version": version_entity["version"],
}
for k in ["source", "author", "fps"]:
for k in ["source", "fps"]:
data_imprint[k] = version_attributes[k]
# getting file path
@ -123,7 +123,7 @@ class AlembicCameraLoader(load.LoaderPlugin):
}
# add attributes from the version to imprint to metadata knob
for k in ["source", "author", "fps"]:
for k in ["source", "fps"]:
data_imprint[k] = version_attributes[k]
# getting file path

View file

@ -9,7 +9,8 @@ from ayon_core.pipeline import (
get_representation_path,
)
from ayon_core.pipeline.colorspace import (
get_imageio_file_rules_colorspace_from_filepath
get_imageio_file_rules_colorspace_from_filepath,
get_current_context_imageio_config_preset,
)
from ayon_core.hosts.nuke.api.lib import (
get_imageio_input_colorspace,
@ -197,7 +198,6 @@ class LoadClip(plugin.NukeLoader):
"frameStart",
"frameEnd",
"source",
"author",
"fps",
"handleStart",
"handleEnd",
@ -347,8 +347,7 @@ class LoadClip(plugin.NukeLoader):
"source": version_attributes.get("source"),
"handleStart": str(self.handle_start),
"handleEnd": str(self.handle_end),
"fps": str(version_attributes.get("fps")),
"author": version_attributes.get("author")
"fps": str(version_attributes.get("fps"))
}
last_version_entity = ayon_api.get_last_version_by_product_id(
@ -547,9 +546,10 @@ class LoadClip(plugin.NukeLoader):
f"Colorspace from representation colorspaceData: {colorspace}"
)
config_data = get_current_context_imageio_config_preset()
# check if any filerules are not applicable
new_parsed_colorspace = get_imageio_file_rules_colorspace_from_filepath( # noqa
filepath, "nuke", project_name
filepath, "nuke", project_name, config_data=config_data
)
self.log.debug(f"Colorspace new filerules: {new_parsed_colorspace}")

View file

@ -69,7 +69,6 @@ class LoadEffects(load.LoaderPlugin):
"handleStart",
"handleEnd",
"source",
"author",
"fps"
]:
data_imprint[k] = version_attributes[k]
@ -189,7 +188,6 @@ class LoadEffects(load.LoaderPlugin):
"handleStart",
"handleEnd",
"source",
"author",
"fps",
]:
data_imprint[k] = version_attributes[k]

View file

@ -69,7 +69,6 @@ class LoadEffectsInputProcess(load.LoaderPlugin):
"handleStart",
"handleEnd",
"source",
"author",
"fps"
]:
data_imprint[k] = version_attributes[k]
@ -192,7 +191,6 @@ class LoadEffectsInputProcess(load.LoaderPlugin):
"handleStart",
"handleEnd",
"source",
"author",
"fps"
]:
data_imprint[k] = version_attributes[k]

View file

@ -71,7 +71,6 @@ class LoadGizmo(load.LoaderPlugin):
"handleStart",
"handleEnd",
"source",
"author",
"fps"
]:
data_imprint[k] = version_attributes[k]
@ -139,7 +138,6 @@ class LoadGizmo(load.LoaderPlugin):
"handleStart",
"handleEnd",
"source",
"author",
"fps"
]:
data_imprint[k] = version_attributes[k]

View file

@ -73,7 +73,6 @@ class LoadGizmoInputProcess(load.LoaderPlugin):
"handleStart",
"handleEnd",
"source",
"author",
"fps"
]:
data_imprint[k] = version_attributes[k]
@ -145,7 +144,6 @@ class LoadGizmoInputProcess(load.LoaderPlugin):
"handleStart",
"handleEnd",
"source",
"author",
"fps"
]:
data_imprint[k] = version_attributes[k]

View file

@ -133,7 +133,7 @@ class LoadImage(load.LoaderPlugin):
"version": version_entity["version"],
"colorspace": colorspace,
}
for k in ["source", "author", "fps"]:
for k in ["source", "fps"]:
data_imprint[k] = version_attributes.get(k, str(None))
r["tile_color"].setValue(int("0x4ecd25ff", 16))
@ -207,7 +207,6 @@ class LoadImage(load.LoaderPlugin):
"colorspace": version_attributes.get("colorSpace"),
"source": version_attributes.get("source"),
"fps": str(version_attributes.get("fps")),
"author": version_attributes.get("author")
}
# change color of node

View file

@ -47,7 +47,7 @@ class AlembicModelLoader(load.LoaderPlugin):
"version": version_entity["version"]
}
# add attributes from the version to imprint to metadata knob
for k in ["source", "author", "fps"]:
for k in ["source", "fps"]:
data_imprint[k] = version_attributes[k]
# getting file path
@ -130,7 +130,7 @@ class AlembicModelLoader(load.LoaderPlugin):
}
# add additional metadata from the version to imprint to Avalon knob
for k in ["source", "author", "fps"]:
for k in ["source", "fps"]:
data_imprint[k] = version_attributes[k]
# getting file path

View file

@ -55,7 +55,6 @@ class LinkAsGroup(load.LoaderPlugin):
"handleStart",
"handleEnd",
"source",
"author",
"fps"
]:
data_imprint[k] = version_attributes[k]
@ -131,7 +130,6 @@ class LinkAsGroup(load.LoaderPlugin):
"colorspace": version_attributes.get("colorSpace"),
"source": version_attributes.get("source"),
"fps": version_attributes.get("fps"),
"author": version_attributes.get("author")
}
# Update the imprinted representation

View file

@ -156,14 +156,9 @@ This creator publishes color space look file (LUT).
]
def apply_settings(self, project_settings):
host = self.create_context.host
host_name = host.name
project_name = host.get_current_project_name()
config_data = colorspace.get_imageio_config(
project_name, host_name,
config_data = colorspace.get_current_context_imageio_config_preset(
project_settings=project_settings
)
if not config_data:
self.enabled = False
return

View file

@ -0,0 +1,96 @@
from pathlib import Path
from ayon_core.pipeline import (
CreatedInstance,
)
from ayon_core.lib.attribute_definitions import (
FileDef,
BoolDef,
TextDef,
)
from ayon_core.hosts.traypublisher.api.plugin import TrayPublishCreator
class EditorialPackageCreator(TrayPublishCreator):
"""Creates instance for OTIO file from published folder.
Folder contains OTIO file and exported .mov files. Process should publish
whole folder as single `editorial_pckg` product type and (possibly) convert
.mov files into different format and copy them into `publish` `resources`
subfolder.
"""
identifier = "editorial_pckg"
label = "Editorial package"
product_type = "editorial_pckg"
description = "Publish folder with OTIO file and resources"
# Position batch creator after simple creators
order = 120
conversion_enabled = False
def apply_settings(self, project_settings):
self.conversion_enabled = (
project_settings["traypublisher"]
["publish"]
["ExtractEditorialPckgConversion"]
["conversion_enabled"]
)
def get_icon(self):
return "fa.folder"
def create(self, product_name, instance_data, pre_create_data):
folder_path = pre_create_data.get("folder_path")
if not folder_path:
return
instance_data["creator_attributes"] = {
"folder_path": (Path(folder_path["directory"]) /
Path(folder_path["filenames"][0])).as_posix(),
"conversion_enabled": pre_create_data["conversion_enabled"]
}
# Create new instance
new_instance = CreatedInstance(self.product_type, product_name,
instance_data, self)
self._store_new_instance(new_instance)
def get_pre_create_attr_defs(self):
# Use same attributes as for instance attributes
return [
FileDef(
"folder_path",
folders=True,
single_item=True,
extensions=[],
allow_sequences=False,
label="Folder path"
),
BoolDef("conversion_enabled",
tooltip="Convert to output defined in Settings.",
default=self.conversion_enabled,
label="Convert resources"),
]
def get_instance_attr_defs(self):
return [
TextDef(
"folder_path",
label="Folder path",
disabled=True
),
BoolDef("conversion_enabled",
tooltip="Convert to output defined in Settings.",
label="Convert resources"),
]
def get_detail_description(self):
return """# Publish folder with OTIO file and video clips
Folder contains OTIO file and exported .mov files. Process should
publish whole folder as single `editorial_pckg` product type and
(possibly) convert .mov files into different format and copy them into
`publish` `resources` subfolder.
"""

View file

@ -0,0 +1,58 @@
"""Produces instance.data["editorial_pckg"] data used during integration.
Requires:
instance.data["creator_attributes"]["path"] - from creator
Provides:
instance -> editorial_pckg (dict):
folder_path (str)
otio_path (str) - from dragged folder
resource_paths (list)
"""
import os
import pyblish.api
from ayon_core.lib.transcoding import VIDEO_EXTENSIONS
class CollectEditorialPackage(pyblish.api.InstancePlugin):
"""Collects path to OTIO file and resources"""
label = "Collect Editorial Package"
order = pyblish.api.CollectorOrder - 0.1
hosts = ["traypublisher"]
families = ["editorial_pckg"]
def process(self, instance):
folder_path = instance.data["creator_attributes"]["folder_path"]
if not folder_path or not os.path.exists(folder_path):
self.log.info((
"Instance doesn't contain collected existing folder path."
))
return
instance.data["editorial_pckg"] = {}
instance.data["editorial_pckg"]["folder_path"] = folder_path
otio_path, resource_paths = (
self._get_otio_and_resource_paths(folder_path))
instance.data["editorial_pckg"]["otio_path"] = otio_path
instance.data["editorial_pckg"]["resource_paths"] = resource_paths
def _get_otio_and_resource_paths(self, folder_path):
otio_path = None
resource_paths = []
file_names = os.listdir(folder_path)
for filename in file_names:
_, ext = os.path.splitext(filename)
file_path = os.path.join(folder_path, filename)
if ext == ".otio":
otio_path = file_path
elif ext in VIDEO_EXTENSIONS:
resource_paths.append(file_path)
return otio_path, resource_paths

View file

@ -1,10 +1,7 @@
import pyblish.api
from ayon_core.pipeline import (
publish,
registered_host
)
from ayon_core.lib import EnumDef
from ayon_core.pipeline import colorspace
from ayon_core.pipeline import publish
from ayon_core.pipeline.publish import KnownPublishError
@ -19,9 +16,10 @@ class CollectColorspace(pyblish.api.InstancePlugin,
families = ["render", "plate", "reference", "image", "online"]
enabled = False
colorspace_items = [
default_colorspace_items = [
(None, "Don't override")
]
colorspace_items = list(default_colorspace_items)
colorspace_attr_show = False
config_items = None
@ -69,14 +67,13 @@ class CollectColorspace(pyblish.api.InstancePlugin,
@classmethod
def apply_settings(cls, project_settings):
host = registered_host()
host_name = host.name
project_name = host.get_current_project_name()
config_data = colorspace.get_imageio_config(
project_name, host_name,
config_data = colorspace.get_current_context_imageio_config_preset(
project_settings=project_settings
)
enabled = False
colorspace_items = list(cls.default_colorspace_items)
config_items = None
if config_data:
filepath = config_data["path"]
config_items = colorspace.get_ocio_config_colorspaces(filepath)
@ -85,9 +82,11 @@ class CollectColorspace(pyblish.api.InstancePlugin,
include_aliases=True,
include_roles=True
)
cls.config_items = config_items
cls.colorspace_items.extend(labeled_colorspaces)
cls.enabled = True
colorspace_items.extend(labeled_colorspaces)
cls.config_items = config_items
cls.colorspace_items = colorspace_items
cls.enabled = enabled
@classmethod
def get_attribute_defs(cls):

View file

@ -10,9 +10,13 @@ class CollectFrameDataFromAssetEntity(pyblish.api.InstancePlugin):
order = pyblish.api.CollectorOrder + 0.491
label = "Collect Missing Frame Data From Folder"
families = ["plate", "pointcache",
"vdbcache", "online",
"render"]
families = [
"plate",
"pointcache",
"vdbcache",
"online",
"render",
]
hosts = ["traypublisher"]
def process(self, instance):
@ -22,16 +26,26 @@ class CollectFrameDataFromAssetEntity(pyblish.api.InstancePlugin):
"frameStart",
"frameEnd",
"handleStart",
"handleEnd"
"handleEnd",
):
if key not in instance.data:
missing_keys.append(key)
# Skip the logic if all keys are already collected.
# NOTE: In editorial is not 'folderEntity' filled, so it would crash
# even if we don't need it.
if not missing_keys:
return
keys_set = []
folder_attributes = instance.data["folderEntity"]["attrib"]
for key in missing_keys:
if key in folder_attributes:
instance.data[key] = folder_attributes[key]
keys_set.append(key)
if keys_set:
self.log.debug(f"Frame range data {keys_set} "
"has been collected from folder entity.")
self.log.debug(
f"Frame range data {keys_set} "
"has been collected from folder entity."
)

View file

@ -0,0 +1,232 @@
import copy
import os.path
import subprocess
import opentimelineio
import pyblish.api
from ayon_core.lib import get_ffmpeg_tool_args, run_subprocess
from ayon_core.pipeline import publish
class ExtractEditorialPckgConversion(publish.Extractor):
"""Replaces movie paths in otio file with publish rootless
Prepares movie resources for integration (adds them to `transfers`).
Converts .mov files according to output definition.
"""
label = "Extract Editorial Package"
order = pyblish.api.ExtractorOrder - 0.45
hosts = ["traypublisher"]
families = ["editorial_pckg"]
def process(self, instance):
editorial_pckg_data = instance.data.get("editorial_pckg")
otio_path = editorial_pckg_data["otio_path"]
otio_basename = os.path.basename(otio_path)
staging_dir = self.staging_dir(instance)
editorial_pckg_repre = {
'name': "editorial_pckg",
'ext': "otio",
'files': otio_basename,
"stagingDir": staging_dir,
}
otio_staging_path = os.path.join(staging_dir, otio_basename)
instance.data["representations"].append(editorial_pckg_repre)
publish_resource_folder = self._get_publish_resource_folder(instance)
resource_paths = editorial_pckg_data["resource_paths"]
transfers = self._get_transfers(resource_paths,
publish_resource_folder)
project_settings = instance.context.data["project_settings"]
output_def = (project_settings["traypublisher"]
["publish"]
["ExtractEditorialPckgConversion"]
["output"])
conversion_enabled = (instance.data["creator_attributes"]
["conversion_enabled"])
if conversion_enabled and output_def["ext"]:
transfers = self._convert_resources(output_def, transfers)
instance.data["transfers"] = transfers
source_to_rootless = self._get_resource_path_mapping(instance,
transfers)
otio_data = editorial_pckg_data["otio_data"]
otio_data = self._replace_target_urls(otio_data, source_to_rootless)
opentimelineio.adapters.write_to_file(otio_data, otio_staging_path)
self.log.info("Added Editorial Package representation: {}".format(
editorial_pckg_repre))
def _get_publish_resource_folder(self, instance):
"""Calculates publish folder and create it."""
publish_path = self._get_published_path(instance)
publish_folder = os.path.dirname(publish_path)
publish_resource_folder = os.path.join(publish_folder, "resources")
if not os.path.exists(publish_resource_folder):
os.makedirs(publish_resource_folder, exist_ok=True)
return publish_resource_folder
def _get_resource_path_mapping(self, instance, transfers):
"""Returns dict of {source_mov_path: rootless_published_path}."""
replace_paths = {}
anatomy = instance.context.data["anatomy"]
for source, destination in transfers:
rootless_path = self._get_rootless(anatomy, destination)
source_file_name = os.path.basename(source)
replace_paths[source_file_name] = rootless_path
return replace_paths
def _get_transfers(self, resource_paths, publish_resource_folder):
"""Returns list of tuples (source, destination) with movie paths."""
transfers = []
for res_path in resource_paths:
res_basename = os.path.basename(res_path)
pub_res_path = os.path.join(publish_resource_folder, res_basename)
transfers.append((res_path, pub_res_path))
return transfers
def _replace_target_urls(self, otio_data, replace_paths):
"""Replace original movie paths with published rootless ones."""
for track in otio_data.tracks:
for clip in track:
# Check if the clip has a media reference
if clip.media_reference is not None:
# Access the target_url from the media reference
target_url = clip.media_reference.target_url
if not target_url:
continue
file_name = os.path.basename(target_url)
replace_path = replace_paths.get(file_name)
if replace_path:
clip.media_reference.target_url = replace_path
if clip.name == file_name:
clip.name = os.path.basename(replace_path)
return otio_data
def _get_rootless(self, anatomy, path):
"""Try to find rootless {root[work]} path from `path`"""
success, rootless_path = anatomy.find_root_template_from_path(
path)
if not success:
# `rootless_path` is not set to `output_dir` if none of roots match
self.log.warning(
f"Could not find root path for remapping '{path}'."
)
rootless_path = path
return rootless_path
def _get_published_path(self, instance):
"""Calculates expected `publish` folder"""
# determine published path from Anatomy.
template_data = instance.data.get("anatomyData")
rep = instance.data["representations"][0]
template_data["representation"] = rep.get("name")
template_data["ext"] = rep.get("ext")
template_data["comment"] = None
anatomy = instance.context.data["anatomy"]
template_data["root"] = anatomy.roots
template = anatomy.get_template_item("publish", "default", "path")
template_filled = template.format_strict(template_data)
return os.path.normpath(template_filled)
def _convert_resources(self, output_def, transfers):
"""Converts all resource files to configured format."""
out_extension = output_def["ext"]
if not out_extension:
self.log.warning("No output extension configured in "
"ayon+settings://traypublisher/publish/ExtractEditorialPckgConversion") # noqa
return transfers
final_transfers = []
out_def_ffmpeg_args = output_def["ffmpeg_args"]
ffmpeg_input_args = [
value.strip()
for value in out_def_ffmpeg_args["input"]
if value.strip()
]
ffmpeg_video_filters = [
value.strip()
for value in out_def_ffmpeg_args["video_filters"]
if value.strip()
]
ffmpeg_audio_filters = [
value.strip()
for value in out_def_ffmpeg_args["audio_filters"]
if value.strip()
]
ffmpeg_output_args = [
value.strip()
for value in out_def_ffmpeg_args["output"]
if value.strip()
]
ffmpeg_input_args = self._split_ffmpeg_args(ffmpeg_input_args)
generic_args = [
subprocess.list2cmdline(get_ffmpeg_tool_args("ffmpeg"))
]
generic_args.extend(ffmpeg_input_args)
if ffmpeg_video_filters:
generic_args.append("-filter:v")
generic_args.append(
"\"{}\"".format(",".join(ffmpeg_video_filters)))
if ffmpeg_audio_filters:
generic_args.append("-filter:a")
generic_args.append(
"\"{}\"".format(",".join(ffmpeg_audio_filters)))
for source, destination in transfers:
base_name = os.path.basename(destination)
file_name, ext = os.path.splitext(base_name)
dest_path = os.path.join(os.path.dirname(destination),
f"{file_name}.{out_extension}")
final_transfers.append((source, dest_path))
all_args = copy.deepcopy(generic_args)
all_args.append(f"-i \"{source}\"")
all_args.extend(ffmpeg_output_args) # order matters
all_args.append(f"\"{dest_path}\"")
subprcs_cmd = " ".join(all_args)
# run subprocess
self.log.debug("Executing: {}".format(subprcs_cmd))
run_subprocess(subprcs_cmd, shell=True, logger=self.log)
return final_transfers
def _split_ffmpeg_args(self, in_args):
"""Makes sure all entered arguments are separated in individual items.
Split each argument string with " -" to identify if string contains
one or more arguments.
"""
splitted_args = []
for arg in in_args:
sub_args = arg.split(" -")
if len(sub_args) == 1:
if arg and arg not in splitted_args:
splitted_args.append(arg)
continue
for idx, arg in enumerate(sub_args):
if idx != 0:
arg = "-" + arg
if arg and arg not in splitted_args:
splitted_args.append(arg)
return splitted_args

View file

@ -0,0 +1,68 @@
import os
import opentimelineio
import pyblish.api
from ayon_core.pipeline import PublishValidationError
class ValidateEditorialPackage(pyblish.api.InstancePlugin):
"""Checks that published folder contains all resources from otio
Currently checks only by file names and expects flat structure.
It ignores path to resources in otio file as folder might be dragged in and
published from different location than it was created.
"""
label = "Validate Editorial Package"
order = pyblish.api.ValidatorOrder - 0.49
hosts = ["traypublisher"]
families = ["editorial_pckg"]
def process(self, instance):
editorial_pckg_data = instance.data.get("editorial_pckg")
if not editorial_pckg_data:
raise PublishValidationError("Editorial package not collected")
folder_path = editorial_pckg_data["folder_path"]
otio_path = editorial_pckg_data["otio_path"]
if not otio_path:
raise PublishValidationError(
f"Folder {folder_path} missing otio file")
resource_paths = editorial_pckg_data["resource_paths"]
resource_file_names = {os.path.basename(path)
for path in resource_paths}
otio_data = opentimelineio.adapters.read_from_file(otio_path)
target_urls = self._get_all_target_urls(otio_data)
missing_files = set()
for target_url in target_urls:
target_basename = os.path.basename(target_url)
if target_basename not in resource_file_names:
missing_files.add(target_basename)
if missing_files:
raise PublishValidationError(
f"Otio file contains missing files `{missing_files}`.\n\n"
f"Please add them to `{folder_path}` and republish.")
instance.data["editorial_pckg"]["otio_data"] = otio_data
def _get_all_target_urls(self, otio_data):
target_urls = []
# Iterate through tracks, clips, or other elements
for track in otio_data.tracks:
for clip in track:
# Check if the clip has a media reference
if clip.media_reference is not None:
# Access the target_url from the media reference
target_url = clip.media_reference.target_url
if target_url:
target_urls.append(target_url)
return target_urls

View file

@ -85,7 +85,7 @@ class HoudiniSubmitDeadline(
priority = 50
chunk_size = 1
group = ""
@classmethod
def get_attribute_defs(cls):
return [
@ -188,7 +188,7 @@ class HoudiniSubmitDeadline(
job_info.Pool = instance.data.get("primaryPool")
job_info.SecondaryPool = instance.data.get("secondaryPool")
if split_render_job and is_export_job:
job_info.Priority = attribute_values.get(
"export_priority", self.export_priority
@ -309,6 +309,11 @@ class HoudiniSubmitDeadline(
return attr.asdict(plugin_info)
def process(self, instance):
if not instance.data["farm"]:
self.log.debug("Render on farm is disabled. "
"Skipping deadline submission.")
return
super(HoudiniSubmitDeadline, self).process(instance)
# TODO: Avoid the need for this logic here, needed for submit publish

View file

@ -7,7 +7,7 @@ from ayon_core.lib import Logger, run_subprocess, AYONSettingsRegistry
from ayon_core.lib.vendor_bin_utils import find_tool_in_custom_paths
from .rr_job import SubmitFile
from .rr_job import RRjob, SubmitterParameter # noqa F401
from .rr_job import RRJob, SubmitterParameter # noqa F401
class Api:

File diff suppressed because it is too large Load diff

View file

@ -459,36 +459,6 @@ def is_representation_from_latest(representation):
)
def get_template_data_from_session(session=None, settings=None):
"""Template data for template fill from session keys.
Args:
session (Union[Dict[str, str], None]): The Session to use. If not
provided use the currently active global Session.
settings (Optional[Dict[str, Any]]): Prepared studio or project
settings.
Returns:
Dict[str, Any]: All available data from session.
"""
if session is not None:
project_name = session["AYON_PROJECT_NAME"]
folder_path = session["AYON_FOLDER_PATH"]
task_name = session["AYON_TASK_NAME"]
host_name = session["AYON_HOST_NAME"]
else:
context = get_current_context()
project_name = context["project_name"]
folder_path = context["folder_path"]
task_name = context["task_name"]
host_name = get_current_host_name()
return get_template_data_with_names(
project_name, folder_path, task_name, host_name, settings
)
def get_current_context_template_data(settings=None):
"""Prepare template data for current context.

View file

@ -108,69 +108,6 @@ class IntegrateAsset(pyblish.api.InstancePlugin):
label = "Integrate Asset"
order = pyblish.api.IntegratorOrder
families = ["workfile",
"pointcache",
"pointcloud",
"proxyAbc",
"camera",
"animation",
"model",
"maxScene",
"mayaAscii",
"mayaScene",
"setdress",
"layout",
"ass",
"assProxy",
"vdbcache",
"scene",
"vrayproxy",
"vrayscene_layer",
"render",
"prerender",
"imagesequence",
"review",
"rendersetup",
"rig",
"plate",
"look",
"ociolook",
"audio",
"yetiRig",
"yeticache",
"nukenodes",
"gizmo",
"source",
"matchmove",
"image",
"assembly",
"fbx",
"gltf",
"textures",
"action",
"harmony.template",
"harmony.palette",
"editorial",
"background",
"camerarig",
"redshiftproxy",
"effect",
"xgen",
"hda",
"usd",
"staticMesh",
"skeletalMesh",
"mvLook",
"mvUsd",
"mvUsdComposition",
"mvUsdOverride",
"online",
"uasset",
"blendScene",
"yeticacheUE",
"tycache",
"csv_ingest_file",
]
default_template_name = "publish"
@ -360,7 +297,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin):
# Compute the resource file infos once (files belonging to the
# version instance instead of an individual representation) so
# we can re-use those file infos per representation
# we can reuse those file infos per representation
resource_file_infos = self.get_files_info(
resource_destinations, anatomy
)

View file

@ -1,28 +1,31 @@
"""OpenColorIO Wrapper.
Only to be interpreted by Python 3. It is run in subprocess in case
Python 2 hosts needs to use it. Or it is used as module for Python 3
processing.
Providing functionality:
- get_colorspace - console command - python 2
- returning all available color spaces
found in input config path.
- _get_colorspace_data - python 3 - module function
- returning all available colorspaces
found in input config path.
- get_views - console command - python 2
- returning all available viewers
found in input config path.
- _get_views_data - python 3 - module function
- returning all available viewers
found in input config path.
Receive OpenColorIO information and store it in JSON format for processed
that don't have access to OpenColorIO or their version of OpenColorIO is
not compatible.
"""
import click
import json
from pathlib import Path
import PyOpenColorIO as ocio
import click
from ayon_core.pipeline.colorspace import (
has_compatible_ocio_package,
get_display_view_colorspace_name,
get_config_file_rules_colorspace_from_filepath,
get_config_version_data,
get_ocio_config_views,
get_ocio_config_colorspaces,
)
def _save_output_to_json_file(output, output_path):
json_path = Path(output_path)
with open(json_path, "w") as stream:
json.dump(output, stream)
print(f"Data are saved to '{json_path}'")
@click.group()
@ -30,404 +33,185 @@ def main():
pass # noqa: WPS100
@main.group()
def config():
"""Config related commands group
Example of use:
> pyton.exe ./ocio_wrapper.py config <command> *args
"""
pass # noqa: WPS100
@main.group()
def colorspace():
"""Colorspace related commands group
Example of use:
> pyton.exe ./ocio_wrapper.py config <command> *args
"""
pass # noqa: WPS100
@config.command(
name="get_colorspace",
help=(
"return all colorspaces from config file "
"--path input arg is required"
)
)
@click.option("--in_path", required=True,
help="path where to read ocio config file",
type=click.Path(exists=True))
@click.option("--out_path", required=True,
help="path where to write output json file",
type=click.Path())
def get_colorspace(in_path, out_path):
@main.command(
name="get_ocio_config_colorspaces",
help="return all colorspaces from config file")
@click.option(
"--config_path",
required=True,
help="OCIO config path to read ocio config file.",
type=click.Path(exists=True))
@click.option(
"--output_path",
required=True,
help="path where to write output json file",
type=click.Path())
def _get_ocio_config_colorspaces(config_path, output_path):
"""Aggregate all colorspace to file.
Python 2 wrapped console command
Args:
in_path (str): config file path string
out_path (str): temp json file path string
config_path (str): config file path string
output_path (str): temp json file path string
Example of use:
> pyton.exe ./ocio_wrapper.py config get_colorspace
--in_path=<path> --out_path=<path>
--config_path <path> --output_path <path>
"""
json_path = Path(out_path)
out_data = _get_colorspace_data(in_path)
with open(json_path, "w") as f_:
json.dump(out_data, f_)
print(f"Colorspace data are saved to '{json_path}'")
def _get_colorspace_data(config_path):
"""Return all found colorspace data.
Args:
config_path (str): path string leading to config.ocio
Raises:
IOError: Input config does not exist.
Returns:
dict: aggregated available colorspaces
"""
config_path = Path(config_path)
if not config_path.is_file():
raise IOError(
f"Input path `{config_path}` should be `config.ocio` file")
config = ocio.Config().CreateFromFile(str(config_path))
colorspace_data = {
"roles": {},
"colorspaces": {
color.getName(): {
"family": color.getFamily(),
"categories": list(color.getCategories()),
"aliases": list(color.getAliases()),
"equalitygroup": color.getEqualityGroup(),
}
for color in config.getColorSpaces()
},
"displays_views": {
f"{view} ({display})": {
"display": display,
"view": view
}
for display in config.getDisplays()
for view in config.getViews(display)
},
"looks": {}
}
# add looks
looks = config.getLooks()
if looks:
colorspace_data["looks"] = {
look.getName(): {"process_space": look.getProcessSpace()}
for look in looks
}
# add roles
roles = config.getRoles()
if roles:
colorspace_data["roles"] = {
role: {"colorspace": colorspace}
for (role, colorspace) in roles
}
return colorspace_data
@config.command(
name="get_views",
help=(
"return all viewers from config file "
"--path input arg is required"
_save_output_to_json_file(
get_ocio_config_colorspaces(config_path),
output_path
)
)
@click.option("--in_path", required=True,
help="path where to read ocio config file",
type=click.Path(exists=True))
@click.option("--out_path", required=True,
help="path where to write output json file",
type=click.Path())
def get_views(in_path, out_path):
@main.command(
name="get_ocio_config_views",
help="All viewers from config file")
@click.option(
"--config_path",
required=True,
help="OCIO config path to read ocio config file.",
type=click.Path(exists=True))
@click.option(
"--output_path",
required=True,
help="path where to write output json file",
type=click.Path())
def _get_ocio_config_views(config_path, output_path):
"""Aggregate all viewers to file.
Python 2 wrapped console command
Args:
in_path (str): config file path string
out_path (str): temp json file path string
config_path (str): config file path string
output_path (str): temp json file path string
Example of use:
> pyton.exe ./ocio_wrapper.py config get_views \
--in_path=<path> --out_path=<path>
--config_path <path> --output <path>
"""
json_path = Path(out_path)
out_data = _get_views_data(in_path)
with open(json_path, "w") as f_:
json.dump(out_data, f_)
print(f"Viewer data are saved to '{json_path}'")
def _get_views_data(config_path):
"""Return all found viewer data.
Args:
config_path (str): path string leading to config.ocio
Raises:
IOError: Input config does not exist.
Returns:
dict: aggregated available viewers
"""
config_path = Path(config_path)
if not config_path.is_file():
raise IOError("Input path should be `config.ocio` file")
config = ocio.Config().CreateFromFile(str(config_path))
data_ = {}
for display in config.getDisplays():
for view in config.getViews(display):
colorspace = config.getDisplayViewColorSpaceName(display, view)
# Special token. See https://opencolorio.readthedocs.io/en/latest/guides/authoring/authoring.html#shared-views # noqa
if colorspace == "<USE_DISPLAY_NAME>":
colorspace = display
data_[f"{display}/{view}"] = {
"display": display,
"view": view,
"colorspace": colorspace
}
return data_
@config.command(
name="get_version",
help=(
"return major and minor version from config file "
"--config_path input arg is required"
"--out_path input arg is required"
_save_output_to_json_file(
get_ocio_config_views(config_path),
output_path
)
)
@click.option("--config_path", required=True,
help="path where to read ocio config file",
type=click.Path(exists=True))
@click.option("--out_path", required=True,
help="path where to write output json file",
type=click.Path())
def get_version(config_path, out_path):
"""Get version of config.
Python 2 wrapped console command
@main.command(
name="get_config_version_data",
help="Get major and minor version from config file")
@click.option(
"--config_path",
required=True,
help="OCIO config path to read ocio config file.",
type=click.Path(exists=True))
@click.option(
"--output_path",
required=True,
help="path where to write output json file",
type=click.Path())
def _get_config_version_data(config_path, output_path):
"""Get version of config.
Args:
config_path (str): ocio config file path string
out_path (str): temp json file path string
output_path (str): temp json file path string
Example of use:
> pyton.exe ./ocio_wrapper.py config get_version \
--config_path=<path> --out_path=<path>
--config_path <path> --output_path <path>
"""
json_path = Path(out_path)
out_data = _get_version_data(config_path)
with open(json_path, "w") as f_:
json.dump(out_data, f_)
print(f"Config version data are saved to '{json_path}'")
def _get_version_data(config_path):
"""Return major and minor version info.
Args:
config_path (str): path string leading to config.ocio
Raises:
IOError: Input config does not exist.
Returns:
dict: minor and major keys with values
"""
config_path = Path(config_path)
if not config_path.is_file():
raise IOError("Input path should be `config.ocio` file")
config = ocio.Config().CreateFromFile(str(config_path))
return {
"major": config.getMajorVersion(),
"minor": config.getMinorVersion()
}
@colorspace.command(
name="get_config_file_rules_colorspace_from_filepath",
help=(
"return colorspace from filepath "
"--config_path - ocio config file path (input arg is required) "
"--filepath - any file path (input arg is required) "
"--out_path - temp json file path (input arg is required)"
_save_output_to_json_file(
get_config_version_data(config_path),
output_path
)
)
@click.option("--config_path", required=True,
help="path where to read ocio config file",
type=click.Path(exists=True))
@click.option("--filepath", required=True,
help="path to file to get colorspace from",
type=click.Path())
@click.option("--out_path", required=True,
help="path where to write output json file",
type=click.Path())
def get_config_file_rules_colorspace_from_filepath(
config_path, filepath, out_path
@main.command(
name="get_config_file_rules_colorspace_from_filepath",
help="Colorspace file rules from filepath")
@click.option(
"--config_path",
required=True,
help="OCIO config path to read ocio config file.",
type=click.Path(exists=True))
@click.option(
"--filepath",
required=True,
help="Path to file to get colorspace from.",
type=click.Path())
@click.option(
"--output_path",
required=True,
help="Path where to write output json file.",
type=click.Path())
def _get_config_file_rules_colorspace_from_filepath(
config_path, filepath, output_path
):
"""Get colorspace from file path wrapper.
Python 2 wrapped console command
Args:
config_path (str): config file path string
filepath (str): path string leading to file
out_path (str): temp json file path string
output_path (str): temp json file path string
Example of use:
> pyton.exe ./ocio_wrapper.py \
> python.exe ./ocio_wrapper.py \
colorspace get_config_file_rules_colorspace_from_filepath \
--config_path=<path> --filepath=<path> --out_path=<path>
--config_path <path> --filepath <path> --output_path <path>
"""
json_path = Path(out_path)
colorspace = _get_config_file_rules_colorspace_from_filepath(
config_path, filepath)
with open(json_path, "w") as f_:
json.dump(colorspace, f_)
print(f"Colorspace name is saved to '{json_path}'")
_save_output_to_json_file(
get_config_file_rules_colorspace_from_filepath(config_path, filepath),
output_path
)
def _get_config_file_rules_colorspace_from_filepath(config_path, filepath):
"""Return found colorspace data found in v2 file rules.
Args:
config_path (str): path string leading to config.ocio
filepath (str): path string leading to v2 file rules
Raises:
IOError: Input config does not exist.
Returns:
dict: aggregated available colorspaces
"""
config_path = Path(config_path)
if not config_path.is_file():
raise IOError(
f"Input path `{config_path}` should be `config.ocio` file")
config = ocio.Config().CreateFromFile(str(config_path))
# TODO: use `parseColorSpaceFromString` instead if ocio v1
colorspace = config.getColorSpaceFromFilepath(str(filepath))
return colorspace
def _get_display_view_colorspace_name(config_path, display, view):
"""Returns the colorspace attribute of the (display, view) pair.
Args:
config_path (str): path string leading to config.ocio
display (str): display name e.g. "ACES"
view (str): view name e.g. "sRGB"
Raises:
IOError: Input config does not exist.
Returns:
view color space name (str) e.g. "Output - sRGB"
"""
config_path = Path(config_path)
if not config_path.is_file():
raise IOError("Input path should be `config.ocio` file")
config = ocio.Config.CreateFromFile(str(config_path))
colorspace = config.getDisplayViewColorSpaceName(display, view)
return colorspace
@config.command(
@main.command(
name="get_display_view_colorspace_name",
help=(
"return default view colorspace name "
"for the given display and view "
"--path input arg is required"
)
)
@click.option("--in_path", required=True,
help="path where to read ocio config file",
type=click.Path(exists=True))
@click.option("--out_path", required=True,
help="path where to write output json file",
type=click.Path())
@click.option("--display", required=True,
help="display name",
type=click.STRING)
@click.option("--view", required=True,
help="view name",
type=click.STRING)
def get_display_view_colorspace_name(in_path, out_path,
display, view):
"Default view colorspace name for the given display and view"
))
@click.option(
"--config_path",
required=True,
help="path where to read ocio config file",
type=click.Path(exists=True))
@click.option(
"--display",
required=True,
help="Display name",
type=click.STRING)
@click.option(
"--view",
required=True,
help="view name",
type=click.STRING)
@click.option(
"--output_path",
required=True,
help="path where to write output json file",
type=click.Path())
def _get_display_view_colorspace_name(
config_path, display, view, output_path
):
"""Aggregate view colorspace name to file.
Wrapper command for processes without access to OpenColorIO
Args:
in_path (str): config file path string
out_path (str): temp json file path string
config_path (str): config file path string
output_path (str): temp json file path string
display (str): display name e.g. "ACES"
view (str): view name e.g. "sRGB"
Example of use:
> pyton.exe ./ocio_wrapper.py config \
get_display_view_colorspace_name --in_path=<path> \
--out_path=<path> --display=<display> --view=<view>
get_display_view_colorspace_name --config_path <path> \
--output_path <path> --display <display> --view <view>
"""
_save_output_to_json_file(
get_display_view_colorspace_name(config_path, display, view),
output_path
)
out_data = _get_display_view_colorspace_name(in_path,
display,
view)
with open(out_path, "w") as f:
json.dump(out_data, f)
print(f"Display view colorspace saved to '{out_path}'")
if __name__ == '__main__':
if __name__ == "__main__":
if not has_compatible_ocio_package():
raise RuntimeError("OpenColorIO is not available.")
main()

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring AYON core addon version."""
__version__ = "0.3.1-dev.1"
__version__ = "0.3.2-dev.1"

View file

@ -16,7 +16,7 @@ aiohttp_json_rpc = "*" # TVPaint server
aiohttp-middlewares = "^2.0.0"
wsrpc_aiohttp = "^3.1.1" # websocket server
Click = "^8"
OpenTimelineIO = "0.14.1"
OpenTimelineIO = "0.16.0"
opencolorio = "2.2.1"
Pillow = "9.5.0"
pynput = "^1.7.2" # Timers manager - TODO remove

View file

@ -1,11 +1,12 @@
name = "core"
title = "Core"
version = "0.3.1-dev.1"
version = "0.3.2-dev.1"
client_dir = "ayon_core"
plugin_for = ["ayon_server"]
requires = [
"~ayon_server-1.0.3+<2.0.0",
]
ayon_server_version = ">=1.0.3,<2.0.0"
ayon_launcher_version = ">=1.0.2"
ayon_required_addons = {}
ayon_compatible_addons = {}

View file

@ -1,3 +1,5 @@
from typing import Any
from ayon_server.addons import BaseServerAddon
from .settings import CoreSettings, DEFAULT_VALUES
@ -9,3 +11,53 @@ class CoreAddon(BaseServerAddon):
async def get_default_settings(self):
settings_model_cls = self.get_settings_model()
return settings_model_cls(**DEFAULT_VALUES)
async def convert_settings_overrides(
self,
source_version: str,
overrides: dict[str, Any],
) -> dict[str, Any]:
self._convert_imagio_configs_0_3_1(overrides)
# Use super conversion
return await super().convert_settings_overrides(
source_version, overrides
)
def _convert_imagio_configs_0_3_1(self, overrides):
"""Imageio config settings did change to profiles since 0.3.1. ."""
imageio_overrides = overrides.get("imageio") or {}
if (
"ocio_config" not in imageio_overrides
or "filepath" not in imageio_overrides["ocio_config"]
):
return
ocio_config = imageio_overrides.pop("ocio_config")
filepath = ocio_config["filepath"]
if not filepath:
return
first_filepath = filepath[0]
ocio_config_profiles = imageio_overrides.setdefault(
"ocio_config_profiles", []
)
base_value = {
"type": "builtin_path",
"product_name": "",
"host_names": [],
"task_names": [],
"task_types": [],
"custom_path": "",
"builtin_path": "{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio"
}
if first_filepath in (
"{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio",
"{BUILTIN_OCIO_ROOT}/nuke-default/config.ocio",
):
base_value["type"] = "builtin_path"
base_value["builtin_path"] = first_filepath
else:
base_value["type"] = "custom_path"
base_value["custom_path"] = first_filepath
ocio_config_profiles.append(base_value)

View file

@ -54,9 +54,67 @@ class CoreImageIOFileRulesModel(BaseSettingsModel):
return value
class CoreImageIOConfigModel(BaseSettingsModel):
filepath: list[str] = SettingsField(
default_factory=list, title="Config path"
def _ocio_config_profile_types():
return [
{"value": "builtin_path", "label": "AYON built-in OCIO config"},
{"value": "custom_path", "label": "Path to OCIO config"},
{"value": "product_name", "label": "Published product"},
]
def _ocio_built_in_paths():
return [
{
"value": "{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio",
"label": "ACES 1.2",
"description": "Aces 1.2 OCIO config file."
},
{
"value": "{BUILTIN_OCIO_ROOT}/nuke-default/config.ocio",
"label": "Nuke default",
},
]
class CoreImageIOConfigProfilesModel(BaseSettingsModel):
_layout = "expanded"
host_names: list[str] = SettingsField(
default_factory=list,
title="Host names"
)
task_types: list[str] = SettingsField(
default_factory=list,
title="Task types",
enum_resolver=task_types_enum
)
task_names: list[str] = SettingsField(
default_factory=list,
title="Task names"
)
type: str = SettingsField(
title="Profile type",
enum_resolver=_ocio_config_profile_types,
conditionalEnum=True,
default="builtin_path",
section="---",
)
builtin_path: str = SettingsField(
"ACES 1.2",
title="Built-in OCIO config",
enum_resolver=_ocio_built_in_paths,
)
custom_path: str = SettingsField(
"",
title="OCIO config path",
description="Path to OCIO config. Anatomy formatting is supported.",
)
product_name: str = SettingsField(
"",
title="Product name",
description=(
"Published product name to get OCIO config from. "
"Partial match is supported."
),
)
@ -65,9 +123,8 @@ class CoreImageIOBaseModel(BaseSettingsModel):
False,
title="Enable Color Management"
)
ocio_config: CoreImageIOConfigModel = SettingsField(
default_factory=CoreImageIOConfigModel,
title="OCIO config"
ocio_config_profiles: list[CoreImageIOConfigProfilesModel] = SettingsField(
default_factory=list, title="OCIO config profiles"
)
file_rules: CoreImageIOFileRulesModel = SettingsField(
default_factory=CoreImageIOFileRulesModel,
@ -186,12 +243,17 @@ class CoreSettings(BaseSettingsModel):
DEFAULT_VALUES = {
"imageio": {
"activate_global_color_management": False,
"ocio_config": {
"filepath": [
"{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio",
"{BUILTIN_OCIO_ROOT}/nuke-default/config.ocio"
]
},
"ocio_config_profiles": [
{
"host_names": [],
"task_types": [],
"task_names": [],
"type": "builtin_path",
"builtin_path": "{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio",
"custom_path": "",
"product_name": "",
}
],
"file_rules": {
"activate_global_file_rules": False,
"rules": [
@ -199,42 +261,57 @@ DEFAULT_VALUES = {
"name": "example",
"pattern": ".*(beauty).*",
"colorspace": "ACES - ACEScg",
"ext": "exr"
"ext": "exr",
}
]
}
],
},
},
"studio_name": "",
"studio_code": "",
"environments": "{\n\"STUDIO_SW\": {\n \"darwin\": \"/mnt/REPO_SW\",\n \"linux\": \"/mnt/REPO_SW\",\n \"windows\": \"P:/REPO_SW\"\n }\n}",
"environments": json.dumps(
{
"STUDIO_SW": {
"darwin": "/mnt/REPO_SW",
"linux": "/mnt/REPO_SW",
"windows": "P:/REPO_SW"
}
},
indent=4
),
"tools": DEFAULT_TOOLS_VALUES,
"version_start_category": {
"profiles": []
},
"publish": DEFAULT_PUBLISH_VALUES,
"project_folder_structure": json.dumps({
"__project_root__": {
"prod": {},
"resources": {
"footage": {
"plates": {},
"offline": {}
"project_folder_structure": json.dumps(
{
"__project_root__": {
"prod": {},
"resources": {
"footage": {
"plates": {},
"offline": {}
},
"audio": {},
"art_dept": {}
},
"audio": {},
"art_dept": {}
},
"editorial": {},
"assets": {
"characters": {},
"locations": {}
},
"shots": {}
}
}, indent=4),
"editorial": {},
"assets": {
"characters": {},
"locations": {}
},
"shots": {}
}
},
indent=4
),
"project_plugins": {
"windows": [],
"darwin": [],
"linux": []
},
"project_environments": "{}"
"project_environments": json.dumps(
{},
indent=4
)
}

View file

@ -1,3 +1,3 @@
name = "houdini"
title = "Houdini"
version = "0.2.13"
version = "0.2.14"

View file

@ -1,4 +1,7 @@
from ayon_server.settings import BaseSettingsModel, SettingsField
from ayon_server.settings import (
BaseSettingsModel,
SettingsField
)
# Publish Plugins
@ -20,6 +23,27 @@ class CollectChunkSizeModel(BaseSettingsModel):
title="Frames Per Task")
class AOVFilterSubmodel(BaseSettingsModel):
"""You should use the same host name you are using for Houdini."""
host_name: str = SettingsField("", title="Houdini Host name")
value: list[str] = SettingsField(
default_factory=list,
title="AOV regex"
)
class CollectLocalRenderInstancesModel(BaseSettingsModel):
use_deadline_aov_filter: bool = SettingsField(
False,
title="Use Deadline AOV Filter"
)
aov_filter: AOVFilterSubmodel = SettingsField(
default_factory=AOVFilterSubmodel,
title="Reviewable products filter"
)
class ValidateWorkfilePathsModel(BaseSettingsModel):
enabled: bool = SettingsField(title="Enabled")
optional: bool = SettingsField(title="Optional")
@ -49,6 +73,10 @@ class PublishPluginsModel(BaseSettingsModel):
default_factory=CollectChunkSizeModel,
title="Collect Chunk Size."
)
CollectLocalRenderInstances: CollectLocalRenderInstancesModel = SettingsField(
default_factory=CollectLocalRenderInstancesModel,
title="Collect Local Render Instances."
)
ValidateContainers: BasicValidateModel = SettingsField(
default_factory=BasicValidateModel,
title="Validate Latest Containers.",
@ -82,6 +110,15 @@ DEFAULT_HOUDINI_PUBLISH_SETTINGS = {
"optional": True,
"chunk_size": 999999
},
"CollectLocalRenderInstances": {
"use_deadline_aov_filter": False,
"aov_filter" : {
"host_name": "houdini",
"value": [
".*([Bb]eauty).*"
]
}
},
"ValidateContainers": {
"enabled": True,
"optional": True,

View file

@ -1,3 +1,3 @@
name = "traypublisher"
title = "TrayPublisher"
version = "0.1.4"
version = "0.1.5"

View file

@ -1,4 +1,7 @@
from ayon_server.settings import BaseSettingsModel, SettingsField
from ayon_server.settings import (
BaseSettingsModel,
SettingsField,
)
class ValidatePluginModel(BaseSettingsModel):
@ -14,6 +17,45 @@ class ValidateFrameRangeModel(ValidatePluginModel):
'my_asset_to_publish.mov')"""
class ExtractEditorialPckgFFmpegModel(BaseSettingsModel):
video_filters: list[str] = SettingsField(
default_factory=list,
title="Video filters"
)
audio_filters: list[str] = SettingsField(
default_factory=list,
title="Audio filters"
)
input: list[str] = SettingsField(
default_factory=list,
title="Input arguments"
)
output: list[str] = SettingsField(
default_factory=list,
title="Output arguments"
)
class ExtractEditorialPckgOutputDefModel(BaseSettingsModel):
_layout = "expanded"
ext: str = SettingsField("", title="Output extension")
ffmpeg_args: ExtractEditorialPckgFFmpegModel = SettingsField(
default_factory=ExtractEditorialPckgFFmpegModel,
title="FFmpeg arguments"
)
class ExtractEditorialPckgConversionModel(BaseSettingsModel):
"""Set output definition if resource files should be converted."""
conversion_enabled: bool = SettingsField(True,
title="Conversion enabled")
output: ExtractEditorialPckgOutputDefModel = SettingsField(
default_factory=ExtractEditorialPckgOutputDefModel,
title="Output Definitions",
)
class TrayPublisherPublishPlugins(BaseSettingsModel):
CollectFrameDataFromAssetEntity: ValidatePluginModel = SettingsField(
default_factory=ValidatePluginModel,
@ -28,6 +70,13 @@ class TrayPublisherPublishPlugins(BaseSettingsModel):
default_factory=ValidatePluginModel,
)
ExtractEditorialPckgConversion: ExtractEditorialPckgConversionModel = (
SettingsField(
default_factory=ExtractEditorialPckgConversionModel,
title="Extract Editorial Package Conversion"
)
)
DEFAULT_PUBLISH_PLUGINS = {
"CollectFrameDataFromAssetEntity": {
@ -44,5 +93,24 @@ DEFAULT_PUBLISH_PLUGINS = {
"enabled": True,
"optional": True,
"active": True
},
"ExtractEditorialPckgConversion": {
"optional": False,
"conversion_enabled": True,
"output": {
"ext": "",
"ffmpeg_args": {
"video_filters": [],
"audio_filters": [],
"input": [
"-apply_trc gamma22"
],
"output": [
"-pix_fmt yuv420p",
"-crf 18",
"-intra"
]
}
}
}
}