Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use product name templates for render products on farm publish #447

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
2d403b3
:fire: remove interface file
antirotor Apr 24, 2024
43f2fed
:art: collect rop name
antirotor Apr 24, 2024
cd2d5f4
:art: add aov and renderlayer to anatomy data
antirotor Apr 24, 2024
cfe8f08
:recycle: use StringTemplate for template formatting in product names
antirotor Apr 24, 2024
8fa1317
:recycle: use product name templates for render instances
antirotor Apr 24, 2024
bf46634
Merge branch 'develop' into enhancement/render-product-names-templated
antirotor Apr 30, 2024
ad61ced
Merge branch 'develop' into enhancement/render-product-names-templated
MustafaJafar May 22, 2024
1ae71bb
Update client/ayon_core/pipeline/farm/pyblish_functions.py
antirotor May 22, 2024
6541dbe
Update client/ayon_core/pipeline/farm/pyblish_functions.py
antirotor May 22, 2024
f7aada3
Update client/ayon_core/pipeline/farm/pyblish_functions.py
antirotor May 22, 2024
b37b0ec
Merge remote-tracking branch 'origin/develop' into enhancement/render…
antirotor May 22, 2024
eb83310
Merge branch 'enhancement/render-product-names-templated' of github.c…
antirotor May 22, 2024
61147d2
:recycle: remove debug print
antirotor May 22, 2024
19c7c04
:art: pass render layer to representation context
antirotor May 22, 2024
0bcf3d3
Merge branch 'develop' into enhancement/render-product-names-templated
MustafaJafar May 22, 2024
9012e52
add renderlayer and aov_name to dynamic_data of get_product_name
MustafaJafar May 22, 2024
cba100a
add renderlayer and aov keys to created runtime render instances
MustafaJafar May 22, 2024
62870f2
add TODO about product_group value
MustafaJafar May 22, 2024
871f6a7
Merge pull request #543 from ynput/enhancement/render-product-names-t…
MustafaJafar Jun 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ class CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin):
def process(self, instance):

rop = hou.node(instance.data.get("instance_node"))
# to align with maya render layers
instance.data["renderlayer"] = rop.name()
Comment on lines +36 to +37
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, what? Why is the ROP node's name used as the renderlayer? That seems odd?

Isn't it much easier if a studio wants this for them to us $OS as part of the productName or subset attributes on the node itself so that it's editable by the studio, requires no additional changes?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but then you can't use it in anatomy data. One thing is product name, the other is to use it somewhere else in path. I agree that render layer is somewhat arbitrary name in Houdini context, but most of the DCCs are using render layer is one way or the other and I wouldn't spam the anatomy data with every DCC specific term.


# Collect chunkSize
chunk_size_parm = rop.parm("chunkSize")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin):
def process(self, instance):

rop = hou.node(instance.data.get("instance_node"))
# to align with maya render layers
instance.data["renderlayer"] = rop.name()

# Collect chunkSize
chunk_size_parm = rop.parm("chunkSize")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin):

def process(self, instance):
rop = hou.node(instance.data.get("instance_node"))
# to align with maya render layers
instance.data["renderlayer"] = rop.name()
"""
print("-" * 20)
print(f"project_name: {instance.context.data['projectName']}")
print(f"task_name: {instance.data['taskEntity']['name']}")
print(f"task_type: {instance.data['taskEntity']['type']}")
print(f"product_type: {instance.data['productType']}")
print(f"variant: {instance.data.get('variant')}")
print("-" * 20)
"""

# Collect chunkSize
chunk_size_parm = rop.parm("chunkSize")
Expand Down Expand Up @@ -63,7 +74,7 @@ def process(self, instance):
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 = ""

# Default beauty/main layer AOV
Expand All @@ -75,7 +86,7 @@ def process(self, instance):
beauty_suffix: self.generate_expected_files(instance,
beauty_product)
}

aovs_rop = rop.parm("RS_aovGetFromNode").evalAsNode()
if aovs_rop:
rop = aovs_rop
Expand All @@ -98,7 +109,7 @@ def process(self, instance):

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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin):
def process(self, instance):

rop = hou.node(instance.data.get("instance_node"))
# to align with maya render layers
instance.data["renderlayer"] = rop.name()

