Merge pull request #2065 from pypeclub/bugfix/maya-fix-camera-token

Maya: Fix multi-camera renders
This commit is contained in:
Ondřej Samohel 2021-10-06 17:15:02 +02:00 committed by GitHub
commit cf6eacaa58
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 194 additions and 75 deletions

View file

@ -114,6 +114,8 @@ class RenderProduct(object):
aov = attr.ib(default=None) # source aov
driver = attr.ib(default=None) # source driver
multipart = attr.ib(default=False) # multichannel file
camera = attr.ib(default=None) # used only when rendering
# from multiple cameras
def get(layer, render_instance=None):
@ -183,6 +185,16 @@ class ARenderProducts:
self.layer_data = self._get_layer_data()
self.layer_data.products = self.get_render_products()
def has_camera_token(self):
# type: () -> bool
"""Check if camera token is in image prefix.
Returns:
bool: True/False if camera token is present.
"""
return "<camera>" in self.layer_data.filePrefix.lower()
@abstractmethod
def get_render_products(self):
"""To be implemented by renderer class.
@ -307,7 +319,7 @@ class ARenderProducts:
# Deadline allows submitting renders with a custom frame list
# to support those cases we might want to allow 'custom frames'
# to be overridden to `ExpectFiles` class?
layer_data = LayerMetadata(
return LayerMetadata(
frameStart=int(self.get_render_attribute("startFrame")),
frameEnd=int(self.get_render_attribute("endFrame")),
frameStep=int(self.get_render_attribute("byFrameStep")),
@ -321,7 +333,6 @@ class ARenderProducts:
defaultExt=self._get_attr("defaultRenderGlobals.imfPluginKey"),
filePrefix=file_prefix
)
return layer_data
def _generate_file_sequence(
self, layer_data,
@ -330,7 +341,7 @@ class ARenderProducts:
force_cameras=None):
# type: (LayerMetadata, str, str, list) -> list
expected_files = []
cameras = force_cameras if force_cameras else layer_data.cameras
cameras = force_cameras or layer_data.cameras
ext = force_ext or layer_data.defaultExt
for cam in cameras:
file_prefix = layer_data.filePrefix
@ -361,8 +372,8 @@ class ARenderProducts:
)
return expected_files
def get_files(self, product, camera):
# type: (RenderProduct, str) -> list
def get_files(self, product):
# type: (RenderProduct) -> list
"""Return list of expected files.
It will translate render token strings ('<RenderPass>', etc.) to
@ -373,7 +384,6 @@ class ARenderProducts:
Args:
product (RenderProduct): Render product to be used for file
generation.
camera (str): Camera name.
Returns:
List of files
@ -383,7 +393,7 @@ class ARenderProducts:
self.layer_data,
force_aov_name=product.productName,
force_ext=product.ext,
force_cameras=[camera]
force_cameras=[product.camera]
)
def get_renderable_cameras(self):
@ -460,15 +470,21 @@ class RenderProductsArnold(ARenderProducts):
return prefix
def _get_aov_render_products(self, aov):
def _get_aov_render_products(self, aov, cameras=None):
"""Return all render products for the AOV"""
products = list()
products = []
aov_name = self._get_attr(aov, "name")
ai_drivers = cmds.listConnections("{}.outputs".format(aov),
source=True,
destination=False,
type="aiAOVDriver") or []
if not cameras:
cameras = [
self.sanitize_camera_name(
self.get_renderable_cameras()[0]
)
]
for ai_driver in ai_drivers:
# todo: check aiAOVDriver.prefix as it could have
@ -497,30 +513,37 @@ class RenderProductsArnold(ARenderProducts):
name = "beauty"
# Support Arnold light groups for AOVs
# Global AOV: When disabled the main layer is not written: `{pass}`
# Global AOV: When disabled the main layer is
# not written: `{pass}`
# All Light Groups: When enabled, a `{pass}_lgroups` file is
# written and is always merged into a single file
# Light Groups List: When set, a product per light group is written
# written and is always merged into a
# single file
# Light Groups List: When set, a product per light
# group is written
# e.g. {pass}_front, {pass}_rim
global_aov = self._get_attr(aov, "globalAov")
if global_aov:
product = RenderProduct(productName=name,
ext=ext,
aov=aov_name,
driver=ai_driver)
products.append(product)
for camera in cameras:
product = RenderProduct(productName=name,
ext=ext,
aov=aov_name,
driver=ai_driver,
camera=camera)
products.append(product)
all_light_groups = self._get_attr(aov, "lightGroups")
if all_light_groups:
# All light groups is enabled. A single multipart
# Render Product
product = RenderProduct(productName=name + "_lgroups",
ext=ext,
aov=aov_name,
driver=ai_driver,
# Always multichannel output
multipart=True)
products.append(product)
for camera in cameras:
product = RenderProduct(productName=name + "_lgroups",
ext=ext,
aov=aov_name,
driver=ai_driver,
# Always multichannel output
multipart=True,
camera=camera)
products.append(product)
else:
value = self._get_attr(aov, "lightGroupsList")
if not value:
@ -529,11 +552,15 @@ class RenderProductsArnold(ARenderProducts):
for light_group in selected_light_groups:
# Render Product per selected light group
aov_light_group_name = "{}_{}".format(name, light_group)
product = RenderProduct(productName=aov_light_group_name,
aov=aov_name,
driver=ai_driver,
ext=ext)
products.append(product)
for camera in cameras:
product = RenderProduct(
productName=aov_light_group_name,
aov=aov_name,
driver=ai_driver,
ext=ext,
camera=camera
)
products.append(product)
return products
@ -556,17 +583,26 @@ class RenderProductsArnold(ARenderProducts):
# anyway.
return []
default_ext = self._get_attr("defaultRenderGlobals.imfPluginKey")
beauty_product = RenderProduct(productName="beauty",
ext=default_ext,
driver="defaultArnoldDriver")
# check if camera token is in prefix. If so, and we have list of
# renderable cameras, generate render product for each and every
# of them.
cameras = [
self.sanitize_camera_name(c)
for c in self.get_renderable_cameras()
]
default_ext = self._get_attr("defaultRenderGlobals.imfPluginKey")
beauty_products = [RenderProduct(
productName="beauty",
ext=default_ext,
driver="defaultArnoldDriver",
camera=camera) for camera in cameras]
# AOVs > Legacy > Maya Render View > Mode
aovs_enabled = bool(
self._get_attr("defaultArnoldRenderOptions.aovMode")
)
if not aovs_enabled:
return [beauty_product]
return beauty_products
# Common > File Output > Merge AOVs or <RenderPass>
# We don't need to check for Merge AOVs due to overridden
@ -575,8 +611,9 @@ class RenderProductsArnold(ARenderProducts):
"<renderpass>" in self.layer_data.filePrefix.lower()
)
if not has_renderpass_token:
beauty_product.multipart = True
return [beauty_product]
for product in beauty_products:
product.multipart = True
return beauty_products
# AOVs are set to be rendered separately. We should expect
# <RenderPass> token in path.
@ -598,14 +635,14 @@ class RenderProductsArnold(ARenderProducts):
continue
# For now stick to the legacy output format.
aov_products = self._get_aov_render_products(aov)
aov_products = self._get_aov_render_products(aov, cameras)
products.extend(aov_products)
if not any(product.aov == "RGBA" for product in products):
if all(product.aov != "RGBA" for product in products):
# Append default 'beauty' as this is arnolds default.
# However, it is excluded whenever a RGBA pass is enabled.
# For legibility add the beauty layer as first entry
products.insert(0, beauty_product)
products += beauty_products
# TODO: Output Denoising AOVs?
@ -670,6 +707,11 @@ class RenderProductsVray(ARenderProducts):
# anyway.
return []
cameras = [
self.sanitize_camera_name(c)
for c in self.get_renderable_cameras()
]
image_format_str = self._get_attr("vraySettings.imageFormatStr")
default_ext = image_format_str
if default_ext in {"exr (multichannel)", "exr (deep)"}:
@ -680,13 +722,21 @@ class RenderProductsVray(ARenderProducts):
# add beauty as default when not disabled
dont_save_rgb = self._get_attr("vraySettings.dontSaveRgbChannel")
if not dont_save_rgb:
products.append(RenderProduct(productName="", ext=default_ext))
for camera in cameras:
products.append(
RenderProduct(productName="",
ext=default_ext,
camera=camera))
# separate alpha file
separate_alpha = self._get_attr("vraySettings.separateAlpha")
if separate_alpha:
products.append(RenderProduct(productName="Alpha",
ext=default_ext))
for camera in cameras:
products.append(
RenderProduct(productName="Alpha",
ext=default_ext,
camera=camera)
)
if image_format_str == "exr (multichannel)":
# AOVs are merged in m-channel file, only main layer is rendered
@ -716,19 +766,23 @@ class RenderProductsVray(ARenderProducts):
# instead seems to output multiple Render Products,
# specifically "Self_Illumination" and "Environment"
product_names = ["Self_Illumination", "Environment"]
for name in product_names:
product = RenderProduct(productName=name,
ext=default_ext,
aov=aov)
products.append(product)
for camera in cameras:
for name in product_names:
product = RenderProduct(productName=name,
ext=default_ext,
aov=aov,
camera=camera)
products.append(product)
# Continue as we've processed this special case AOV
continue
aov_name = self._get_vray_aov_name(aov)
product = RenderProduct(productName=aov_name,
ext=default_ext,
aov=aov)
products.append(product)
for camera in cameras:
product = RenderProduct(productName=aov_name,
ext=default_ext,
aov=aov,
camera=camera)
products.append(product)
return products
@ -875,6 +929,11 @@ class RenderProductsRedshift(ARenderProducts):
# anyway.
return []
cameras = [
self.sanitize_camera_name(c)
for c in self.get_renderable_cameras()
]
# For Redshift we don't directly return upon forcing multilayer
# due to some AOVs still being written into separate files,
# like Cryptomatte.
@ -933,11 +992,14 @@ class RenderProductsRedshift(ARenderProducts):
for light_group in light_groups:
aov_light_group_name = "{}_{}".format(aov_name,
light_group)
product = RenderProduct(productName=aov_light_group_name,
aov=aov_name,
ext=ext,
multipart=aov_multipart)
products.append(product)
for camera in cameras:
product = RenderProduct(
productName=aov_light_group_name,
aov=aov_name,
ext=ext,
multipart=aov_multipart,
camera=camera)
products.append(product)
if light_groups:
light_groups_enabled = True
@ -945,11 +1007,13 @@ class RenderProductsRedshift(ARenderProducts):
# Redshift AOV Light Select always renders the global AOV
# even when light groups are present so we don't need to
# exclude it when light groups are active
product = RenderProduct(productName=aov_name,
aov=aov_name,
ext=ext,
multipart=aov_multipart)
products.append(product)
for camera in cameras:
product = RenderProduct(productName=aov_name,
aov=aov_name,
ext=ext,
multipart=aov_multipart,
camera=camera)
products.append(product)
# When a Beauty AOV is added manually, it will be rendered as
# 'Beauty_other' in file name and "standard" beauty will have
@ -959,10 +1023,12 @@ class RenderProductsRedshift(ARenderProducts):
return products
beauty_name = "Beauty_other" if has_beauty_aov else ""
products.insert(0,
RenderProduct(productName=beauty_name,
ext=ext,
multipart=multipart))
for camera in cameras:
products.insert(0,
RenderProduct(productName=beauty_name,
ext=ext,
multipart=multipart,
camera=camera))
return products
@ -987,6 +1053,16 @@ class RenderProductsRenderman(ARenderProducts):
:func:`ARenderProducts.get_render_products()`
"""
cameras = [
self.sanitize_camera_name(c)
for c in self.get_renderable_cameras()
]
if not cameras:
cameras = [
self.sanitize_camera_name(
self.get_renderable_cameras()[0])
]
products = []
default_ext = "exr"
@ -1000,9 +1076,11 @@ class RenderProductsRenderman(ARenderProducts):
if aov_name == "rmanDefaultDisplay":
aov_name = "beauty"
product = RenderProduct(productName=aov_name,
ext=default_ext)
products.append(product)
for camera in cameras:
product = RenderProduct(productName=aov_name,
ext=default_ext,
camera=camera)
products.append(product)
return products

View file

@ -174,10 +174,16 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
assert render_products, "no render products generated"
exp_files = []
for product in render_products:
for camera in layer_render_products.layer_data.cameras:
exp_files.append(
{product.productName: layer_render_products.get_files(
product, camera)})
product_name = product.productName
if product.camera and layer_render_products.has_camera_token():
product_name = "{}{}".format(
product.camera,
"_" + product_name if product_name else "")
exp_files.append(
{
product_name: layer_render_products.get_files(
product)
})
self.log.info("multipart: {}".format(
layer_render_products.multipart))
@ -199,12 +205,14 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
# replace relative paths with absolute. Render products are
# returned as list of dictionaries.
publish_meta_path = None
for aov in exp_files:
full_paths = []
for file in aov[aov.keys()[0]]:
full_path = os.path.join(workspace, "renders", file)
full_path = full_path.replace("\\", "/")
full_paths.append(full_path)
publish_meta_path = os.path.dirname(full_path)
aov_dict[aov.keys()[0]] = full_paths
frame_start_render = int(self.get_render_attribute(
@ -230,6 +238,26 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
frame_end_handle = frame_end_render
full_exp_files.append(aov_dict)
# find common path to store metadata
# so if image prefix is branching to many directories
# metadata file will be located in top-most common
# directory.
# TODO: use `os.path.commonpath()` after switch to Python 3
common_publish_meta_path = os.path.splitdrive(
publish_meta_path)[0]
if common_publish_meta_path:
common_publish_meta_path += os.path.sep
for part in publish_meta_path.split("/"):
common_publish_meta_path = os.path.join(
common_publish_meta_path, part)
if part == expected_layer_name:
break
common_publish_meta_path = common_publish_meta_path.replace(
"\\", "/")
self.log.info(
"Publish meta path: {}".format(common_publish_meta_path))
self.log.info(full_exp_files)
self.log.info("collecting layer: {}".format(layer_name))
# Get layer specific settings, might be overrides
@ -262,6 +290,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
# which was submitted originally
"source": filepath,
"expectedFiles": full_exp_files,
"publishRenderMetadataFolder": common_publish_meta_path,
"resolutionWidth": cmds.getAttr("defaultResolution.width"),
"resolutionHeight": cmds.getAttr("defaultResolution.height"),
"pixelAspect": cmds.getAttr("defaultResolution.pixelAspect"),

View file

@ -76,7 +76,7 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
r'%a|<aov>|<renderpass>', re.IGNORECASE)
R_LAYER_TOKEN = re.compile(
r'%l|<layer>|<renderlayer>', re.IGNORECASE)
R_CAMERA_TOKEN = re.compile(r'%c|<camera>', re.IGNORECASE)
R_CAMERA_TOKEN = re.compile(r'%c|Camera>')
R_SCENE_TOKEN = re.compile(r'%s|<scene>', re.IGNORECASE)
DEFAULT_PADDING = 4
@ -126,7 +126,9 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
if len(cameras) > 1 and not re.search(cls.R_CAMERA_TOKEN, prefix):
invalid = True
cls.log.error("Wrong image prefix [ {} ] - "
"doesn't have: '<camera>' token".format(prefix))
"doesn't have: '<Camera>' token".format(prefix))
cls.log.error(
"Note that to needs to have capital 'C' at the beginning")
# renderer specific checks
if renderer == "vray":

View file

@ -351,6 +351,11 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin):
f.replace(orig_scene, new_scene)
)
instance.data["expectedFiles"] = [new_exp]
if instance.data.get("publishRenderMetadataFolder"):
instance.data["publishRenderMetadataFolder"] = \
instance.data["publishRenderMetadataFolder"].replace(
orig_scene, new_scene)
self.log.info("Scene name was switched {} -> {}".format(
orig_scene, new_scene
))

View file

@ -385,6 +385,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
"""
task = os.environ["AVALON_TASK"]
subset = instance_data["subset"]
cameras = instance_data.get("cameras", [])
instances = []
# go through aovs in expected files
for aov, files in exp_files[0].items():
@ -410,7 +411,11 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
task[0].upper(), task[1:],
subset[0].upper(), subset[1:])
subset_name = '{}_{}'.format(group_name, aov)
cam = [c for c in cameras if c in col.head]
if cam:
subset_name = '{}_{}_{}'.format(group_name, cam, aov)
else:
subset_name = '{}_{}'.format(group_name, aov)
if isinstance(col, (list, tuple)):
staging = os.path.dirname(col[0])