Merge branch 'develop' into enhancement/OP-3095_TrayPublisher-Simple-families-from-settings

This commit is contained in:
Jakub Trllo 2022-04-27 10:50:22 +02:00
commit 0cc02f3abe
11 changed files with 505 additions and 39 deletions

View file

@ -79,6 +79,7 @@ IMAGE_PREFIXES = {
"redshift": "defaultRenderGlobals.imageFilePrefix",
}
RENDERMAN_IMAGE_DIR = "maya/<scene>/<layer>"
@attr.s
class LayerMetadata(object):
@ -1054,6 +1055,8 @@ class RenderProductsRenderman(ARenderProducts):
:func:`ARenderProducts.get_render_products()`
"""
from rfm2.api.displays import get_displays # noqa
cameras = [
self.sanitize_camera_name(c)
for c in self.get_renderable_cameras()
@ -1066,42 +1069,56 @@ class RenderProductsRenderman(ARenderProducts):
]
products = []
default_ext = "exr"
displays = cmds.listConnections("rmanGlobals.displays")
for aov in displays:
enabled = self._get_attr(aov, "enabled")
# NOTE: This is guessing extensions from renderman display types.
# Some of them are just framebuffers, d_texture format can be
# set in display setting. We set those now to None, but it
# should be handled more gracefully.
display_types = {
"d_deepexr": "exr",
"d_it": None,
"d_null": None,
"d_openexr": "exr",
"d_png": "png",
"d_pointcloud": "ptc",
"d_targa": "tga",
"d_texture": None,
"d_tiff": "tif"
}
displays = get_displays()["displays"]
for name, display in displays.items():
enabled = display["params"]["enable"]["value"]
if not enabled:
continue
aov_name = str(aov)
aov_name = name
if aov_name == "rmanDefaultDisplay":
aov_name = "beauty"
extensions = display_types.get(
display["driverNode"]["type"], "exr")
for camera in cameras:
product = RenderProduct(productName=aov_name,
ext=default_ext,
ext=extensions,
camera=camera)
products.append(product)
return products
def get_files(self, product, camera):
def get_files(self, product):
"""Get expected files.
In renderman we hack it with prepending path. This path would
normally be translated from `rmanGlobals.imageOutputDir`. We skip
this and hardcode prepend path we expect. There is no place for user
to mess around with this settings anyway and it is enforced in
render settings validator.
"""
files = super(RenderProductsRenderman, self).get_files(product, camera)
files = super(RenderProductsRenderman, self).get_files(product)
layer_data = self.layer_data
new_files = []
resolved_image_dir = re.sub("<scene>", layer_data.sceneName, RENDERMAN_IMAGE_DIR, flags=re.IGNORECASE) # noqa: E501
resolved_image_dir = re.sub("<layer>", layer_data.layerName, resolved_image_dir, flags=re.IGNORECASE) # noqa: E501
for file in files:
new_file = "{}/{}/{}".format(
layer_data["sceneName"], layer_data["layerName"], file
)
new_file = "{}/{}".format(resolved_image_dir, file)
new_files.append(new_file)
return new_files

View file

@ -76,7 +76,7 @@ class CreateRender(plugin.Creator):
'mentalray': 'defaultRenderGlobals.imageFilePrefix',
'vray': 'vraySettings.fileNamePrefix',
'arnold': 'defaultRenderGlobals.imageFilePrefix',
'renderman': 'defaultRenderGlobals.imageFilePrefix',
'renderman': 'rmanGlobals.imageFileFormat',
'redshift': 'defaultRenderGlobals.imageFilePrefix'
}
@ -84,7 +84,9 @@ class CreateRender(plugin.Creator):
'mentalray': 'maya/<Scene>/<RenderLayer>/<RenderLayer>{aov_separator}<RenderPass>', # noqa
'vray': 'maya/<scene>/<Layer>/<Layer>',
'arnold': 'maya/<Scene>/<RenderLayer>/<RenderLayer>{aov_separator}<RenderPass>', # noqa
'renderman': 'maya/<Scene>/<layer>/<layer>{aov_separator}<aov>',
# this needs `imageOutputDir`
# (<ws>/renders/maya/<scene>) set separately
'renderman': '<layer>_<aov>.<f4>.<ext>',
'redshift': 'maya/<Scene>/<RenderLayer>/<RenderLayer>' # noqa
}
@ -440,6 +442,10 @@ class CreateRender(plugin.Creator):
self._set_global_output_settings()
if renderer == "renderman":
cmds.setAttr("rmanGlobals.imageOutputDir",
"maya/<scene>/<layer>", type="string")
def _set_vray_settings(self, asset):
# type: (dict) -> None
"""Sets important settings for Vray."""

View file

@ -194,13 +194,11 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
assert render_products, "no render products generated"
exp_files = []
multipart = False
render_cameras = []
for product in render_products:
if product.multipart:
multipart = True
product_name = product.productName
if product.camera and layer_render_products.has_camera_token():
render_cameras.append(product.camera)
product_name = "{}{}".format(
product.camera,
"_" + product_name if product_name else "")
@ -210,7 +208,8 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
product)
})
assert render_cameras, "No render cameras found."
has_cameras = any(product.camera for product in render_products)
assert has_cameras, "No render cameras found."
self.log.info("multipart: {}".format(
multipart))

View file

@ -69,14 +69,7 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
redshift_AOV_prefix = "<BeautyPath>/<BeautyFile>{aov_separator}<RenderPass>" # noqa: E501
# WARNING: There is bug? in renderman, translating <scene> token
# to something left behind mayas default image prefix. So instead
# `SceneName_v01` it translates to:
# `SceneName_v01/<RenderLayer>/<RenderLayers_<RenderPass>` that means
# for example:
# `SceneName_v01/Main/Main_<RenderPass>`. Possible solution is to define
# custom token like <scene_name> to point to determined scene name.
RendermanDirPrefix = "<ws>/renders/maya/<scene>/<layer>"
renderman_dir_prefix = "maya/<scene>/<layer>"
R_AOV_TOKEN = re.compile(
r'%a|<aov>|<renderpass>', re.IGNORECASE)
@ -116,15 +109,22 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
prefix = prefix.replace(
"{aov_separator}", instance.data.get("aovSeparator", "_"))
required_prefix = "maya/<scene>"
if not anim_override:
invalid = True
cls.log.error("Animation needs to be enabled. Use the same "
"frame for start and end to render single frame")
if not prefix.lower().startswith("maya/<scene>"):
if renderer != "renderman" and not prefix.lower().startswith(
required_prefix):
invalid = True
cls.log.error("Wrong image prefix [ {} ] - "
"doesn't start with: 'maya/<scene>'".format(prefix))
cls.log.error(
("Wrong image prefix [ {} ] "
" - doesn't start with: '{}'").format(
prefix, required_prefix)
)
if not re.search(cls.R_LAYER_TOKEN, prefix):
invalid = True
@ -198,7 +198,7 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
invalid = True
cls.log.error("Wrong image prefix [ {} ]".format(file_prefix))
if dir_prefix.lower() != cls.RendermanDirPrefix.lower():
if dir_prefix.lower() != cls.renderman_dir_prefix.lower():
invalid = True
cls.log.error("Wrong directory prefix [ {} ]".format(
dir_prefix))
@ -304,7 +304,7 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
default_prefix,
type="string")
cmds.setAttr("rmanGlobals.imageOutputDir",
cls.RendermanDirPrefix,
cls.renderman_dir_prefix,
type="string")
if renderer == "vray":

View file

@ -188,6 +188,10 @@ def get_renderer_variables(renderlayer, root):
filename_0 = re.sub('_<RenderPass>', '_beauty',
filename_0, flags=re.IGNORECASE)
prefix_attr = "defaultRenderGlobals.imageFilePrefix"
scene = cmds.file(query=True, sceneName=True)
scene, _ = os.path.splitext(os.path.basename(scene))
if renderer == "vray":
renderlayer = renderlayer.split("_")[-1]
# Maya's renderSettings function does not return V-Ray file extension
@ -207,8 +211,7 @@ def get_renderer_variables(renderlayer, root):
filename_prefix = cmds.getAttr(prefix_attr)
# we need to determine path for vray as maya `renderSettings` query
# does not work for vray.
scene = cmds.file(query=True, sceneName=True)
scene, _ = os.path.splitext(os.path.basename(scene))
filename_0 = re.sub('<Scene>', scene, filename_prefix, flags=re.IGNORECASE) # noqa: E501
filename_0 = re.sub('<Layer>', renderlayer, filename_0, flags=re.IGNORECASE) # noqa: E501
filename_0 = "{}.{}.{}".format(
@ -216,6 +219,39 @@ def get_renderer_variables(renderlayer, root):
filename_0 = os.path.normpath(os.path.join(root, filename_0))
elif renderer == "renderman":
prefix_attr = "rmanGlobals.imageFileFormat"
# NOTE: This is guessing extensions from renderman display types.
# Some of them are just framebuffers, d_texture format can be
# set in display setting. We set those now to None, but it
# should be handled more gracefully.
display_types = {
"d_deepexr": "exr",
"d_it": None,
"d_null": None,
"d_openexr": "exr",
"d_png": "png",
"d_pointcloud": "ptc",
"d_targa": "tga",
"d_texture": None,
"d_tiff": "tif"
}
extension = display_types.get(
cmds.listConnections("rmanDefaultDisplay.displayType")[0],
"exr"
) or "exr"
filename_prefix = "{}/{}".format(
cmds.getAttr("rmanGlobals.imageOutputDir"),
cmds.getAttr("rmanGlobals.imageFileFormat")
)
renderlayer = renderlayer.split("_")[-1]
filename_0 = re.sub('<scene>', scene, filename_prefix, flags=re.IGNORECASE) # noqa: E501
filename_0 = re.sub('<layer>', renderlayer, filename_0, flags=re.IGNORECASE) # noqa: E501
filename_0 = re.sub('<f[\\d+]>', "#" * int(padding), filename_0, flags=re.IGNORECASE) # noqa: E501
filename_0 = re.sub('<ext>', extension, filename_0, flags=re.IGNORECASE) # noqa: E501
filename_0 = os.path.normpath(os.path.join(root, filename_0))
elif renderer == "redshift":
# mapping redshift extension dropdown values to strings
ext_mapping = ["iff", "exr", "tif", "png", "tga", "jpg"]
@ -404,6 +440,8 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin):
output_filename_0 = filename_0
dirname = os.path.dirname(output_filename_0)
# Create render folder ----------------------------------------------
try:
# Ensure render folder exists
@ -799,6 +837,23 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin):
"AssetDependency0": data["filepath"],
}
renderer = self._instance.data["renderer"]
# This hack is here because of how Deadline handles Renderman version.
# it considers everything with `renderman` set as version older than
# Renderman 22, and so if we are using renderman > 21 we need to set
# renderer string on the job to `renderman22`. We will have to change
# this when Deadline releases new version handling this.
if self._instance.data["renderer"] == "renderman":
try:
from rfm2.config import cfg # noqa
except ImportError:
raise Exception("Cannot determine renderman version")
rman_version = cfg().build_info.version() # type: str
if int(rman_version.split(".")[0]) > 22:
renderer = "renderman22"
plugin_info = {
"SceneFile": data["filepath"],
# Output directory and filename
@ -812,7 +867,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin):
"RenderLayer": data["renderlayer"],
# Determine which renderer to use from the file itself
"Renderer": self._instance.data["renderer"],
"Renderer": renderer,
# Resolve relative references
"ProjectPath": data["workspace"],

View file

@ -52,10 +52,39 @@
"environment": {},
"variants": {}
},
"renderman": {
"environment": {},
"variants": {
"24-3-maya": {
"host_names": [
"maya"
],
"app_variants": [
"maya/2022"
],
"environment": {
"RFMTREE": {
"windows": "C:\\Program Files\\Pixar\\RenderManForMaya-24.3",
"darwin": "/Applications/Pixar/RenderManForMaya-24.3",
"linux": "/opt/pixar/RenderManForMaya-24.3"
},
"RMANTREE": {
"windows": "C:\\Program Files\\Pixar\\RenderManProServer-24.3",
"darwin": "/Applications/Pixar/RenderManProServer-24.3",
"linux": "/opt/pixar/RenderManProServer-24.3"
}
}
},
"__dynamic_keys_labels__": {
"24-3-maya": "24.3 RFM"
}
}
},
"__dynamic_keys_labels__": {
"mtoa": "Autodesk Arnold",
"vray": "Chaos Group Vray",
"yeti": "Pergrine Labs Yeti"
"yeti": "Peregrine Labs Yeti",
"renderman": "Pixar Renderman"
}
}
}

View file

@ -61,7 +61,11 @@
"icon-entity-default": "#bfccd6",
"icon-entity-disabled": "#808080",
"font-entity-deprecated": "#666666",
"overlay-messages": {
"close-btn": "#D3D8DE",
"bg-success": "#458056",
"bg-success-hover": "#55a066"
},
"tab-widget": {
"bg": "#21252B",
"bg-selected": "#434a56",

View file

@ -687,6 +687,26 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
background: none;
}
/* Messages overlay */
#OverlayMessageWidget {
border-radius: 0.2em;
background: {color:bg-buttons};
}
#OverlayMessageWidget:hover {
background: {color:bg-button-hover};
}
#OverlayMessageWidget {
background: {color:overlay-messages:bg-success};
}
#OverlayMessageWidget:hover {
background: {color:overlay-messages:bg-success-hover};
}
#OverlayMessageWidget QWidget {
background: transparent;
}
/* Password dialog*/
#PasswordBtn {
border: none;

View file

@ -8,6 +8,7 @@ from openpype.settings.lib import (
save_local_settings
)
from openpype.tools.settings import CHILD_OFFSET
from openpype.tools.utils import MessageOverlayObject
from openpype.api import (
Logger,
SystemSettings,
@ -221,6 +222,8 @@ class LocalSettingsWindow(QtWidgets.QWidget):
self.setWindowTitle("OpenPype Local settings")
overlay_object = MessageOverlayObject(self)
stylesheet = style.load_stylesheet()
self.setStyleSheet(stylesheet)
self.setWindowIcon(QtGui.QIcon(style.app_icon_path()))
@ -247,6 +250,7 @@ class LocalSettingsWindow(QtWidgets.QWidget):
save_btn.clicked.connect(self._on_save_clicked)
reset_btn.clicked.connect(self._on_reset_clicked)
self._overlay_object = overlay_object
# Do not create local settings widget in init phase as it's using
# settings objects that must be OK to be able create this widget
# - we want to show dialog if anything goes wrong
@ -312,8 +316,10 @@ class LocalSettingsWindow(QtWidgets.QWidget):
def _on_reset_clicked(self):
self.reset()
self._overlay_object.add_message("Refreshed...")
def _on_save_clicked(self):
value = self._settings_widget.settings_value()
save_local_settings(value)
self._overlay_object.add_message("Saved...", message_type="success")
self.reset()

View file

@ -22,6 +22,10 @@ from .lib import (
from .models import (
RecursiveSortFilterProxyModel,
)
from .overlay_messages import (
MessageOverlayObject,
)
__all__ = (
"PlaceholderLineEdit",
@ -45,4 +49,6 @@ __all__ = (
"get_asset_icon",
"RecursiveSortFilterProxyModel",
"MessageOverlayObject",
)

View file

@ -0,0 +1,324 @@
import uuid
from Qt import QtWidgets, QtCore, QtGui
from openpype.style import get_objected_colors
from .lib import set_style_property
class CloseButton(QtWidgets.QFrame):
"""Close button drawed manually."""
clicked = QtCore.Signal()
def __init__(self, parent):
super(CloseButton, self).__init__(parent)
colors = get_objected_colors()
close_btn_color = colors["overlay-messages"]["close-btn"]
self._color = close_btn_color.get_qcolor()
self._mouse_pressed = False
policy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Fixed,
QtWidgets.QSizePolicy.Fixed
)
self.setSizePolicy(policy)
def sizeHint(self):
size = self.fontMetrics().height()
return QtCore.QSize(size, size)
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
self._mouse_pressed = True
super(CloseButton, self).mousePressEvent(event)
def mouseReleaseEvent(self, event):
if self._mouse_pressed:
self._mouse_pressed = False
if self.rect().contains(event.pos()):
self.clicked.emit()
super(CloseButton, self).mouseReleaseEvent(event)
def paintEvent(self, event):
rect = self.rect()
painter = QtGui.QPainter(self)
painter.setClipRect(event.rect())
pen = QtGui.QPen()
pen.setWidth(2)
pen.setColor(self._color)
pen.setStyle(QtCore.Qt.SolidLine)
pen.setCapStyle(QtCore.Qt.RoundCap)
painter.setPen(pen)
offset = int(rect.height() / 4)
top = rect.top() + offset
left = rect.left() + offset
right = rect.right() - offset
bottom = rect.bottom() - offset
painter.drawLine(
left, top,
right, bottom
)
painter.drawLine(
left, bottom,
right, top
)
class OverlayMessageWidget(QtWidgets.QFrame):
"""Message widget showed as overlay.
Message is hidden after timeout but can be overriden by mouse hover.
Mouse hover can add additional 2 seconds of widget's visibility.
Args:
message_id (str): Unique identifier of message widget for
'MessageOverlayObject'.
message (str): Text shown in message.
parent (QWidget): Parent widget where message is visible.
timeout (int): Timeout of message's visibility (default 5000).
message_type (str): Property which can be used in styles for specific
kid of message.
"""
close_requested = QtCore.Signal(str)
_default_timeout = 5000
def __init__(
self, message_id, message, parent, message_type=None, timeout=None
):
super(OverlayMessageWidget, self).__init__(parent)
self.setObjectName("OverlayMessageWidget")
if message_type:
set_style_property(self, "type", message_type)
if not timeout:
timeout = self._default_timeout
timeout_timer = QtCore.QTimer()
timeout_timer.setInterval(timeout)
timeout_timer.setSingleShot(True)
hover_timer = QtCore.QTimer()
hover_timer.setInterval(2000)
hover_timer.setSingleShot(True)
label_widget = QtWidgets.QLabel(message, self)
label_widget.setAlignment(QtCore.Qt.AlignCenter)
label_widget.setWordWrap(True)
close_btn = CloseButton(self)
layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(5, 5, 0, 5)
layout.addWidget(label_widget, 1)
layout.addWidget(close_btn, 0)
close_btn.clicked.connect(self._on_close_clicked)
timeout_timer.timeout.connect(self._on_timer_timeout)
hover_timer.timeout.connect(self._on_hover_timeout)
self._label_widget = label_widget
self._message_id = message_id
self._timeout_timer = timeout_timer
self._hover_timer = hover_timer
def size_hint_without_word_wrap(self):
"""Size hint in cases that word wrap of label is disabled."""
self._label_widget.setWordWrap(False)
size_hint = self.sizeHint()
self._label_widget.setWordWrap(True)
return size_hint
def showEvent(self, event):
"""Start timeout on show."""
super(OverlayMessageWidget, self).showEvent(event)
self._timeout_timer.start()
def _on_timer_timeout(self):
"""On message timeout."""
# Skip closing if hover timer is active
if not self._hover_timer.isActive():
self._close_message()
def _on_hover_timeout(self):
"""Hover timer timed out."""
# Check if is still under widget
if self.underMouse():
self._hover_timer.start()
else:
self._close_message()
def _on_close_clicked(self):
self._close_message()
def _close_message(self):
"""Emmit close request to 'MessageOverlayObject'."""
self.close_requested.emit(self._message_id)
def enterEvent(self, event):
"""Start hover timer on hover."""
super(OverlayMessageWidget, self).enterEvent(event)
self._hover_timer.start()
def leaveEvent(self, event):
"""Start hover timer on hover leave."""
super(OverlayMessageWidget, self).leaveEvent(event)
self._hover_timer.start()
class MessageOverlayObject(QtCore.QObject):
"""Object that can be used to add overlay messages.
Args:
widget (QWidget):
"""
def __init__(self, widget, default_timeout=None):
super(MessageOverlayObject, self).__init__()
widget.installEventFilter(self)
# Timer which triggers recalculation of message positions
recalculate_timer = QtCore.QTimer()
recalculate_timer.setInterval(10)
recalculate_timer.timeout.connect(self._recalculate_positions)
self._widget = widget
self._recalculate_timer = recalculate_timer
self._messages_order = []
self._closing_messages = set()
self._messages = {}
self._spacing = 5
self._move_size = 4
self._move_size_remove = 8
self._default_timeout = default_timeout
def add_message(self, message, message_type=None, timeout=None):
"""Add single message into overlay.
Args:
message (str): Message that will be shown.
timeout (int): Message timeout.
message_type (str): Message type can be used as property in
stylesheets.
"""
# Skip empty messages
if not message:
return
if timeout is None:
timeout = self._default_timeout
# Create unique id of message
label_id = str(uuid.uuid4())
# Create message widget
widget = OverlayMessageWidget(
label_id, message, self._widget, message_type, timeout
)
widget.close_requested.connect(self._on_message_close_request)
widget.show()
# Move widget outside of window
pos = widget.pos()
pos.setY(pos.y() - widget.height())
widget.move(pos)
# Store message
self._messages[label_id] = widget
self._messages_order.append(label_id)
# Trigger recalculation timer
self._recalculate_timer.start()
def _on_message_close_request(self, label_id):
"""Message widget requested removement."""
widget = self._messages.get(label_id)
if widget is not None:
# Add message to closing messages and start recalculation
self._closing_messages.add(label_id)
self._recalculate_timer.start()
def _recalculate_positions(self):
"""Recalculate positions of widgets."""
# Skip if there are no messages to process
if not self._messages_order:
self._recalculate_timer.stop()
return
# All message widgets are in expected positions
all_at_place = True
# Starting y position
pos_y = self._spacing
# Current widget width
widget_width = self._widget.width()
max_width = widget_width - (2 * self._spacing)
widget_half_width = widget_width / 2
# Store message ids that should be removed
message_ids_to_remove = set()
for message_id in reversed(self._messages_order):
widget = self._messages[message_id]
pos = widget.pos()
# Messages to remove are moved upwards
if message_id in self._closing_messages:
bottom = pos.y() + widget.height()
# Add message to remove if is not visible
if bottom < 0 or self._move_size_remove < 1:
message_ids_to_remove.add(message_id)
continue
# Calculate new y position of message
dst_pos_y = pos.y() - self._move_size_remove
else:
# Calculate y position of message
# - use y position of previous message widget and add
# move size if is not in final destination yet
if widget.underMouse():
dst_pos_y = pos.y()
elif pos.y() == pos_y or self._move_size < 1:
dst_pos_y = pos_y
elif pos.y() < pos_y:
dst_pos_y = min(pos_y, pos.y() + self._move_size)
else:
dst_pos_y = max(pos_y, pos.y() - self._move_size)
# Store if widget is in place where should be
if all_at_place and dst_pos_y != pos_y:
all_at_place = False
# Calculate ideal width and height of message widget
height = widget.heightForWidth(max_width)
w_size_hint = widget.size_hint_without_word_wrap()
widget.resize(min(max_width, w_size_hint.width()), height)
# Center message widget
size = widget.size()
pos_x = widget_half_width - (size.width() / 2)
# Move widget to destination position
widget.move(pos_x, dst_pos_y)
# Add message widget height and spacing for next message widget
pos_y += size.height() + self._spacing
# Remove widgets to remove
for message_id in message_ids_to_remove:
self._messages_order.remove(message_id)
self._closing_messages.remove(message_id)
widget = self._messages.pop(message_id)
widget.hide()
widget.deleteLater()
# Stop recalculation timer if all widgets are where should be
if all_at_place:
self._recalculate_timer.stop()
def eventFilter(self, source, event):
# Trigger recalculation of timer on resize of widget
if source is self._widget and event.type() == QtCore.QEvent.Resize:
self._recalculate_timer.start()
return super(MessageOverlayObject, self).eventFilter(source, event)