# Collect chunkSize
chunk_size_parm = rop.parm("chunkSize")
Expand Down
9 changes: 7 additions & 2 deletions client/ayon_core/pipeline/create/product_name.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from ayon_core.lib import StringTemplate, filter_profiles, prepare_template_data
from ayon_core.settings import get_project_settings
from ayon_core.lib import filter_profiles, prepare_template_data

from .constants import DEFAULT_PRODUCT_TEMPLATE

Expand Down Expand Up @@ -138,6 +138,7 @@ def get_product_name(
default_template=default_template,
project_settings=project_settings
)

# Simple check of task name existence for template with {task} in
# - missing task should be possible only in Standalone publisher
if not task_name and "{task" in template.lower():
Expand All @@ -164,7 +165,11 @@ def get_product_name(
fill_pairs[key] = value

try:
return template.format(**prepare_template_data(fill_pairs))
return StringTemplate.format_template(
template=template,
data=prepare_template_data(fill_pairs)
)
# return template.format(**prepare_template_data(fill_pairs))
except KeyError as exp:
raise TemplateFillError(
"Value for {} key is missing in template '{}'."
Expand Down
151 changes: 88 additions & 63 deletions client/ayon_core/pipeline/farm/pyblish_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
get_current_project_name,
get_representation_path,
)
from ayon_core.pipeline.create import get_product_name

from ayon_core.lib import Logger
from ayon_core.pipeline.publish import KnownPublishError
from ayon_core.pipeline.farm.patterning import match_aov_pattern
Expand Down Expand Up @@ -464,7 +466,9 @@ def create_instances_for_aov(instance, skeleton, aov_filter,
Args:
instance (pyblish.api.Instance): Original instance.
skeleton (dict): Skeleton instance data.
aov_filter (dict): AOV filter.
skip_integration_repre_list (list): skip
do_not_add_review (bool): explicitly disable review

Returns:
list of pyblish.api.Instance: Instances created from
Expand Down Expand Up @@ -526,10 +530,10 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data,
instance (pyblish.api.Instance): Original instance.
skeleton (dict): Skeleton data for instance (those needed) later
by collector.
additional_data (dict): ..
additional_data (dict): ...
skip_integration_repre_list (list): list of extensions that shouldn't
be published
do_not_addbe _review (bool): explicitly disable review
do_not_add_review (bool): explicitly disable review


Returns:
Expand All @@ -539,68 +543,60 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data,
ValueError:

"""
# TODO: this needs to be taking the task from context or instance
task = os.environ["AYON_TASK_NAME"]
task_name = instance.data['taskEntity']['name']
antirotor marked this conversation as resolved.
Show resolved Hide resolved

anatomy = instance.context.data["anatomy"]
s_product_name = skeleton["productName"]
src_product_name = skeleton["productName"]
cameras = instance.data.get("cameras", [])
exp_files = instance.data["expectedFiles"]
expected_files = instance.data["expectedFiles"]
log = Logger.get_logger("farm_publishing")

instances = []
# go through AOVs in expected files
for aov, files in exp_files[0].items():
cols, rem = clique.assemble(files)
# we shouldn't have any reminders. And if we do, it should
# be just one item for single frame renders.
if not cols and rem:
if len(rem) != 1:
raise ValueError("Found multiple non related files "
"to render, don't know what to do "
"with them.")
col = rem[0]
ext = os.path.splitext(col)[1].lstrip(".")
else:
# but we really expect only one collection.
# Nothing else make sense.
if len(cols) != 1:
raise ValueError("Only one image sequence type is expected.") # noqa: E501
ext = cols[0].tail.lstrip(".")
col = list(cols[0])
for aov, files in expected_files[0].items():
collected_files = _collect_expected_files_for_aov(files)

# get file path (use first from the list or single frame)
expected_filepath = collected_files[0] if isinstance(collected_files, (list, tuple)) else collected_files

dynamic_data = {
"aov": aov,
"renderlayer": instance.data.get("renderlayer"),
}

# find if camera is used in the file path
camera = [cam for cam in cameras if cam in expected_filepath]

# Is there just one camera matching?
# TODO: this is not true, we can have multiple cameras in the scene
# and we should be able to detect them all. Currently, we are
# keeping the old behavior, taking the first one found.
if camera:
dynamic_data["camera"] = camera[0]

product_name = get_product_name(
project_name=instance.context.data["projectName"],
task_name=task_name,
task_type=instance.data['taskEntity']['taskType'],
antirotor marked this conversation as resolved.
Show resolved Hide resolved
host_name=instance.context.data["hostName"],
product_type=skeleton['productType'],
antirotor marked this conversation as resolved.
Show resolved Hide resolved
dynamic_data=dynamic_data,
variant=instance.data.get('variant', ''),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really want to fall back to an empty variant?

project_settings=instance.context.data.get("project_settings"),
)

# create product name `<product type><Task><Product name>`
# TODO refactor/remove me
product_type = skeleton["productType"]
if not s_product_name.startswith(product_type):
# Group name isn't based on a product name template as it is
# difficult to differentiate in the custom defined template
# what part is the least common denominator.
if not src_product_name.startswith(skeleton["productType"]):
group_name = '{}{}{}{}{}'.format(
product_type,
task[0].upper(), task[1:],
s_product_name[0].upper(), s_product_name[1:])
skeleton["productType"],
task_name[0].upper(), task_name[1:],
src_product_name[0].upper(), src_product_name[1:])
else:
group_name = s_product_name

# if there are multiple cameras, we need to add camera name
expected_filepath = col[0] if isinstance(col, (list, tuple)) else col
cams = [cam for cam in cameras if cam in expected_filepath]
if cams:
for cam in cams:
if not aov:
product_name = '{}_{}'.format(group_name, cam)
elif not aov.startswith(cam):
product_name = '{}_{}_{}'.format(group_name, cam, aov)
else:
product_name = "{}_{}".format(group_name, aov)
else:
if aov:
product_name = '{}_{}'.format(group_name, aov)
else:
product_name = '{}'.format(group_name)
group_name = src_product_name

if isinstance(col, (list, tuple)):
staging = os.path.dirname(col[0])
else:
staging = os.path.dirname(col)
staging = os.path.dirname(expected_filepath)

try:
staging = remap_source(staging, anatomy)
Expand All @@ -611,20 +607,18 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data,

app = os.environ.get("AYON_HOST_NAME", "")

if isinstance(col, list):
render_file_name = os.path.basename(col[0])
else:
render_file_name = os.path.basename(col)
aov_patterns = aov_filter
render_file_name = os.path.basename(expected_filepath)

aov_patterns = aov_filter
preview = match_aov_pattern(app, aov_patterns, render_file_name)

new_instance = deepcopy(skeleton)
new_instance["productName"] = product_name
new_instance["productGroup"] = group_name
new_instance["aov"] = aov

# toggle preview on if multipart is on
# Because we cant query the multipartExr data member of each AOV we'll
# Because we can't query the multipartExr data member of each AOV we'll
# need to have hardcoded rule of excluding any renders with
# "cryptomatte" in the file name from being a multipart EXR. This issue
# happens with Redshift that forces Cryptomatte renders to be separate
Expand All @@ -650,10 +644,9 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data,
new_instance["review"] = True

# create representation
if isinstance(col, (list, tuple)):
files = [os.path.basename(f) for f in col]
else:
files = os.path.basename(col)
ext = os.path.splitext(
os.path.basename(expected_filepath)
)[1].lstrip(".")

# Copy render product "colorspace" data to representation.
colorspace = ""
Expand Down Expand Up @@ -708,6 +701,36 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data,
return instances


def _collect_expected_files_for_aov(files):
"""Collect expected files.

Args:
files (list): List of files.

Returns:
list or str: Collection of files or single file.

Raises:
ValueError: If there are multiple collections.

"""
cols, rem = clique.assemble(files)
# we shouldn't have any reminders. And if we do, it should
# be just one item for single frame renders.
if not cols and rem:
if len(rem) != 1:
raise ValueError("Found multiple non related files "
"to render, don't know what to do "
"with them.")
return rem[0]
else:
# but we really expect only one collection.
# Nothing else make sense.
if len(cols) != 1:
raise ValueError("Only one image sequence type is expected.") # noqa: E501
return list(cols[0])


def get_resources(project_name, version_entity, extension=None):
"""Get the files from the specific version.

Expand Down Expand Up @@ -837,6 +860,8 @@ def create_skeleton_instance_cache(instance):
# map inputVersions `ObjectId` -> `str` so json supports it
"inputVersions": list(map(str, data.get("inputVersions", []))),
}
if instance.data.get("renderlayer"):
instance_skeleton_data["renderlayer"] = instance.data["renderlayer"]

# skip locking version if we are creating v01
instance_version = data.get("version") # take this if exists
Expand Down
24 changes: 0 additions & 24 deletions client/ayon_core/pipeline/farm/pyblish_functions.pyi

This file was deleted.

17 changes: 14 additions & 3 deletions client/ayon_core/plugins/publish/collect_anatomy_instance_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ def fill_missing_task_entities(self, context, project_name):
folder_path_by_id = {}
for instance in context:
folder_entity = instance.data.get("folderEntity")
# Skip if instnace does not have filled folder entity
# Skip if instance does not have filled folder entity
if not folder_entity:
continue
folder_id = folder_entity["id"]
Expand Down Expand Up @@ -352,6 +352,17 @@ def fill_anatomy_data(self, context):
if resolution_height:
anatomy_data["resolution_height"] = resolution_height

# make render layer name (or ROP name) available in the
# templates
render_layer = instance.data.get("renderlayer")
if render_layer:
anatomy_data["renderlayer"] = render_layer

# make AOV name if present available for templates
aov = instance.data.get("aov")
if aov:
anatomy_data["aov"] = aov
Comment on lines +356 to +365
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's "renderlayer" if not part of the product name already? This seems like an odd decision to allow renderlayer (which as far as I know doesn't really have a valid connotation in Houdini?) to be part of the anatomy template? Or?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you might want to have render layer as a part of the path (not the whole product name).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you might want to have render layer as a part of the path (not the whole product name).

When doing so - what would you anticipate the product name be instead?

Coming back to this from your PR description:

This was bypassing any template set in ayon+settings://core/tools/creator/product_name_profiles. With this change, one can set template like this:

{product[type]}<{Renderlayer}><_{Aov}>

resulting in:

renderMainlayer_normal

It is up to TD to set correctly such template to make sure, product name will be stable enough. As a side-effect, optional keys can be used now in product name templates adding flexibility (and perhaps some danger of misuse).

renderlayer is now set on instance along with aov and is collected to anatomy data, so it can be used in template formatting so you can use template for renders like:

In practice a user should usually never see this. They see the product name or representation in the loader or the manager. Which raises the question - how come its this crucial in the published filepath which usually isn't something that is displayed to the artists anyway?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is just an option to categorize things when you need to have access to them outside loaders/pipeline. We get those requests from time to time when you have part that is not integrated and you need to access the files directly. Then dropping the whole render layer folder to DCC with all of it's AOV sequences can speed things up.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally agree that's valuable - but doesn't that mean we should just expose these as data on the representations that can be picked up by the Delivery action instead? Because I believe this currently requires the template to actually use {aov} in the published filepaths just so one can use that also in the delivery template since I think it stores only the used context on publish, right?

If not and this is solely for "delivery" templates I think it could be nice.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean to "force" it to context on representation even without it being used in the template so it can be picked up later on by delivery action? Like we are storing udim here

# Explicitly store the full list even though template data might
# have a different value because it uses just a single udim tile
if repre.get("udim"):
repre_context["udim"] = repre.get("udim") # store list
(and also frame somewhere)

Copy link
Collaborator

@BigRoy BigRoy May 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, similar to that. Not sure if that's "good design" - but that was indeed what I meant.

I'd expect this to way more valuable for delivery templates than production templates since in production templates it may be likely you will have duplicity anyway since usually the AOV is part of the product name as well.


pixel_aspect = instance.data.get("pixelAspect")
if pixel_aspect:
anatomy_data["pixel_aspect"] = float(
Expand All @@ -378,7 +389,7 @@ def fill_anatomy_data(self, context):
))

def _fill_folder_data(self, instance, project_entity, anatomy_data):
# QUESTION should we make sure that all folder data are poped if
# QUESTION should we make sure that all folder data are popped if
# folder data cannot be found?
# - 'folder', 'hierarchy', 'parent', 'folder'
folder_entity = instance.data.get("folderEntity")
Expand Down Expand Up @@ -422,7 +433,7 @@ def _fill_folder_data(self, instance, project_entity, anatomy_data):
})

def _fill_task_data(self, instance, task_types_by_name, anatomy_data):
# QUESTION should we make sure that all task data are poped if task
# QUESTION should we make sure that all task data are popped if task
# data cannot be resolved?
# - 'task'

Expand Down
Loading