mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-25 05:14:40 +01:00
Merge remote-tracking branch 'upstream/develop' into maya_extract_look_tx_colorspace
This commit is contained in:
commit
11da84f71f
57 changed files with 1439 additions and 1337 deletions
3
.github/workflows/project_actions.yml
vendored
3
.github/workflows/project_actions.yml
vendored
|
|
@ -2,7 +2,7 @@ name: project-actions
|
|||
|
||||
on:
|
||||
pull_request:
|
||||
types: [review_requested, closed]
|
||||
types: [review_requested]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
|
|
@ -16,7 +16,6 @@ jobs:
|
|||
uses: leonsteinhaeuser/project-beta-automations@v2.1.0
|
||||
with:
|
||||
gh_token: ${{ secrets.YNPUT_BOT_TOKEN }}
|
||||
user: ${{ secrets.CI_USER }}
|
||||
organization: ynput
|
||||
project_id: 11
|
||||
resource_node_id: ${{ github.event.pull_request.node_id }}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ class LaunchFoundryAppsWindows(PreLaunchHook):
|
|||
|
||||
Nuke is executed "like" python process so it is required to pass
|
||||
`CREATE_NEW_CONSOLE` flag on windows to trigger creation of new console.
|
||||
At the same time the newly created console won't create it's own stdout
|
||||
At the same time the newly created console won't create its own stdout
|
||||
and stderr handlers so they should not be redirected to DEVNULL.
|
||||
"""
|
||||
|
||||
|
|
@ -18,7 +18,7 @@ class LaunchFoundryAppsWindows(PreLaunchHook):
|
|||
|
||||
def execute(self):
|
||||
# Change `creationflags` to CREATE_NEW_CONSOLE
|
||||
# - on Windows will nuke create new window using it's console
|
||||
# - on Windows nuke will create new window using its console
|
||||
# Set `stdout` and `stderr` to None so new created console does not
|
||||
# have redirected output to DEVNULL in build
|
||||
self.launch_context.kwargs.update({
|
||||
|
|
|
|||
|
|
@ -84,11 +84,11 @@ class MainThreadItem:
|
|||
self.kwargs = kwargs
|
||||
|
||||
def execute(self):
|
||||
"""Execute callback and store it's result.
|
||||
"""Execute callback and store its result.
|
||||
|
||||
Method must be called from main thread. Item is marked as `done`
|
||||
when callback execution finished. Store output of callback of exception
|
||||
information when callback raise one.
|
||||
information when callback raises one.
|
||||
"""
|
||||
print("Executing process in main thread")
|
||||
if self.done:
|
||||
|
|
|
|||
|
|
@ -6,12 +6,13 @@ from openpype.pipeline.publish import get_errored_instances_from_context
|
|||
|
||||
|
||||
class SelectInvalidAction(pyblish.api.Action):
|
||||
"""Select invalid nodes in Maya when plug-in failed.
|
||||
"""Select invalid nodes in Fusion when plug-in failed.
|
||||
|
||||
To retrieve the invalid nodes this assumes a static `get_invalid()`
|
||||
method is available on the plugin.
|
||||
|
||||
"""
|
||||
|
||||
label = "Select invalid"
|
||||
on = "failed" # This action is only available on a failed plug-in
|
||||
icon = "search" # Icon from Awesome Icon
|
||||
|
|
@ -31,8 +32,10 @@ class SelectInvalidAction(pyblish.api.Action):
|
|||
if isinstance(invalid_nodes, (list, tuple)):
|
||||
invalid.extend(invalid_nodes)
|
||||
else:
|
||||
self.log.warning("Plug-in returned to be invalid, "
|
||||
"but has no selectable nodes.")
|
||||
self.log.warning(
|
||||
"Plug-in returned to be invalid, "
|
||||
"but has no selectable nodes."
|
||||
)
|
||||
|
||||
if not invalid:
|
||||
# Assume relevant comp is current comp and clear selection
|
||||
|
|
@ -51,4 +54,6 @@ class SelectInvalidAction(pyblish.api.Action):
|
|||
for tool in invalid:
|
||||
flow.Select(tool, True)
|
||||
names.add(tool.Name)
|
||||
self.log.info("Selecting invalid tools: %s" % ", ".join(sorted(names)))
|
||||
self.log.info(
|
||||
"Selecting invalid tools: %s" % ", ".join(sorted(names))
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ from openpype.tools.utils import host_tools
|
|||
from openpype.style import load_stylesheet
|
||||
from openpype.lib import register_event_callback
|
||||
from openpype.hosts.fusion.scripts import (
|
||||
set_rendermode,
|
||||
duplicate_with_inputs,
|
||||
)
|
||||
from openpype.hosts.fusion.api.lib import (
|
||||
|
|
@ -60,7 +59,6 @@ class OpenPypeMenu(QtWidgets.QWidget):
|
|||
publish_btn = QtWidgets.QPushButton("Publish...", self)
|
||||
manager_btn = QtWidgets.QPushButton("Manage...", self)
|
||||
libload_btn = QtWidgets.QPushButton("Library...", self)
|
||||
rendermode_btn = QtWidgets.QPushButton("Set render mode...", self)
|
||||
set_framerange_btn = QtWidgets.QPushButton("Set Frame Range", self)
|
||||
set_resolution_btn = QtWidgets.QPushButton("Set Resolution", self)
|
||||
duplicate_with_inputs_btn = QtWidgets.QPushButton(
|
||||
|
|
@ -91,7 +89,6 @@ class OpenPypeMenu(QtWidgets.QWidget):
|
|||
|
||||
layout.addWidget(set_framerange_btn)
|
||||
layout.addWidget(set_resolution_btn)
|
||||
layout.addWidget(rendermode_btn)
|
||||
|
||||
layout.addSpacing(20)
|
||||
|
||||
|
|
@ -108,7 +105,6 @@ class OpenPypeMenu(QtWidgets.QWidget):
|
|||
load_btn.clicked.connect(self.on_load_clicked)
|
||||
manager_btn.clicked.connect(self.on_manager_clicked)
|
||||
libload_btn.clicked.connect(self.on_libload_clicked)
|
||||
rendermode_btn.clicked.connect(self.on_rendermode_clicked)
|
||||
duplicate_with_inputs_btn.clicked.connect(
|
||||
self.on_duplicate_with_inputs_clicked
|
||||
)
|
||||
|
|
@ -162,15 +158,6 @@ class OpenPypeMenu(QtWidgets.QWidget):
|
|||
def on_libload_clicked(self):
|
||||
host_tools.show_library_loader()
|
||||
|
||||
def on_rendermode_clicked(self):
|
||||
if self.render_mode_widget is None:
|
||||
window = set_rendermode.SetRenderMode()
|
||||
window.setStyleSheet(load_stylesheet())
|
||||
window.show()
|
||||
self.render_mode_widget = window
|
||||
else:
|
||||
self.render_mode_widget.show()
|
||||
|
||||
def on_duplicate_with_inputs_clicked(self):
|
||||
duplicate_with_inputs.duplicate_with_input_connections()
|
||||
|
||||
|
|
|
|||
|
|
@ -4,29 +4,34 @@ import qtawesome
|
|||
|
||||
from openpype.hosts.fusion.api import (
|
||||
get_current_comp,
|
||||
comp_lock_and_undo_chunk
|
||||
comp_lock_and_undo_chunk,
|
||||
)
|
||||
|
||||
from openpype.lib import BoolDef
|
||||
from openpype.lib import (
|
||||
BoolDef,
|
||||
EnumDef,
|
||||
)
|
||||
from openpype.pipeline import (
|
||||
legacy_io,
|
||||
Creator,
|
||||
CreatedInstance
|
||||
CreatedInstance,
|
||||
)
|
||||
from openpype.client import (
|
||||
get_asset_by_name,
|
||||
)
|
||||
from openpype.client import get_asset_by_name
|
||||
|
||||
|
||||
class CreateSaver(Creator):
|
||||
identifier = "io.openpype.creators.fusion.saver"
|
||||
name = "saver"
|
||||
label = "Saver"
|
||||
label = "Render (saver)"
|
||||
name = "render"
|
||||
family = "render"
|
||||
default_variants = ["Main"]
|
||||
|
||||
default_variants = ["Main", "Mask"]
|
||||
description = "Fusion Saver to generate image sequence"
|
||||
|
||||
def create(self, subset_name, instance_data, pre_create_data):
|
||||
instance_attributes = ["reviewable"]
|
||||
|
||||
def create(self, subset_name, instance_data, pre_create_data):
|
||||
# TODO: Add pre_create attributes to choose file format?
|
||||
file_format = "OpenEXRFormat"
|
||||
|
||||
|
|
@ -58,7 +63,8 @@ class CreateSaver(Creator):
|
|||
family=self.family,
|
||||
subset_name=subset_name,
|
||||
data=instance_data,
|
||||
creator=self)
|
||||
creator=self,
|
||||
)
|
||||
|
||||
# Insert the transient data
|
||||
instance.transient_data["tool"] = saver
|
||||
|
|
@ -68,11 +74,9 @@ class CreateSaver(Creator):
|
|||
return instance
|
||||
|
||||
def collect_instances(self):
|
||||
|
||||
comp = get_current_comp()
|
||||
tools = comp.GetToolList(False, "Saver").values()
|
||||
for tool in tools:
|
||||
|
||||
data = self.get_managed_tool_data(tool)
|
||||
if not data:
|
||||
data = self._collect_unmanaged_saver(tool)
|
||||
|
|
@ -90,7 +94,6 @@ class CreateSaver(Creator):
|
|||
|
||||
def update_instances(self, update_list):
|
||||
for created_inst, _changes in update_list:
|
||||
|
||||
new_data = created_inst.data_to_store()
|
||||
tool = created_inst.transient_data["tool"]
|
||||
self._update_tool_with_data(tool, new_data)
|
||||
|
|
@ -139,7 +142,6 @@ class CreateSaver(Creator):
|
|||
tool.SetAttrs({"TOOLS_Name": subset})
|
||||
|
||||
def _collect_unmanaged_saver(self, tool):
|
||||
|
||||
# TODO: this should not be done this way - this should actually
|
||||
# get the data as stored on the tool explicitly (however)
|
||||
# that would disallow any 'regular saver' to be collected
|
||||
|
|
@ -153,8 +155,7 @@ class CreateSaver(Creator):
|
|||
asset = legacy_io.Session["AVALON_ASSET"]
|
||||
task = legacy_io.Session["AVALON_TASK"]
|
||||
|
||||
asset_doc = get_asset_by_name(project_name=project,
|
||||
asset_name=asset)
|
||||
asset_doc = get_asset_by_name(project_name=project, asset_name=asset)
|
||||
|
||||
path = tool["Clip"][comp.TIME_UNDEFINED]
|
||||
fname = os.path.basename(path)
|
||||
|
|
@ -178,21 +179,20 @@ class CreateSaver(Creator):
|
|||
"variant": variant,
|
||||
"active": not passthrough,
|
||||
"family": self.family,
|
||||
|
||||
# Unique identifier for instance and this creator
|
||||
"id": "pyblish.avalon.instance",
|
||||
"creator_identifier": self.identifier
|
||||
"creator_identifier": self.identifier,
|
||||
}
|
||||
|
||||
def get_managed_tool_data(self, tool):
|
||||
"""Return data of the tool if it matches creator identifier"""
|
||||
data = tool.GetData('openpype')
|
||||
data = tool.GetData("openpype")
|
||||
if not isinstance(data, dict):
|
||||
return
|
||||
|
||||
required = {
|
||||
"id": "pyblish.avalon.instance",
|
||||
"creator_identifier": self.identifier
|
||||
"creator_identifier": self.identifier,
|
||||
}
|
||||
for key, value in required.items():
|
||||
if key not in data or data[key] != value:
|
||||
|
|
@ -205,11 +205,40 @@ class CreateSaver(Creator):
|
|||
|
||||
return data
|
||||
|
||||
def get_instance_attr_defs(self):
|
||||
return [
|
||||
BoolDef(
|
||||
"review",
|
||||
default=True,
|
||||
label="Review"
|
||||
)
|
||||
def get_pre_create_attr_defs(self):
|
||||
"""Settings for create page"""
|
||||
attr_defs = [
|
||||
self._get_render_target_enum(),
|
||||
self._get_reviewable_bool(),
|
||||
]
|
||||
return attr_defs
|
||||
|
||||
def get_instance_attr_defs(self):
|
||||
"""Settings for publish page"""
|
||||
attr_defs = [
|
||||
self._get_render_target_enum(),
|
||||
self._get_reviewable_bool(),
|
||||
]
|
||||
return attr_defs
|
||||
|
||||
# These functions below should be moved to another file
|
||||
# so it can be used by other plugins. plugin.py ?
|
||||
|
||||
def _get_render_target_enum(self):
|
||||
rendering_targets = {
|
||||
"local": "Local machine rendering",
|
||||
"frames": "Use existing frames",
|
||||
}
|
||||
if "farm_rendering" in self.instance_attributes:
|
||||
rendering_targets["farm"] = "Farm rendering"
|
||||
|
||||
return EnumDef(
|
||||
"render_target", items=rendering_targets, label="Render target"
|
||||
)
|
||||
|
||||
def _get_reviewable_bool(self):
|
||||
return BoolDef(
|
||||
"review",
|
||||
default=("reviewable" in self.instance_attributes),
|
||||
label="Review",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
import pyblish.api
|
||||
from openpype.pipeline import publish
|
||||
import os
|
||||
|
||||
|
||||
class CollectFusionExpectedFrames(
|
||||
pyblish.api.InstancePlugin, publish.ColormanagedPyblishPluginMixin
|
||||
):
|
||||
"""Collect all frames needed to publish expected frames"""
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.5
|
||||
label = "Collect Expected Frames"
|
||||
hosts = ["fusion"]
|
||||
families = ["render"]
|
||||
|
||||
def process(self, instance):
|
||||
context = instance.context
|
||||
|
||||
frame_start = context.data["frameStartHandle"]
|
||||
frame_end = context.data["frameEndHandle"]
|
||||
path = instance.data["path"]
|
||||
output_dir = instance.data["outputDir"]
|
||||
|
||||
basename = os.path.basename(path)
|
||||
head, ext = os.path.splitext(basename)
|
||||
files = [
|
||||
f"{head}{str(frame).zfill(4)}{ext}"
|
||||
for frame in range(frame_start, frame_end + 1)
|
||||
]
|
||||
repre = {
|
||||
"name": ext[1:],
|
||||
"ext": ext[1:],
|
||||
"frameStart": f"%0{len(str(frame_end))}d" % frame_start,
|
||||
"files": files,
|
||||
"stagingDir": output_dir,
|
||||
}
|
||||
|
||||
self.set_representation_colorspace(
|
||||
representation=repre,
|
||||
context=context,
|
||||
)
|
||||
|
||||
# review representation
|
||||
if instance.data.get("review", False):
|
||||
repre["tags"] = ["review"]
|
||||
|
||||
# add the repre to the instance
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
instance.data["representations"].append(repre)
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
import pyblish.api
|
||||
|
||||
|
||||
class CollectFusionRenderMode(pyblish.api.InstancePlugin):
|
||||
"""Collect current comp's render Mode
|
||||
|
||||
Options:
|
||||
local
|
||||
farm
|
||||
|
||||
Note that this value is set for each comp separately. When you save the
|
||||
comp this information will be stored in that file. If for some reason the
|
||||
available tool does not visualize which render mode is set for the
|
||||
current comp, please run the following line in the console (Py2)
|
||||
|
||||
comp.GetData("openpype.rendermode")
|
||||
|
||||
This will return the name of the current render mode as seen above under
|
||||
Options.
|
||||
|
||||
"""
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.4
|
||||
label = "Collect Render Mode"
|
||||
hosts = ["fusion"]
|
||||
families = ["render"]
|
||||
|
||||
def process(self, instance):
|
||||
"""Collect all image sequence tools"""
|
||||
options = ["local", "farm"]
|
||||
|
||||
comp = instance.context.data.get("currentComp")
|
||||
if not comp:
|
||||
raise RuntimeError("No comp previously collected, unable to "
|
||||
"retrieve Fusion version.")
|
||||
|
||||
rendermode = comp.GetData("openpype.rendermode") or "local"
|
||||
assert rendermode in options, "Must be supported render mode"
|
||||
|
||||
self.log.info("Render mode: {0}".format(rendermode))
|
||||
|
||||
# Append family
|
||||
family = "render.{0}".format(rendermode)
|
||||
instance.data["families"].append(family)
|
||||
25
openpype/hosts/fusion/plugins/publish/collect_renders.py
Normal file
25
openpype/hosts/fusion/plugins/publish/collect_renders.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import pyblish.api
|
||||
|
||||
|
||||
class CollectFusionRenders(pyblish.api.InstancePlugin):
|
||||
"""Collect current saver node's render Mode
|
||||
|
||||
Options:
|
||||
local (Render locally)
|
||||
frames (Use existing frames)
|
||||
|
||||
"""
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.4
|
||||
label = "Collect Renders"
|
||||
hosts = ["fusion"]
|
||||
families = ["render"]
|
||||
|
||||
def process(self, instance):
|
||||
render_target = instance.data["render_target"]
|
||||
family = instance.data["family"]
|
||||
|
||||
# add targeted family to families
|
||||
instance.data["families"].append(
|
||||
"{}.{}".format(family, render_target)
|
||||
)
|
||||
109
openpype/hosts/fusion/plugins/publish/extract_render_local.py
Normal file
109
openpype/hosts/fusion/plugins/publish/extract_render_local.py
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import logging
|
||||
import contextlib
|
||||
import pyblish.api
|
||||
from openpype.hosts.fusion.api import comp_lock_and_undo_chunk
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def enabled_savers(comp, savers):
|
||||
"""Enable only the `savers` in Comp during the context.
|
||||
|
||||
Any Saver tool in the passed composition that is not in the savers list
|
||||
will be set to passthrough during the context.
|
||||
|
||||
Args:
|
||||
comp (object): Fusion composition object.
|
||||
savers (list): List of Saver tool objects.
|
||||
|
||||
"""
|
||||
passthrough_key = "TOOLB_PassThrough"
|
||||
original_states = {}
|
||||
enabled_save_names = {saver.Name for saver in savers}
|
||||
try:
|
||||
all_savers = comp.GetToolList(False, "Saver").values()
|
||||
for saver in all_savers:
|
||||
original_state = saver.GetAttrs()[passthrough_key]
|
||||
original_states[saver] = original_state
|
||||
|
||||
# The passthrough state we want to set (passthrough != enabled)
|
||||
state = saver.Name not in enabled_save_names
|
||||
if state != original_state:
|
||||
saver.SetAttrs({passthrough_key: state})
|
||||
yield
|
||||
finally:
|
||||
for saver, original_state in original_states.items():
|
||||
saver.SetAttrs({"TOOLB_PassThrough": original_state})
|
||||
|
||||
|
||||
class FusionRenderLocal(pyblish.api.InstancePlugin):
|
||||
"""Render the current Fusion composition locally."""
|
||||
|
||||
order = pyblish.api.ExtractorOrder - 0.2
|
||||
label = "Render Local"
|
||||
hosts = ["fusion"]
|
||||
families = ["render.local"]
|
||||
|
||||
def process(self, instance):
|
||||
context = instance.context
|
||||
|
||||
# Start render
|
||||
self.render_once(context)
|
||||
|
||||
# Log render status
|
||||
self.log.info(
|
||||
"Rendered '{nm}' for asset '{ast}' under the task '{tsk}'".format(
|
||||
nm=instance.data["name"],
|
||||
ast=instance.data["asset"],
|
||||
tsk=instance.data["task"],
|
||||
)
|
||||
)
|
||||
|
||||
def render_once(self, context):
|
||||
"""Render context comp only once, even with more render instances"""
|
||||
|
||||
# This plug-in assumes all render nodes get rendered at the same time
|
||||
# to speed up the rendering. The check below makes sure that we only
|
||||
# execute the rendering once and not for each instance.
|
||||
key = f"__hasRun{self.__class__.__name__}"
|
||||
|
||||
savers_to_render = [
|
||||
# Get the saver tool from the instance
|
||||
instance[0] for instance in context if
|
||||
# Only active instances
|
||||
instance.data.get("publish", True) and
|
||||
# Only render.local instances
|
||||
"render.local" in instance.data["families"]
|
||||
]
|
||||
|
||||
if key not in context.data:
|
||||
# We initialize as false to indicate it wasn't successful yet
|
||||
# so we can keep track of whether Fusion succeeded
|
||||
context.data[key] = False
|
||||
|
||||
current_comp = context.data["currentComp"]
|
||||
frame_start = context.data["frameStartHandle"]
|
||||
frame_end = context.data["frameEndHandle"]
|
||||
|
||||
self.log.info("Starting Fusion render")
|
||||
self.log.info(f"Start frame: {frame_start}")
|
||||
self.log.info(f"End frame: {frame_end}")
|
||||
saver_names = ", ".join(saver.Name for saver in savers_to_render)
|
||||
self.log.info(f"Rendering tools: {saver_names}")
|
||||
|
||||
with comp_lock_and_undo_chunk(current_comp):
|
||||
with enabled_savers(current_comp, savers_to_render):
|
||||
result = current_comp.Render(
|
||||
{
|
||||
"Start": frame_start,
|
||||
"End": frame_end,
|
||||
"Wait": True,
|
||||
}
|
||||
)
|
||||
|
||||
context.data[key] = bool(result)
|
||||
|
||||
if context.data[key] is False:
|
||||
raise RuntimeError("Comp render failed")
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
import os
|
||||
import pyblish.api
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.fusion.api import comp_lock_and_undo_chunk
|
||||
|
||||
|
||||
class Fusionlocal(pyblish.api.InstancePlugin,
|
||||
publish.ColormanagedPyblishPluginMixin):
|
||||
"""Render the current Fusion composition locally.
|
||||
|
||||
Extract the result of savers by starting a comp render
|
||||
This will run the local render of Fusion.
|
||||
|
||||
"""
|
||||
|
||||
order = pyblish.api.ExtractorOrder - 0.1
|
||||
label = "Render Local"
|
||||
hosts = ["fusion"]
|
||||
families = ["render.local"]
|
||||
|
||||
def process(self, instance):
|
||||
context = instance.context
|
||||
|
||||
# Start render
|
||||
self.render_once(context)
|
||||
|
||||
# Log render status
|
||||
self.log.info(
|
||||
"Rendered '{nm}' for asset '{ast}' under the task '{tsk}'".format(
|
||||
nm=instance.data["name"],
|
||||
ast=instance.data["asset"],
|
||||
tsk=instance.data["task"],
|
||||
)
|
||||
)
|
||||
|
||||
frame_start = context.data["frameStartHandle"]
|
||||
frame_end = context.data["frameEndHandle"]
|
||||
path = instance.data["path"]
|
||||
output_dir = instance.data["outputDir"]
|
||||
|
||||
basename = os.path.basename(path)
|
||||
head, ext = os.path.splitext(basename)
|
||||
files = [
|
||||
f"{head}{str(frame).zfill(4)}{ext}"
|
||||
for frame in range(frame_start, frame_end + 1)
|
||||
]
|
||||
repre = {
|
||||
"name": ext[1:],
|
||||
"ext": ext[1:],
|
||||
"frameStart": f"%0{len(str(frame_end))}d" % frame_start,
|
||||
"files": files,
|
||||
"stagingDir": output_dir,
|
||||
}
|
||||
|
||||
self.set_representation_colorspace(
|
||||
representation=repre,
|
||||
context=context,
|
||||
)
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
instance.data["representations"].append(repre)
|
||||
|
||||
# review representation
|
||||
if instance.data.get("review", False):
|
||||
repre["tags"] = ["review", "ftrackreview"]
|
||||
|
||||
def render_once(self, context):
|
||||
"""Render context comp only once, even with more render instances"""
|
||||
|
||||
# This plug-in assumes all render nodes get rendered at the same time
|
||||
# to speed up the rendering. The check below makes sure that we only
|
||||
# execute the rendering once and not for each instance.
|
||||
key = f"__hasRun{self.__class__.__name__}"
|
||||
if key not in context.data:
|
||||
# We initialize as false to indicate it wasn't successful yet
|
||||
# so we can keep track of whether Fusion succeeded
|
||||
context.data[key] = False
|
||||
|
||||
current_comp = context.data["currentComp"]
|
||||
frame_start = context.data["frameStartHandle"]
|
||||
frame_end = context.data["frameEndHandle"]
|
||||
|
||||
self.log.info("Starting Fusion render")
|
||||
self.log.info(f"Start frame: {frame_start}")
|
||||
self.log.info(f"End frame: {frame_end}")
|
||||
|
||||
with comp_lock_and_undo_chunk(current_comp):
|
||||
result = current_comp.Render(
|
||||
{
|
||||
"Start": frame_start,
|
||||
"End": frame_end,
|
||||
"Wait": True,
|
||||
}
|
||||
)
|
||||
|
||||
context.data[key] = bool(result)
|
||||
|
||||
if context.data[key] is False:
|
||||
raise RuntimeError("Comp render failed")
|
||||
|
|
@ -14,22 +14,19 @@ class ValidateCreateFolderChecked(pyblish.api.InstancePlugin):
|
|||
"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
actions = [RepairAction]
|
||||
label = "Validate Create Folder Checked"
|
||||
families = ["render"]
|
||||
hosts = ["fusion"]
|
||||
actions = [SelectInvalidAction]
|
||||
actions = [RepairAction, SelectInvalidAction]
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance):
|
||||
active = instance.data.get("active", instance.data.get("publish"))
|
||||
if not active:
|
||||
return []
|
||||
|
||||
tool = instance[0]
|
||||
create_dir = tool.GetInput("CreateDir")
|
||||
if create_dir == 0.0:
|
||||
cls.log.error("%s has Create Folder turned off" % instance[0].Name)
|
||||
cls.log.error(
|
||||
"%s has Create Folder turned off" % instance[0].Name
|
||||
)
|
||||
return [tool]
|
||||
|
||||
def process(self, instance):
|
||||
|
|
@ -37,7 +34,8 @@ class ValidateCreateFolderChecked(pyblish.api.InstancePlugin):
|
|||
if invalid:
|
||||
raise PublishValidationError(
|
||||
"Found Saver with Create Folder During Render checked off",
|
||||
title=self.label)
|
||||
title=self.label,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def repair(cls, instance):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
import os
|
||||
import pyblish.api
|
||||
|
||||
from openpype.pipeline.publish import RepairAction
|
||||
from openpype.pipeline import PublishValidationError
|
||||
|
||||
from openpype.hosts.fusion.api.action import SelectInvalidAction
|
||||
|
||||
|
||||
class ValidateLocalFramesExistence(pyblish.api.InstancePlugin):
|
||||
"""Checks if files for savers that's set
|
||||
to publish expected frames exists
|
||||
"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
label = "Validate Expected Frames Exists"
|
||||
families = ["render"]
|
||||
hosts = ["fusion"]
|
||||
actions = [RepairAction, SelectInvalidAction]
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance, non_existing_frames=None):
|
||||
if non_existing_frames is None:
|
||||
non_existing_frames = []
|
||||
|
||||
if instance.data.get("render_target") == "frames":
|
||||
tool = instance[0]
|
||||
|
||||
frame_start = instance.data["frameStart"]
|
||||
frame_end = instance.data["frameEnd"]
|
||||
path = instance.data["path"]
|
||||
output_dir = instance.data["outputDir"]
|
||||
|
||||
basename = os.path.basename(path)
|
||||
head, ext = os.path.splitext(basename)
|
||||
files = [
|
||||
f"{head}{str(frame).zfill(4)}{ext}"
|
||||
for frame in range(frame_start, frame_end + 1)
|
||||
]
|
||||
|
||||
for file in files:
|
||||
if not os.path.exists(os.path.join(output_dir, file)):
|
||||
cls.log.error(
|
||||
f"Missing file: {os.path.join(output_dir, file)}"
|
||||
)
|
||||
non_existing_frames.append(file)
|
||||
|
||||
if len(non_existing_frames) > 0:
|
||||
cls.log.error(f"Some of {tool.Name}'s files does not exist")
|
||||
return [tool]
|
||||
|
||||
def process(self, instance):
|
||||
non_existing_frames = []
|
||||
invalid = self.get_invalid(instance, non_existing_frames)
|
||||
if invalid:
|
||||
raise PublishValidationError(
|
||||
"{} is set to publish existing frames but "
|
||||
"some frames are missing. "
|
||||
"The missing file(s) are:\n\n{}".format(
|
||||
invalid[0].Name,
|
||||
"\n\n".join(non_existing_frames),
|
||||
),
|
||||
title=self.label,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def repair(cls, instance):
|
||||
invalid = cls.get_invalid(instance)
|
||||
if invalid:
|
||||
tool = invalid[0]
|
||||
|
||||
# Change render target to local to render locally
|
||||
tool.SetData("openpype.creator_attributes.render_target", "local")
|
||||
|
||||
cls.log.info(
|
||||
f"Reload the publisher and {tool.Name} "
|
||||
"will be set to render locally"
|
||||
)
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
from qtpy import QtWidgets
|
||||
import qtawesome
|
||||
from openpype.hosts.fusion.api import get_current_comp
|
||||
|
||||
|
||||
_help = {"local": "Render the comp on your own machine and publish "
|
||||
"it from that the destination folder",
|
||||
"farm": "Submit a Fusion render job to a Render farm to use all other"
|
||||
" computers and add a publish job"}
|
||||
|
||||
|
||||
class SetRenderMode(QtWidgets.QWidget):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QtWidgets.QWidget.__init__(self, parent)
|
||||
|
||||
self._comp = get_current_comp()
|
||||
self._comp_name = self._get_comp_name()
|
||||
|
||||
self.setWindowTitle("Set Render Mode")
|
||||
self.setFixedSize(300, 175)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
|
||||
# region comp info
|
||||
comp_info_layout = QtWidgets.QHBoxLayout()
|
||||
|
||||
update_btn = QtWidgets.QPushButton(qtawesome.icon("fa.refresh",
|
||||
color="white"), "")
|
||||
update_btn.setFixedWidth(25)
|
||||
update_btn.setFixedHeight(25)
|
||||
|
||||
comp_information = QtWidgets.QLineEdit()
|
||||
comp_information.setEnabled(False)
|
||||
|
||||
comp_info_layout.addWidget(comp_information)
|
||||
comp_info_layout.addWidget(update_btn)
|
||||
# endregion comp info
|
||||
|
||||
# region modes
|
||||
mode_options = QtWidgets.QComboBox()
|
||||
mode_options.addItems(_help.keys())
|
||||
|
||||
mode_information = QtWidgets.QTextEdit()
|
||||
mode_information.setReadOnly(True)
|
||||
# endregion modes
|
||||
|
||||
accept_btn = QtWidgets.QPushButton("Accept")
|
||||
|
||||
layout.addLayout(comp_info_layout)
|
||||
layout.addWidget(mode_options)
|
||||
layout.addWidget(mode_information)
|
||||
layout.addWidget(accept_btn)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
self.comp_information = comp_information
|
||||
self.update_btn = update_btn
|
||||
|
||||
self.mode_options = mode_options
|
||||
self.mode_information = mode_information
|
||||
|
||||
self.accept_btn = accept_btn
|
||||
|
||||
self.connections()
|
||||
self.update()
|
||||
|
||||
# Force updated render mode help text
|
||||
self._update_rendermode_info()
|
||||
|
||||
def connections(self):
|
||||
"""Build connections between code and buttons"""
|
||||
|
||||
self.update_btn.clicked.connect(self.update)
|
||||
self.accept_btn.clicked.connect(self._set_comp_rendermode)
|
||||
self.mode_options.currentIndexChanged.connect(
|
||||
self._update_rendermode_info)
|
||||
|
||||
def update(self):
|
||||
"""Update all information in the UI"""
|
||||
|
||||
self._comp = get_current_comp()
|
||||
self._comp_name = self._get_comp_name()
|
||||
self.comp_information.setText(self._comp_name)
|
||||
|
||||
# Update current comp settings
|
||||
mode = self._get_comp_rendermode()
|
||||
index = self.mode_options.findText(mode)
|
||||
self.mode_options.setCurrentIndex(index)
|
||||
|
||||
def _update_rendermode_info(self):
|
||||
rendermode = self.mode_options.currentText()
|
||||
self.mode_information.setText(_help[rendermode])
|
||||
|
||||
def _get_comp_name(self):
|
||||
return self._comp.GetAttrs("COMPS_Name")
|
||||
|
||||
def _get_comp_rendermode(self):
|
||||
return self._comp.GetData("openpype.rendermode") or "local"
|
||||
|
||||
def _set_comp_rendermode(self):
|
||||
rendermode = self.mode_options.currentText()
|
||||
self._comp.SetData("openpype.rendermode", rendermode)
|
||||
|
||||
self._comp.Print("Updated render mode to '%s'\n" % rendermode)
|
||||
self.hide()
|
||||
|
||||
def _validation(self):
|
||||
ui_mode = self.mode_options.currentText()
|
||||
comp_mode = self._get_comp_rendermode()
|
||||
|
||||
return comp_mode == ui_mode
|
||||
|
|
@ -6,6 +6,11 @@ from pymxs import runtime as rt
|
|||
from typing import Union
|
||||
import contextlib
|
||||
|
||||
from openpype.pipeline.context_tools import (
|
||||
get_current_project_asset,
|
||||
get_current_project
|
||||
)
|
||||
|
||||
|
||||
JSON_PREFIX = "JSON::"
|
||||
|
||||
|
|
@ -157,6 +162,60 @@ def get_multipass_setting(project_setting=None):
|
|||
["multipass"])
|
||||
|
||||
|
||||
def set_scene_resolution(width: int, height: int):
|
||||
"""Set the render resolution
|
||||
|
||||
Args:
|
||||
width(int): value of the width
|
||||
height(int): value of the height
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
rt.renderWidth = width
|
||||
rt.renderHeight = height
|
||||
|
||||
|
||||
def reset_scene_resolution():
|
||||
"""Apply the scene resolution from the project definition
|
||||
|
||||
scene resolution can be overwritten by an asset if the asset.data contains
|
||||
any information regarding scene resolution .
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
data = ["data.resolutionWidth", "data.resolutionHeight"]
|
||||
project_resolution = get_current_project(fields=data)
|
||||
project_resolution_data = project_resolution["data"]
|
||||
asset_resolution = get_current_project_asset(fields=data)
|
||||
asset_resolution_data = asset_resolution["data"]
|
||||
# Set project resolution
|
||||
project_width = int(project_resolution_data.get("resolutionWidth", 1920))
|
||||
project_height = int(project_resolution_data.get("resolutionHeight", 1080))
|
||||
width = int(asset_resolution_data.get("resolutionWidth", project_width))
|
||||
height = int(asset_resolution_data.get("resolutionHeight", project_height))
|
||||
|
||||
set_scene_resolution(width, height)
|
||||
|
||||
|
||||
def set_context_setting():
|
||||
"""Apply the project settings from the project definition
|
||||
|
||||
Settings can be overwritten by an asset if the asset.data contains
|
||||
any information regarding those settings.
|
||||
|
||||
Examples of settings:
|
||||
frame range
|
||||
resolution
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
reset_scene_resolution()
|
||||
|
||||
|
||||
def get_max_version():
|
||||
"""
|
||||
Args:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from qtpy import QtWidgets, QtCore
|
|||
from pymxs import runtime as rt
|
||||
|
||||
from openpype.tools.utils import host_tools
|
||||
|
||||
from openpype.hosts.max.api import lib
|
||||
|
||||
class OpenPypeMenu(object):
|
||||
"""Object representing OpenPype menu.
|
||||
|
|
@ -107,6 +107,13 @@ class OpenPypeMenu(object):
|
|||
workfiles_action = QtWidgets.QAction("Work Files...", openpype_menu)
|
||||
workfiles_action.triggered.connect(self.workfiles_callback)
|
||||
openpype_menu.addAction(workfiles_action)
|
||||
|
||||
openpype_menu.addSeparator()
|
||||
|
||||
res_action = QtWidgets.QAction("Set Resolution", openpype_menu)
|
||||
res_action.triggered.connect(self.resolution_callback)
|
||||
openpype_menu.addAction(res_action)
|
||||
|
||||
return openpype_menu
|
||||
|
||||
def load_callback(self):
|
||||
|
|
@ -128,3 +135,7 @@ class OpenPypeMenu(object):
|
|||
def workfiles_callback(self):
|
||||
"""Callback to show Workfiles tool."""
|
||||
host_tools.show_workfiles(parent=self.main_widget)
|
||||
|
||||
def resolution_callback(self):
|
||||
"""Callback to reset scene resolution"""
|
||||
return lib.reset_scene_resolution()
|
||||
|
|
|
|||
|
|
@ -50,6 +50,11 @@ class MaxHost(HostBase, IWorkfileHost, ILoadHost, INewPublisher):
|
|||
|
||||
self._has_been_setup = True
|
||||
|
||||
def context_setting():
|
||||
return lib.set_context_setting()
|
||||
rt.callbacks.addScript(rt.Name('systemPostNew'),
|
||||
context_setting)
|
||||
|
||||
def has_unsaved_changes(self):
|
||||
# TODO: how to get it from 3dsmax?
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ class CreateReview(plugin.Creator):
|
|||
"alpha cut"
|
||||
]
|
||||
useMayaTimeline = True
|
||||
panZoom = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CreateReview, self).__init__(*args, **kwargs)
|
||||
|
|
@ -45,5 +46,6 @@ class CreateReview(plugin.Creator):
|
|||
data["keepImages"] = self.keepImages
|
||||
data["imagePlane"] = self.imagePlane
|
||||
data["transparency"] = self.transparency
|
||||
data["panZoom"] = self.panZoom
|
||||
|
||||
self.data = data
|
||||
|
|
|
|||
|
|
@ -79,6 +79,8 @@ class CollectReview(pyblish.api.InstancePlugin):
|
|||
data['review_width'] = instance.data['review_width']
|
||||
data['review_height'] = instance.data['review_height']
|
||||
data["isolate"] = instance.data["isolate"]
|
||||
data["panZoom"] = instance.data.get("panZoom", False)
|
||||
data["panel"] = instance.data["panel"]
|
||||
cmds.setAttr(str(instance) + '.active', 1)
|
||||
self.log.debug('data {}'.format(instance.context[i].data))
|
||||
instance.context[i].data.update(data)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import os
|
||||
import json
|
||||
import contextlib
|
||||
|
||||
import clique
|
||||
import capture
|
||||
|
|
@ -11,6 +12,16 @@ from maya import cmds
|
|||
import pymel.core as pm
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def panel_camera(panel, camera):
|
||||
original_camera = cmds.modelPanel(panel, query=True, camera=True)
|
||||
try:
|
||||
cmds.modelPanel(panel, edit=True, camera=camera)
|
||||
yield
|
||||
finally:
|
||||
cmds.modelPanel(panel, edit=True, camera=original_camera)
|
||||
|
||||
|
||||
class ExtractPlayblast(publish.Extractor):
|
||||
"""Extract viewport playblast.
|
||||
|
||||
|
|
@ -25,6 +36,16 @@ class ExtractPlayblast(publish.Extractor):
|
|||
optional = True
|
||||
capture_preset = {}
|
||||
|
||||
def _capture(self, preset):
|
||||
self.log.info(
|
||||
"Using preset:\n{}".format(
|
||||
json.dumps(preset, sort_keys=True, indent=4)
|
||||
)
|
||||
)
|
||||
|
||||
path = capture.capture(log=self.log, **preset)
|
||||
self.log.debug("playblast path {}".format(path))
|
||||
|
||||
def process(self, instance):
|
||||
self.log.info("Extracting capture..")
|
||||
|
||||
|
|
@ -43,7 +64,7 @@ class ExtractPlayblast(publish.Extractor):
|
|||
self.log.info("start: {}, end: {}".format(start, end))
|
||||
|
||||
# get cameras
|
||||
camera = instance.data['review_camera']
|
||||
camera = instance.data["review_camera"]
|
||||
|
||||
preset = lib.load_capture_preset(data=self.capture_preset)
|
||||
# Grab capture presets from the project settings
|
||||
|
|
@ -57,23 +78,23 @@ class ExtractPlayblast(publish.Extractor):
|
|||
asset_height = asset_data.get("resolutionHeight")
|
||||
review_instance_width = instance.data.get("review_width")
|
||||
review_instance_height = instance.data.get("review_height")
|
||||
preset['camera'] = camera
|
||||
preset["camera"] = camera
|
||||
|
||||
# Tests if project resolution is set,
|
||||
# if it is a value other than zero, that value is
|
||||
# used, if not then the asset resolution is
|
||||
# used
|
||||
if review_instance_width and review_instance_height:
|
||||
preset['width'] = review_instance_width
|
||||
preset['height'] = review_instance_height
|
||||
preset["width"] = review_instance_width
|
||||
preset["height"] = review_instance_height
|
||||
elif width_preset and height_preset:
|
||||
preset['width'] = width_preset
|
||||
preset['height'] = height_preset
|
||||
preset["width"] = width_preset
|
||||
preset["height"] = height_preset
|
||||
elif asset_width and asset_height:
|
||||
preset['width'] = asset_width
|
||||
preset['height'] = asset_height
|
||||
preset['start_frame'] = start
|
||||
preset['end_frame'] = end
|
||||
preset["width"] = asset_width
|
||||
preset["height"] = asset_height
|
||||
preset["start_frame"] = start
|
||||
preset["end_frame"] = end
|
||||
|
||||
# Enforce persisting camera depth of field
|
||||
camera_options = preset.setdefault("camera_options", {})
|
||||
|
|
@ -86,8 +107,8 @@ class ExtractPlayblast(publish.Extractor):
|
|||
|
||||
self.log.info("Outputting images to %s" % path)
|
||||
|
||||
preset['filename'] = path
|
||||
preset['overwrite'] = True
|
||||
preset["filename"] = path
|
||||
preset["overwrite"] = True
|
||||
|
||||
pm.refresh(f=True)
|
||||
|
||||
|
|
@ -114,7 +135,8 @@ class ExtractPlayblast(publish.Extractor):
|
|||
|
||||
# Disable Pan/Zoom.
|
||||
pan_zoom = cmds.getAttr("{}.panZoomEnabled".format(preset["camera"]))
|
||||
cmds.setAttr("{}.panZoomEnabled".format(preset["camera"]), False)
|
||||
preset.pop("pan_zoom", None)
|
||||
preset["camera_options"]["panZoomEnabled"] = instance.data["panZoom"]
|
||||
|
||||
# Need to explicitly enable some viewport changes so the viewport is
|
||||
# refreshed ahead of playblasting.
|
||||
|
|
@ -136,30 +158,39 @@ class ExtractPlayblast(publish.Extractor):
|
|||
)
|
||||
|
||||
override_viewport_options = (
|
||||
capture_presets['Viewport Options']['override_viewport_options']
|
||||
capture_presets["Viewport Options"]["override_viewport_options"]
|
||||
)
|
||||
with lib.maintained_time():
|
||||
filename = preset.get("filename", "%TEMP%")
|
||||
|
||||
# Force viewer to False in call to capture because we have our own
|
||||
# viewer opening call to allow a signal to trigger between
|
||||
# playblast and viewer
|
||||
preset['viewer'] = False
|
||||
# Force viewer to False in call to capture because we have our own
|
||||
# viewer opening call to allow a signal to trigger between
|
||||
# playblast and viewer
|
||||
preset["viewer"] = False
|
||||
|
||||
# Update preset with current panel setting
|
||||
# if override_viewport_options is turned off
|
||||
if not override_viewport_options:
|
||||
panel_preset = capture.parse_view(instance.data["panel"])
|
||||
panel_preset.pop("camera")
|
||||
preset.update(panel_preset)
|
||||
# Update preset with current panel setting
|
||||
# if override_viewport_options is turned off
|
||||
if not override_viewport_options:
|
||||
panel_preset = capture.parse_view(instance.data["panel"])
|
||||
panel_preset.pop("camera")
|
||||
preset.update(panel_preset)
|
||||
|
||||
self.log.info(
|
||||
"Using preset:\n{}".format(
|
||||
json.dumps(preset, sort_keys=True, indent=4)
|
||||
# Need to ensure Python 2 compatibility.
|
||||
# TODO: Remove once dropping Python 2.
|
||||
if getattr(contextlib, "nested", None):
|
||||
# Python 3 compatibility.
|
||||
with contextlib.nested(
|
||||
lib.maintained_time(),
|
||||
panel_camera(instance.data["panel"], preset["camera"])
|
||||
):
|
||||
self._capture(preset)
|
||||
else:
|
||||
# Python 2 compatibility.
|
||||
with contextlib.ExitStack() as stack:
|
||||
stack.enter_context(lib.maintained_time())
|
||||
stack.enter_context(
|
||||
panel_camera(instance.data["panel"], preset["camera"])
|
||||
)
|
||||
)
|
||||
|
||||
path = capture.capture(log=self.log, **preset)
|
||||
self._capture(preset)
|
||||
|
||||
# Restoring viewport options.
|
||||
if viewport_defaults:
|
||||
|
|
@ -169,18 +200,17 @@ class ExtractPlayblast(publish.Extractor):
|
|||
|
||||
cmds.setAttr("{}.panZoomEnabled".format(preset["camera"]), pan_zoom)
|
||||
|
||||
self.log.debug("playblast path {}".format(path))
|
||||
|
||||
collected_files = os.listdir(stagingdir)
|
||||
patterns = [clique.PATTERNS["frames"]]
|
||||
collections, remainder = clique.assemble(collected_files,
|
||||
minimum_items=1,
|
||||
patterns=patterns)
|
||||
|
||||
filename = preset.get("filename", "%TEMP%")
|
||||
self.log.debug("filename {}".format(filename))
|
||||
frame_collection = None
|
||||
for collection in collections:
|
||||
filebase = collection.format('{head}').rstrip(".")
|
||||
filebase = collection.format("{head}").rstrip(".")
|
||||
self.log.debug("collection head {}".format(filebase))
|
||||
if filebase in filename:
|
||||
frame_collection = collection
|
||||
|
|
@ -204,15 +234,15 @@ class ExtractPlayblast(publish.Extractor):
|
|||
collected_files = collected_files[0]
|
||||
|
||||
representation = {
|
||||
'name': 'png',
|
||||
'ext': 'png',
|
||||
'files': collected_files,
|
||||
"name": self.capture_preset["Codec"]["compression"],
|
||||
"ext": self.capture_preset["Codec"]["compression"],
|
||||
"files": collected_files,
|
||||
"stagingDir": stagingdir,
|
||||
"frameStart": start,
|
||||
"frameEnd": end,
|
||||
'fps': fps,
|
||||
'preview': True,
|
||||
'tags': tags,
|
||||
'camera_name': camera_node_name
|
||||
"fps": fps,
|
||||
"preview": True,
|
||||
"tags": tags,
|
||||
"camera_name": camera_node_name
|
||||
}
|
||||
instance.data["representations"].append(representation)
|
||||
|
|
|
|||
|
|
@ -26,28 +26,28 @@ class ExtractThumbnail(publish.Extractor):
|
|||
def process(self, instance):
|
||||
self.log.info("Extracting capture..")
|
||||
|
||||
camera = instance.data['review_camera']
|
||||
camera = instance.data["review_camera"]
|
||||
|
||||
capture_preset = (
|
||||
instance.context.data["project_settings"]['maya']['publish']['ExtractPlayblast']['capture_preset']
|
||||
)
|
||||
maya_setting = instance.context.data["project_settings"]["maya"]
|
||||
plugin_setting = maya_setting["publish"]["ExtractPlayblast"]
|
||||
capture_preset = plugin_setting["capture_preset"]
|
||||
override_viewport_options = (
|
||||
capture_preset['Viewport Options']['override_viewport_options']
|
||||
capture_preset["Viewport Options"]["override_viewport_options"]
|
||||
)
|
||||
|
||||
try:
|
||||
preset = lib.load_capture_preset(data=capture_preset)
|
||||
except KeyError as ke:
|
||||
self.log.error('Error loading capture presets: {}'.format(str(ke)))
|
||||
self.log.error("Error loading capture presets: {}".format(str(ke)))
|
||||
preset = {}
|
||||
self.log.info('Using viewport preset: {}'.format(preset))
|
||||
self.log.info("Using viewport preset: {}".format(preset))
|
||||
|
||||
# preset["off_screen"] = False
|
||||
|
||||
preset['camera'] = camera
|
||||
preset['start_frame'] = instance.data["frameStart"]
|
||||
preset['end_frame'] = instance.data["frameStart"]
|
||||
preset['camera_options'] = {
|
||||
preset["camera"] = camera
|
||||
preset["start_frame"] = instance.data["frameStart"]
|
||||
preset["end_frame"] = instance.data["frameStart"]
|
||||
preset["camera_options"] = {
|
||||
"displayGateMask": False,
|
||||
"displayResolution": False,
|
||||
"displayFilmGate": False,
|
||||
|
|
@ -74,14 +74,14 @@ class ExtractThumbnail(publish.Extractor):
|
|||
# used, if not then the asset resolution is
|
||||
# used
|
||||
if review_instance_width and review_instance_height:
|
||||
preset['width'] = review_instance_width
|
||||
preset['height'] = review_instance_height
|
||||
preset["width"] = review_instance_width
|
||||
preset["height"] = review_instance_height
|
||||
elif width_preset and height_preset:
|
||||
preset['width'] = width_preset
|
||||
preset['height'] = height_preset
|
||||
preset["width"] = width_preset
|
||||
preset["height"] = height_preset
|
||||
elif asset_width and asset_height:
|
||||
preset['width'] = asset_width
|
||||
preset['height'] = asset_height
|
||||
preset["width"] = asset_width
|
||||
preset["height"] = asset_height
|
||||
|
||||
# Create temp directory for thumbnail
|
||||
# - this is to avoid "override" of source file
|
||||
|
|
@ -96,8 +96,8 @@ class ExtractThumbnail(publish.Extractor):
|
|||
|
||||
self.log.info("Outputting images to %s" % path)
|
||||
|
||||
preset['filename'] = path
|
||||
preset['overwrite'] = True
|
||||
preset["filename"] = path
|
||||
preset["overwrite"] = True
|
||||
|
||||
pm.refresh(f=True)
|
||||
|
||||
|
|
@ -123,14 +123,14 @@ class ExtractThumbnail(publish.Extractor):
|
|||
preset["viewport_options"] = {"imagePlane": image_plane}
|
||||
|
||||
# Disable Pan/Zoom.
|
||||
pan_zoom = cmds.getAttr("{}.panZoomEnabled".format(preset["camera"]))
|
||||
cmds.setAttr("{}.panZoomEnabled".format(preset["camera"]), False)
|
||||
preset.pop("pan_zoom", None)
|
||||
preset["camera_options"]["panZoomEnabled"] = instance.data["panZoom"]
|
||||
|
||||
with lib.maintained_time():
|
||||
# Force viewer to False in call to capture because we have our own
|
||||
# viewer opening call to allow a signal to trigger between
|
||||
# playblast and viewer
|
||||
preset['viewer'] = False
|
||||
preset["viewer"] = False
|
||||
|
||||
# Update preset with current panel setting
|
||||
# if override_viewport_options is turned off
|
||||
|
|
@ -145,17 +145,15 @@ class ExtractThumbnail(publish.Extractor):
|
|||
|
||||
_, thumbnail = os.path.split(playblast)
|
||||
|
||||
cmds.setAttr("{}.panZoomEnabled".format(preset["camera"]), pan_zoom)
|
||||
|
||||
self.log.info("file list {}".format(thumbnail))
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
||||
representation = {
|
||||
'name': 'thumbnail',
|
||||
'ext': 'jpg',
|
||||
'files': thumbnail,
|
||||
"name": "thumbnail",
|
||||
"ext": "jpg",
|
||||
"files": thumbnail,
|
||||
"stagingDir": dst_staging,
|
||||
"thumbnail": True
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,22 +54,19 @@ class LoadBackdropNodes(load.LoaderPlugin):
|
|||
version = context['version']
|
||||
version_data = version.get("data", {})
|
||||
vname = version.get("name", None)
|
||||
first = version_data.get("frameStart", None)
|
||||
last = version_data.get("frameEnd", None)
|
||||
namespace = namespace or context['asset']['name']
|
||||
colorspace = version_data.get("colorspace", None)
|
||||
object_name = "{}_{}".format(name, namespace)
|
||||
|
||||
# prepare data for imprinting
|
||||
# add additional metadata from the version to imprint to Avalon knob
|
||||
add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd",
|
||||
"source", "author", "fps"]
|
||||
add_keys = ["source", "author", "fps"]
|
||||
|
||||
data_imprint = {"frameStart": first,
|
||||
"frameEnd": last,
|
||||
"version": vname,
|
||||
"colorspaceInput": colorspace,
|
||||
"objectName": object_name}
|
||||
data_imprint = {
|
||||
"version": vname,
|
||||
"colorspaceInput": colorspace,
|
||||
"objectName": object_name
|
||||
}
|
||||
|
||||
for k in add_keys:
|
||||
data_imprint.update({k: version_data[k]})
|
||||
|
|
@ -204,18 +201,13 @@ class LoadBackdropNodes(load.LoaderPlugin):
|
|||
name = container['name']
|
||||
version_data = version_doc.get("data", {})
|
||||
vname = version_doc.get("name", None)
|
||||
first = version_data.get("frameStart", None)
|
||||
last = version_data.get("frameEnd", None)
|
||||
namespace = container['namespace']
|
||||
colorspace = version_data.get("colorspace", None)
|
||||
object_name = "{}_{}".format(name, namespace)
|
||||
|
||||
add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd",
|
||||
"source", "author", "fps"]
|
||||
add_keys = ["source", "author", "fps"]
|
||||
|
||||
data_imprint = {"representation": str(representation["_id"]),
|
||||
"frameStart": first,
|
||||
"frameEnd": last,
|
||||
"version": vname,
|
||||
"colorspaceInput": colorspace,
|
||||
"objectName": object_name}
|
||||
|
|
|
|||
|
|
@ -51,38 +51,10 @@ class CollectBackdrops(pyblish.api.InstancePlugin):
|
|||
instance.data["label"] = "{0} ({1} nodes)".format(
|
||||
bckn.name(), len(instance.data["transientData"]["childNodes"]))
|
||||
|
||||
instance.data["families"].append(instance.data["family"])
|
||||
|
||||
# Get frame range
|
||||
handle_start = instance.context.data["handleStart"]
|
||||
handle_end = instance.context.data["handleEnd"]
|
||||
first_frame = int(nuke.root()["first_frame"].getValue())
|
||||
last_frame = int(nuke.root()["last_frame"].getValue())
|
||||
|
||||
# get version
|
||||
version = instance.context.data.get('version')
|
||||
|
||||
if not version:
|
||||
raise RuntimeError("Script name has no version in the name.")
|
||||
if version:
|
||||
instance.data['version'] = version
|
||||
|
||||
instance.data['version'] = version
|
||||
|
||||
# Add version data to instance
|
||||
version_data = {
|
||||
"handles": handle_start,
|
||||
"handleStart": handle_start,
|
||||
"handleEnd": handle_end,
|
||||
"frameStart": first_frame + handle_start,
|
||||
"frameEnd": last_frame - handle_end,
|
||||
"version": int(version),
|
||||
"families": [instance.data["family"]] + instance.data["families"],
|
||||
"subset": instance.data["subset"],
|
||||
"fps": instance.context.data["fps"]
|
||||
}
|
||||
|
||||
instance.data.update({
|
||||
"versionData": version_data,
|
||||
"frameStart": first_frame,
|
||||
"frameEnd": last_frame
|
||||
})
|
||||
self.log.info("Backdrop instance collected: `{}`".format(instance))
|
||||
|
|
|
|||
|
|
@ -66,11 +66,11 @@ class MainThreadItem:
|
|||
return self._result
|
||||
|
||||
def execute(self):
|
||||
"""Execute callback and store it's result.
|
||||
"""Execute callback and store its result.
|
||||
|
||||
Method must be called from main thread. Item is marked as `done`
|
||||
when callback execution finished. Store output of callback of exception
|
||||
information when callback raise one.
|
||||
information when callback raises one.
|
||||
"""
|
||||
log.debug("Executing process in main thread")
|
||||
if self.done:
|
||||
|
|
|
|||
|
|
@ -389,11 +389,11 @@ class MainThreadItem:
|
|||
self.kwargs = kwargs
|
||||
|
||||
def execute(self):
|
||||
"""Execute callback and store it's result.
|
||||
"""Execute callback and store its result.
|
||||
|
||||
Method must be called from main thread. Item is marked as `done`
|
||||
when callback execution finished. Store output of callback of exception
|
||||
information when callback raise one.
|
||||
information when callback raises one.
|
||||
"""
|
||||
log.debug("Executing process in main thread")
|
||||
if self.done:
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ class TVPaintLegacyConverted(SubsetConvertorPlugin):
|
|||
self._convert_render_layers(
|
||||
to_convert["renderLayer"], current_instances)
|
||||
self._convert_render_passes(
|
||||
to_convert["renderpass"], current_instances)
|
||||
to_convert["renderPass"], current_instances)
|
||||
self._convert_render_scenes(
|
||||
to_convert["renderScene"], current_instances)
|
||||
self._convert_workfiles(
|
||||
|
|
@ -116,7 +116,7 @@ class TVPaintLegacyConverted(SubsetConvertorPlugin):
|
|||
render_layers_by_group_id = {}
|
||||
for instance in current_instances:
|
||||
if instance.get("creator_identifier") == "render.layer":
|
||||
group_id = instance["creator_identifier"]["group_id"]
|
||||
group_id = instance["creator_attributes"]["group_id"]
|
||||
render_layers_by_group_id[group_id] = instance
|
||||
|
||||
for render_pass in render_passes:
|
||||
|
|
|
|||
|
|
@ -415,11 +415,11 @@ class CreateRenderPass(TVPaintCreator):
|
|||
.get("creator_attributes", {})
|
||||
.get("render_layer_instance_id")
|
||||
)
|
||||
render_layer_info = render_layers.get(render_layer_instance_id)
|
||||
render_layer_info = render_layers.get(render_layer_instance_id, {})
|
||||
self.update_instance_labels(
|
||||
instance_data,
|
||||
render_layer_info["variant"],
|
||||
render_layer_info["template_data"]
|
||||
render_layer_info.get("variant"),
|
||||
render_layer_info.get("template_data")
|
||||
)
|
||||
instance = CreatedInstance.from_existing(instance_data, self)
|
||||
self._add_instance_to_context(instance)
|
||||
|
|
@ -607,11 +607,11 @@ class CreateRenderPass(TVPaintCreator):
|
|||
current_instances = self.host.list_instances()
|
||||
render_layers = [
|
||||
{
|
||||
"value": instance["instance_id"],
|
||||
"label": instance["subset"]
|
||||
"value": inst["instance_id"],
|
||||
"label": inst["subset"]
|
||||
}
|
||||
for instance in current_instances
|
||||
if instance["creator_identifier"] == CreateRenderlayer.identifier
|
||||
for inst in current_instances
|
||||
if inst.get("creator_identifier") == CreateRenderlayer.identifier
|
||||
]
|
||||
if not render_layers:
|
||||
render_layers.append({"value": None, "label": "N/A"})
|
||||
|
|
@ -697,6 +697,7 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator):
|
|||
["create"]
|
||||
["auto_detect_render"]
|
||||
)
|
||||
self.enabled = plugin_settings.get("enabled", False)
|
||||
self.allow_group_rename = plugin_settings["allow_group_rename"]
|
||||
self.group_name_template = plugin_settings["group_name_template"]
|
||||
self.group_idx_offset = plugin_settings["group_idx_offset"]
|
||||
|
|
|
|||
|
|
@ -22,9 +22,11 @@ class CollectOutputFrameRange(pyblish.api.InstancePlugin):
|
|||
context = instance.context
|
||||
|
||||
frame_start = asset_doc["data"]["frameStart"]
|
||||
fps = asset_doc["data"]["fps"]
|
||||
frame_end = frame_start + (
|
||||
context.data["sceneMarkOut"] - context.data["sceneMarkIn"]
|
||||
)
|
||||
instance.data["fps"] = fps
|
||||
instance.data["frameStart"] = frame_start
|
||||
instance.data["frameEnd"] = frame_end
|
||||
self.log.info(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import pyblish.api
|
||||
from openpype.pipeline import PublishXmlValidationError
|
||||
from openpype.pipeline import (
|
||||
PublishXmlValidationError,
|
||||
OptionalPyblishPluginMixin,
|
||||
)
|
||||
from openpype.hosts.tvpaint.api.pipeline import (
|
||||
list_instances,
|
||||
write_instances,
|
||||
|
|
@ -31,7 +34,10 @@ class FixAssetNames(pyblish.api.Action):
|
|||
write_instances(new_instance_items)
|
||||
|
||||
|
||||
class ValidateAssetNames(pyblish.api.ContextPlugin):
|
||||
class ValidateAssetName(
|
||||
OptionalPyblishPluginMixin,
|
||||
pyblish.api.ContextPlugin
|
||||
):
|
||||
"""Validate assset name present on instance.
|
||||
|
||||
Asset name on instance should be the same as context's.
|
||||
|
|
@ -43,6 +49,8 @@ class ValidateAssetNames(pyblish.api.ContextPlugin):
|
|||
actions = [FixAssetNames]
|
||||
|
||||
def process(self, context):
|
||||
if not self.is_active(context.data):
|
||||
return
|
||||
context_asset_name = context.data["asset"]
|
||||
for instance in context:
|
||||
asset_name = instance.data.get("asset")
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class ValidateLayersVisiblity(pyblish.api.InstancePlugin):
|
|||
families = ["review", "render"]
|
||||
|
||||
def process(self, instance):
|
||||
layers = instance.data["layers"]
|
||||
layers = instance.data.get("layers")
|
||||
# Instance have empty layers
|
||||
# - it is not job of this validator to check that
|
||||
if not layers:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import json
|
||||
|
||||
import pyblish.api
|
||||
from openpype.pipeline import PublishXmlValidationError
|
||||
from openpype.pipeline import (
|
||||
PublishXmlValidationError,
|
||||
OptionalPyblishPluginMixin,
|
||||
)
|
||||
from openpype.hosts.tvpaint.api.lib import execute_george
|
||||
|
||||
|
||||
|
|
@ -23,7 +26,10 @@ class ValidateMarksRepair(pyblish.api.Action):
|
|||
)
|
||||
|
||||
|
||||
class ValidateMarks(pyblish.api.ContextPlugin):
|
||||
class ValidateMarks(
|
||||
OptionalPyblishPluginMixin,
|
||||
pyblish.api.ContextPlugin
|
||||
):
|
||||
"""Validate mark in and out are enabled and it's duration.
|
||||
|
||||
Mark In/Out does not have to match frameStart and frameEnd but duration is
|
||||
|
|
@ -59,6 +65,9 @@ class ValidateMarks(pyblish.api.ContextPlugin):
|
|||
}
|
||||
|
||||
def process(self, context):
|
||||
if not self.is_active(context.data):
|
||||
return
|
||||
|
||||
current_data = {
|
||||
"markIn": context.data["sceneMarkIn"],
|
||||
"markInState": context.data["sceneMarkInState"],
|
||||
|
|
|
|||
|
|
@ -1,11 +1,17 @@
|
|||
import json
|
||||
|
||||
import pyblish.api
|
||||
from openpype.pipeline import PublishXmlValidationError
|
||||
from openpype.pipeline import (
|
||||
PublishXmlValidationError,
|
||||
OptionalPyblishPluginMixin,
|
||||
)
|
||||
|
||||
|
||||
# TODO @iLliCiTiT add fix action for fps
|
||||
class ValidateProjectSettings(pyblish.api.ContextPlugin):
|
||||
class ValidateProjectSettings(
|
||||
OptionalPyblishPluginMixin,
|
||||
pyblish.api.ContextPlugin
|
||||
):
|
||||
"""Validate scene settings against database."""
|
||||
|
||||
label = "Validate Scene Settings"
|
||||
|
|
@ -13,6 +19,9 @@ class ValidateProjectSettings(pyblish.api.ContextPlugin):
|
|||
optional = True
|
||||
|
||||
def process(self, context):
|
||||
if not self.is_active(context.data):
|
||||
return
|
||||
|
||||
expected_data = context.data["assetEntity"]["data"]
|
||||
scene_data = {
|
||||
"fps": context.data.get("sceneFps"),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import pyblish.api
|
||||
from openpype.pipeline import PublishXmlValidationError
|
||||
from openpype.pipeline import (
|
||||
PublishXmlValidationError,
|
||||
OptionalPyblishPluginMixin,
|
||||
)
|
||||
from openpype.hosts.tvpaint.api.lib import execute_george
|
||||
|
||||
|
||||
|
|
@ -14,7 +17,10 @@ class RepairStartFrame(pyblish.api.Action):
|
|||
execute_george("tv_startframe 0")
|
||||
|
||||
|
||||
class ValidateStartFrame(pyblish.api.ContextPlugin):
|
||||
class ValidateStartFrame(
|
||||
OptionalPyblishPluginMixin,
|
||||
pyblish.api.ContextPlugin
|
||||
):
|
||||
"""Validate start frame being at frame 0."""
|
||||
|
||||
label = "Validate Start Frame"
|
||||
|
|
@ -24,6 +30,9 @@ class ValidateStartFrame(pyblish.api.ContextPlugin):
|
|||
optional = True
|
||||
|
||||
def process(self, context):
|
||||
if not self.is_active(context.data):
|
||||
return
|
||||
|
||||
start_frame = execute_george("tv_startframe")
|
||||
if start_frame == 0:
|
||||
return
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ class AddonSettingsDef(JsonFilesSettingsDef):
|
|||
|
||||
|
||||
class ExampleAddon(OpenPypeAddOn, IPluginPaths, ITrayAction):
|
||||
"""This Addon has defined it's settings and interface.
|
||||
"""This Addon has defined its settings and interface.
|
||||
|
||||
This example has system settings with an enabled option. And use
|
||||
few other interfaces:
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from openpype_modules.ftrack.lib import (
|
|||
|
||||
|
||||
class PushHierValuesToNonHier(ServerAction):
|
||||
"""Action push hierarchical custom attribute values to non hierarchical.
|
||||
"""Action push hierarchical custom attribute values to non-hierarchical.
|
||||
|
||||
Hierarchical value is also pushed to their task entities.
|
||||
|
||||
|
|
@ -119,17 +119,109 @@ class PushHierValuesToNonHier(ServerAction):
|
|||
self.join_query_keys(object_ids)
|
||||
)).all()
|
||||
|
||||
output = {}
|
||||
attrs_by_obj_id = collections.defaultdict(list)
|
||||
hiearchical = []
|
||||
for attr in attrs:
|
||||
if attr["is_hierarchical"]:
|
||||
hiearchical.append(attr)
|
||||
continue
|
||||
obj_id = attr["object_type_id"]
|
||||
if obj_id not in output:
|
||||
output[obj_id] = []
|
||||
output[obj_id].append(attr)
|
||||
return output, hiearchical
|
||||
attrs_by_obj_id[obj_id].append(attr)
|
||||
return attrs_by_obj_id, hiearchical
|
||||
|
||||
def query_attr_value(
|
||||
self,
|
||||
session,
|
||||
hier_attrs,
|
||||
attrs_by_obj_id,
|
||||
dst_object_type_ids,
|
||||
task_entity_ids,
|
||||
non_task_entity_ids,
|
||||
parent_id_by_entity_id
|
||||
):
|
||||
all_non_task_ids_with_parents = set()
|
||||
for entity_id in non_task_entity_ids:
|
||||
all_non_task_ids_with_parents.add(entity_id)
|
||||
_entity_id = entity_id
|
||||
while True:
|
||||
parent_id = parent_id_by_entity_id.get(_entity_id)
|
||||
if (
|
||||
parent_id is None
|
||||
or parent_id in all_non_task_ids_with_parents
|
||||
):
|
||||
break
|
||||
all_non_task_ids_with_parents.add(parent_id)
|
||||
_entity_id = parent_id
|
||||
|
||||
all_entity_ids = (
|
||||
set(all_non_task_ids_with_parents)
|
||||
| set(task_entity_ids)
|
||||
)
|
||||
attr_ids = {attr["id"] for attr in hier_attrs}
|
||||
for obj_id in dst_object_type_ids:
|
||||
attrs = attrs_by_obj_id.get(obj_id)
|
||||
if attrs is not None:
|
||||
for attr in attrs:
|
||||
attr_ids.add(attr["id"])
|
||||
|
||||
real_values_by_entity_id = {
|
||||
entity_id: {}
|
||||
for entity_id in all_entity_ids
|
||||
}
|
||||
|
||||
attr_values = query_custom_attributes(
|
||||
session, attr_ids, all_entity_ids, True
|
||||
)
|
||||
for item in attr_values:
|
||||
entity_id = item["entity_id"]
|
||||
attr_id = item["configuration_id"]
|
||||
real_values_by_entity_id[entity_id][attr_id] = item["value"]
|
||||
|
||||
# Fill hierarchical values
|
||||
hier_attrs_key_by_id = {
|
||||
hier_attr["id"]: hier_attr
|
||||
for hier_attr in hier_attrs
|
||||
}
|
||||
hier_values_per_entity_id = {}
|
||||
for entity_id in all_non_task_ids_with_parents:
|
||||
real_values = real_values_by_entity_id[entity_id]
|
||||
hier_values_per_entity_id[entity_id] = {}
|
||||
for attr_id, attr in hier_attrs_key_by_id.items():
|
||||
key = attr["key"]
|
||||
hier_values_per_entity_id[entity_id][key] = (
|
||||
real_values.get(attr_id)
|
||||
)
|
||||
|
||||
output = {}
|
||||
for entity_id in non_task_entity_ids:
|
||||
output[entity_id] = {}
|
||||
for attr in hier_attrs_key_by_id.values():
|
||||
key = attr["key"]
|
||||
value = hier_values_per_entity_id[entity_id][key]
|
||||
tried_ids = set()
|
||||
if value is None:
|
||||
tried_ids.add(entity_id)
|
||||
_entity_id = entity_id
|
||||
while value is None:
|
||||
parent_id = parent_id_by_entity_id.get(_entity_id)
|
||||
if not parent_id:
|
||||
break
|
||||
value = hier_values_per_entity_id[parent_id][key]
|
||||
if value is not None:
|
||||
break
|
||||
_entity_id = parent_id
|
||||
tried_ids.add(parent_id)
|
||||
|
||||
if value is None:
|
||||
value = attr["default"]
|
||||
|
||||
if value is not None:
|
||||
for ent_id in tried_ids:
|
||||
hier_values_per_entity_id[ent_id][key] = value
|
||||
|
||||
output[entity_id][key] = value
|
||||
|
||||
return real_values_by_entity_id, output
|
||||
|
||||
def propagate_values(self, session, event, selected_entities):
|
||||
ftrack_settings = self.get_ftrack_settings(
|
||||
|
|
@ -156,29 +248,24 @@ class PushHierValuesToNonHier(ServerAction):
|
|||
}
|
||||
|
||||
task_object_type = object_types_by_low_name["task"]
|
||||
destination_object_types = [task_object_type]
|
||||
dst_object_type_ids = {task_object_type["id"]}
|
||||
for ent_type in interest_entity_types:
|
||||
obj_type = object_types_by_low_name.get(ent_type)
|
||||
if obj_type and obj_type not in destination_object_types:
|
||||
destination_object_types.append(obj_type)
|
||||
|
||||
destination_object_type_ids = set(
|
||||
obj_type["id"]
|
||||
for obj_type in destination_object_types
|
||||
)
|
||||
if obj_type:
|
||||
dst_object_type_ids.add(obj_type["id"])
|
||||
|
||||
interest_attributes = action_settings["interest_attributes"]
|
||||
# Find custom attributes definitions
|
||||
attrs_by_obj_id, hier_attrs = self.attrs_configurations(
|
||||
session, destination_object_type_ids, interest_attributes
|
||||
session, dst_object_type_ids, interest_attributes
|
||||
)
|
||||
# Filter destination object types if they have any object specific
|
||||
# custom attribute
|
||||
for obj_id in tuple(destination_object_type_ids):
|
||||
for obj_id in tuple(dst_object_type_ids):
|
||||
if obj_id not in attrs_by_obj_id:
|
||||
destination_object_type_ids.remove(obj_id)
|
||||
dst_object_type_ids.remove(obj_id)
|
||||
|
||||
if not destination_object_type_ids:
|
||||
if not dst_object_type_ids:
|
||||
# TODO report that there are not matching custom attributes
|
||||
return {
|
||||
"success": True,
|
||||
|
|
@ -192,14 +279,14 @@ class PushHierValuesToNonHier(ServerAction):
|
|||
session,
|
||||
selected_ids,
|
||||
project_entity,
|
||||
destination_object_type_ids
|
||||
dst_object_type_ids
|
||||
)
|
||||
|
||||
self.log.debug("Preparing whole project hierarchy by ids.")
|
||||
|
||||
entities_by_obj_id = {
|
||||
obj_id: []
|
||||
for obj_id in destination_object_type_ids
|
||||
for obj_id in dst_object_type_ids
|
||||
}
|
||||
|
||||
self.log.debug("Filtering Task entities.")
|
||||
|
|
@ -223,10 +310,16 @@ class PushHierValuesToNonHier(ServerAction):
|
|||
"message": "Nothing to do in your selection."
|
||||
}
|
||||
|
||||
self.log.debug("Getting Hierarchical custom attribute values parents.")
|
||||
hier_values_by_entity_id = self.get_hier_values(
|
||||
self.log.debug("Getting Custom attribute values.")
|
||||
(
|
||||
real_values_by_entity_id,
|
||||
hier_values_by_entity_id
|
||||
) = self.query_attr_value(
|
||||
session,
|
||||
hier_attrs,
|
||||
attrs_by_obj_id,
|
||||
dst_object_type_ids,
|
||||
task_entity_ids,
|
||||
non_task_entity_ids,
|
||||
parent_id_by_entity_id
|
||||
)
|
||||
|
|
@ -237,7 +330,8 @@ class PushHierValuesToNonHier(ServerAction):
|
|||
hier_attrs,
|
||||
task_entity_ids,
|
||||
hier_values_by_entity_id,
|
||||
parent_id_by_entity_id
|
||||
parent_id_by_entity_id,
|
||||
real_values_by_entity_id
|
||||
)
|
||||
|
||||
self.log.debug("Setting values to entities themselves.")
|
||||
|
|
@ -245,7 +339,8 @@ class PushHierValuesToNonHier(ServerAction):
|
|||
session,
|
||||
entities_by_obj_id,
|
||||
attrs_by_obj_id,
|
||||
hier_values_by_entity_id
|
||||
hier_values_by_entity_id,
|
||||
real_values_by_entity_id
|
||||
)
|
||||
|
||||
return True
|
||||
|
|
@ -322,112 +417,64 @@ class PushHierValuesToNonHier(ServerAction):
|
|||
|
||||
return parent_id_by_entity_id, filtered_entities
|
||||
|
||||
def get_hier_values(
|
||||
self,
|
||||
session,
|
||||
hier_attrs,
|
||||
focus_entity_ids,
|
||||
parent_id_by_entity_id
|
||||
):
|
||||
all_ids_with_parents = set()
|
||||
for entity_id in focus_entity_ids:
|
||||
all_ids_with_parents.add(entity_id)
|
||||
_entity_id = entity_id
|
||||
while True:
|
||||
parent_id = parent_id_by_entity_id.get(_entity_id)
|
||||
if (
|
||||
not parent_id
|
||||
or parent_id in all_ids_with_parents
|
||||
):
|
||||
break
|
||||
all_ids_with_parents.add(parent_id)
|
||||
_entity_id = parent_id
|
||||
|
||||
hier_attr_ids = tuple(hier_attr["id"] for hier_attr in hier_attrs)
|
||||
hier_attrs_key_by_id = {
|
||||
hier_attr["id"]: hier_attr["key"]
|
||||
for hier_attr in hier_attrs
|
||||
}
|
||||
|
||||
values_per_entity_id = {}
|
||||
for entity_id in all_ids_with_parents:
|
||||
values_per_entity_id[entity_id] = {}
|
||||
for key in hier_attrs_key_by_id.values():
|
||||
values_per_entity_id[entity_id][key] = None
|
||||
|
||||
values = query_custom_attributes(
|
||||
session, hier_attr_ids, all_ids_with_parents, True
|
||||
)
|
||||
for item in values:
|
||||
entity_id = item["entity_id"]
|
||||
key = hier_attrs_key_by_id[item["configuration_id"]]
|
||||
|
||||
values_per_entity_id[entity_id][key] = item["value"]
|
||||
|
||||
output = {}
|
||||
for entity_id in focus_entity_ids:
|
||||
output[entity_id] = {}
|
||||
for key in hier_attrs_key_by_id.values():
|
||||
value = values_per_entity_id[entity_id][key]
|
||||
tried_ids = set()
|
||||
if value is None:
|
||||
tried_ids.add(entity_id)
|
||||
_entity_id = entity_id
|
||||
while value is None:
|
||||
parent_id = parent_id_by_entity_id.get(_entity_id)
|
||||
if not parent_id:
|
||||
break
|
||||
value = values_per_entity_id[parent_id][key]
|
||||
if value is not None:
|
||||
break
|
||||
_entity_id = parent_id
|
||||
tried_ids.add(parent_id)
|
||||
|
||||
if value is not None:
|
||||
for ent_id in tried_ids:
|
||||
values_per_entity_id[ent_id][key] = value
|
||||
|
||||
output[entity_id][key] = value
|
||||
return output
|
||||
|
||||
def set_task_attr_values(
|
||||
self,
|
||||
session,
|
||||
hier_attrs,
|
||||
task_entity_ids,
|
||||
hier_values_by_entity_id,
|
||||
parent_id_by_entity_id
|
||||
parent_id_by_entity_id,
|
||||
real_values_by_entity_id
|
||||
):
|
||||
hier_attr_id_by_key = {
|
||||
attr["key"]: attr["id"]
|
||||
for attr in hier_attrs
|
||||
}
|
||||
filtered_task_ids = set()
|
||||
for task_id in task_entity_ids:
|
||||
parent_id = parent_id_by_entity_id.get(task_id) or {}
|
||||
parent_id = parent_id_by_entity_id.get(task_id)
|
||||
parent_values = hier_values_by_entity_id.get(parent_id)
|
||||
if not parent_values:
|
||||
continue
|
||||
if parent_values:
|
||||
filtered_task_ids.add(task_id)
|
||||
|
||||
if not filtered_task_ids:
|
||||
return
|
||||
|
||||
for task_id in filtered_task_ids:
|
||||
parent_id = parent_id_by_entity_id[task_id]
|
||||
parent_values = hier_values_by_entity_id[parent_id]
|
||||
hier_values_by_entity_id[task_id] = {}
|
||||
real_task_attr_values = real_values_by_entity_id[task_id]
|
||||
for key, value in parent_values.items():
|
||||
hier_values_by_entity_id[task_id][key] = value
|
||||
if value is None:
|
||||
continue
|
||||
|
||||
configuration_id = hier_attr_id_by_key[key]
|
||||
_entity_key = collections.OrderedDict([
|
||||
("configuration_id", configuration_id),
|
||||
("entity_id", task_id)
|
||||
])
|
||||
|
||||
session.recorded_operations.push(
|
||||
ftrack_api.operation.UpdateEntityOperation(
|
||||
"ContextCustomAttributeValue",
|
||||
op = None
|
||||
if configuration_id not in real_task_attr_values:
|
||||
op = ftrack_api.operation.CreateEntityOperation(
|
||||
"CustomAttributeValue",
|
||||
_entity_key,
|
||||
{"value": value}
|
||||
)
|
||||
elif real_task_attr_values[configuration_id] != value:
|
||||
op = ftrack_api.operation.UpdateEntityOperation(
|
||||
"CustomAttributeValue",
|
||||
_entity_key,
|
||||
"value",
|
||||
ftrack_api.symbol.NOT_SET,
|
||||
real_task_attr_values[configuration_id],
|
||||
value
|
||||
)
|
||||
)
|
||||
if len(session.recorded_operations) > 100:
|
||||
session.commit()
|
||||
|
||||
if op is not None:
|
||||
session.recorded_operations.push(op)
|
||||
if len(session.recorded_operations) > 100:
|
||||
session.commit()
|
||||
|
||||
session.commit()
|
||||
|
||||
|
|
@ -436,39 +483,68 @@ class PushHierValuesToNonHier(ServerAction):
|
|||
session,
|
||||
entities_by_obj_id,
|
||||
attrs_by_obj_id,
|
||||
hier_values_by_entity_id
|
||||
hier_values_by_entity_id,
|
||||
real_values_by_entity_id
|
||||
):
|
||||
"""Push values from hierarchical custom attributes to non-hierarchical.
|
||||
|
||||
Args:
|
||||
session (ftrack_api.Sessison): Session which queried entities,
|
||||
values and which is used for change propagation.
|
||||
entities_by_obj_id (dict[str, list[str]]): TypedContext
|
||||
ftrack entity ids where the attributes are propagated by their
|
||||
object ids.
|
||||
attrs_by_obj_id (dict[str, ftrack_api.Entity]): Objects of
|
||||
'CustomAttributeConfiguration' by their ids.
|
||||
hier_values_by_entity_id (doc[str, dict[str, Any]]): Attribute
|
||||
values by entity id and by their keys.
|
||||
real_values_by_entity_id (doc[str, dict[str, Any]]): Real attribute
|
||||
values of entities.
|
||||
"""
|
||||
|
||||
for object_id, entity_ids in entities_by_obj_id.items():
|
||||
attrs = attrs_by_obj_id.get(object_id)
|
||||
if not attrs or not entity_ids:
|
||||
continue
|
||||
|
||||
for attr in attrs:
|
||||
for entity_id in entity_ids:
|
||||
value = (
|
||||
hier_values_by_entity_id
|
||||
.get(entity_id, {})
|
||||
.get(attr["key"])
|
||||
)
|
||||
for entity_id in entity_ids:
|
||||
real_values = real_values_by_entity_id.get(entity_id)
|
||||
hier_values = hier_values_by_entity_id.get(entity_id)
|
||||
if hier_values is None:
|
||||
continue
|
||||
|
||||
for attr in attrs:
|
||||
attr_id = attr["id"]
|
||||
attr_key = attr["key"]
|
||||
value = hier_values.get(attr_key)
|
||||
if value is None:
|
||||
continue
|
||||
|
||||
_entity_key = collections.OrderedDict([
|
||||
("configuration_id", attr["id"]),
|
||||
("configuration_id", attr_id),
|
||||
("entity_id", entity_id)
|
||||
])
|
||||
|
||||
session.recorded_operations.push(
|
||||
ftrack_api.operation.UpdateEntityOperation(
|
||||
"ContextCustomAttributeValue",
|
||||
op = None
|
||||
if attr_id not in real_values:
|
||||
op = ftrack_api.operation.CreateEntityOperation(
|
||||
"CustomAttributeValue",
|
||||
_entity_key,
|
||||
{"value": value}
|
||||
)
|
||||
elif real_values[attr_id] != value:
|
||||
op = ftrack_api.operation.UpdateEntityOperation(
|
||||
"CustomAttributeValue",
|
||||
_entity_key,
|
||||
"value",
|
||||
ftrack_api.symbol.NOT_SET,
|
||||
real_values[attr_id],
|
||||
value
|
||||
)
|
||||
)
|
||||
if len(session.recorded_operations) > 100:
|
||||
session.commit()
|
||||
|
||||
if op is not None:
|
||||
session.recorded_operations.push(op)
|
||||
if len(session.recorded_operations) > 100:
|
||||
session.commit()
|
||||
|
||||
session.commit()
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,2 +1,3 @@
|
|||
DEFAULT_PUBLISH_TEMPLATE = "publish"
|
||||
DEFAULT_HERO_PUBLISH_TEMPLATE = "hero"
|
||||
TRANSIENT_DIR_TEMPLATE = "transient"
|
||||
|
|
|
|||
|
|
@ -20,13 +20,15 @@ from openpype.settings import (
|
|||
get_system_settings,
|
||||
)
|
||||
from openpype.pipeline import (
|
||||
tempdir
|
||||
tempdir,
|
||||
Anatomy
|
||||
)
|
||||
from openpype.pipeline.plugin_discover import DiscoverResult
|
||||
|
||||
from .contants import (
|
||||
DEFAULT_PUBLISH_TEMPLATE,
|
||||
DEFAULT_HERO_PUBLISH_TEMPLATE,
|
||||
TRANSIENT_DIR_TEMPLATE
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -690,3 +692,79 @@ def get_publish_repre_path(instance, repre, only_published=False):
|
|||
if os.path.exists(src_path):
|
||||
return src_path
|
||||
return None
|
||||
|
||||
|
||||
def get_custom_staging_dir_info(project_name, host_name, family, task_name,
|
||||
task_type, subset_name,
|
||||
project_settings=None,
|
||||
anatomy=None, log=None):
|
||||
"""Checks profiles if context should use special custom dir as staging.
|
||||
|
||||
Args:
|
||||
project_name (str)
|
||||
host_name (str)
|
||||
family (str)
|
||||
task_name (str)
|
||||
task_type (str)
|
||||
subset_name (str)
|
||||
project_settings(Dict[str, Any]): Prepared project settings.
|
||||
anatomy (Dict[str, Any])
|
||||
log (Logger) (optional)
|
||||
|
||||
Returns:
|
||||
(tuple)
|
||||
Raises:
|
||||
ValueError - if misconfigured template should be used
|
||||
"""
|
||||
settings = project_settings or get_project_settings(project_name)
|
||||
custom_staging_dir_profiles = (settings["global"]
|
||||
["tools"]
|
||||
["publish"]
|
||||
["custom_staging_dir_profiles"])
|
||||
if not custom_staging_dir_profiles:
|
||||
return None, None
|
||||
|
||||
if not log:
|
||||
log = Logger.get_logger("get_custom_staging_dir_info")
|
||||
|
||||
filtering_criteria = {
|
||||
"hosts": host_name,
|
||||
"families": family,
|
||||
"task_names": task_name,
|
||||
"task_types": task_type,
|
||||
"subsets": subset_name
|
||||
}
|
||||
profile = filter_profiles(custom_staging_dir_profiles,
|
||||
filtering_criteria,
|
||||
logger=log)
|
||||
|
||||
if not profile or not profile["active"]:
|
||||
return None, None
|
||||
|
||||
if not anatomy:
|
||||
anatomy = Anatomy(project_name)
|
||||
|
||||
template_name = profile["template_name"] or TRANSIENT_DIR_TEMPLATE
|
||||
_validate_transient_template(project_name, template_name, anatomy)
|
||||
|
||||
custom_staging_dir = anatomy.templates[template_name]["folder"]
|
||||
is_persistent = profile["custom_staging_dir_persistent"]
|
||||
|
||||
return custom_staging_dir, is_persistent
|
||||
|
||||
|
||||
def _validate_transient_template(project_name, template_name, anatomy):
|
||||
"""Check that transient template is correctly configured.
|
||||
|
||||
Raises:
|
||||
ValueError - if misconfigured template
|
||||
"""
|
||||
if template_name not in anatomy.templates:
|
||||
raise ValueError(("Anatomy of project \"{}\" does not have set"
|
||||
" \"{}\" template key!"
|
||||
).format(project_name, template_name))
|
||||
|
||||
if "folder" not in anatomy.templates[template_name]:
|
||||
raise ValueError(("There is not set \"folder\" template in \"{}\" anatomy" # noqa
|
||||
" for project \"{}\"."
|
||||
).format(template_name, project_name))
|
||||
|
|
|
|||
|
|
@ -93,6 +93,10 @@ class CleanUp(pyblish.api.InstancePlugin):
|
|||
self.log.info("No staging directory found: %s" % staging_dir)
|
||||
return
|
||||
|
||||
if instance.data.get("stagingDir_persistent"):
|
||||
self.log.info("Staging dir: %s should be persistent" % staging_dir)
|
||||
return
|
||||
|
||||
self.log.info("Removing staging directory {}".format(staging_dir))
|
||||
shutil.rmtree(staging_dir)
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ class CleanUpFarm(pyblish.api.ContextPlugin):
|
|||
dirpaths_to_remove = set()
|
||||
for instance in context:
|
||||
staging_dir = instance.data.get("stagingDir")
|
||||
if staging_dir:
|
||||
if staging_dir and not instance.data.get("stagingDir_persistent"):
|
||||
dirpaths_to_remove.add(os.path.normpath(staging_dir))
|
||||
|
||||
if "representations" in instance.data:
|
||||
|
|
|
|||
67
openpype/plugins/publish/collect_custom_staging_dir.py
Normal file
67
openpype/plugins/publish/collect_custom_staging_dir.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
"""
|
||||
Requires:
|
||||
anatomy
|
||||
|
||||
|
||||
Provides:
|
||||
instance.data -> stagingDir (folder path)
|
||||
-> stagingDir_persistent (bool)
|
||||
"""
|
||||
import copy
|
||||
import os.path
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from openpype.pipeline.publish.lib import get_custom_staging_dir_info
|
||||
|
||||
|
||||
class CollectCustomStagingDir(pyblish.api.InstancePlugin):
|
||||
"""Looks through profiles if stagingDir should be persistent and in special
|
||||
location.
|
||||
|
||||
Transient staging dir could be useful in specific use cases where is
|
||||
desirable to have temporary renders in specific, persistent folders, could
|
||||
be on disks optimized for speed for example.
|
||||
|
||||
It is studio responsibility to clean up obsolete folders with data.
|
||||
|
||||
Location of the folder is configured in `project_anatomy/templates/others`.
|
||||
('transient' key is expected, with 'folder' key)
|
||||
|
||||
Which family/task type/subset is applicable is configured in:
|
||||
`project_settings/global/tools/publish/custom_staging_dir_profiles`
|
||||
|
||||
"""
|
||||
label = "Collect Custom Staging Directory"
|
||||
order = pyblish.api.CollectorOrder + 0.4990
|
||||
|
||||
template_key = "transient"
|
||||
|
||||
def process(self, instance):
|
||||
family = instance.data["family"]
|
||||
subset_name = instance.data["subset"]
|
||||
host_name = instance.context.data["hostName"]
|
||||
project_name = instance.context.data["projectName"]
|
||||
|
||||
anatomy = instance.context.data["anatomy"]
|
||||
anatomy_data = copy.deepcopy(instance.data["anatomyData"])
|
||||
task = anatomy_data.get("task", {})
|
||||
|
||||
transient_tml, is_persistent = get_custom_staging_dir_info(
|
||||
project_name, host_name, family, task.get("name"),
|
||||
task.get("type"), subset_name, anatomy=anatomy, log=self.log)
|
||||
result_str = "Not adding"
|
||||
if transient_tml:
|
||||
anatomy_data["root"] = anatomy.roots
|
||||
scene_name = instance.context.data.get("currentFile")
|
||||
if scene_name:
|
||||
anatomy_data["scene_name"] = os.path.basename(scene_name)
|
||||
transient_dir = transient_tml.format(**anatomy_data)
|
||||
instance.data["stagingDir"] = transient_dir
|
||||
|
||||
instance.data["stagingDir_persistent"] = is_persistent
|
||||
result_str = "Adding '{}' as".format(transient_dir)
|
||||
|
||||
self.log.info("{} custom staging dir for instance with '{}'".format(
|
||||
result_str, family
|
||||
))
|
||||
|
|
@ -58,12 +58,16 @@
|
|||
"file": "{originalBasename}.{ext}",
|
||||
"path": "{@folder}/{@file}"
|
||||
},
|
||||
"transient": {
|
||||
"folder": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{family}/{subset}"
|
||||
},
|
||||
"__dynamic_keys_labels__": {
|
||||
"maya2unreal": "Maya to Unreal",
|
||||
"simpleUnrealTextureHero": "Simple Unreal Texture - Hero",
|
||||
"simpleUnrealTexture": "Simple Unreal Texture",
|
||||
"online": "online",
|
||||
"source": "source"
|
||||
"source": "source",
|
||||
"transient": "transient"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -591,7 +591,8 @@
|
|||
"task_names": [],
|
||||
"template_name": "simpleUnrealTextureHero"
|
||||
}
|
||||
]
|
||||
],
|
||||
"custom_staging_dir_profiles": []
|
||||
}
|
||||
},
|
||||
"project_folder_structure": "{\"__project_root__\": {\"prod\": {}, \"resources\": {\"footage\": {\"plates\": {}, \"offline\": {}}, \"audio\": {}, \"art_dept\": {}}, \"editorial\": {}, \"assets\": {\"characters\": {}, \"locations\": {}}, \"shots\": {}}}",
|
||||
|
|
|
|||
|
|
@ -791,7 +791,7 @@
|
|||
"ExtractPlayblast": {
|
||||
"capture_preset": {
|
||||
"Codec": {
|
||||
"compression": "jpg",
|
||||
"compression": "png",
|
||||
"format": "image",
|
||||
"quality": 95
|
||||
},
|
||||
|
|
@ -818,7 +818,8 @@
|
|||
},
|
||||
"Generic": {
|
||||
"isolate_view": true,
|
||||
"off_screen": true
|
||||
"off_screen": true,
|
||||
"pan_zoom": false
|
||||
},
|
||||
"Renderer": {
|
||||
"rendererName": "vp2Renderer"
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@
|
|||
"default_variants": []
|
||||
},
|
||||
"auto_detect_render": {
|
||||
"enabled": false,
|
||||
"allow_group_rename": true,
|
||||
"group_name_template": "L{group_index}",
|
||||
"group_idx_offset": 10,
|
||||
|
|
|
|||
|
|
@ -202,7 +202,13 @@
|
|||
"key": "auto_detect_render",
|
||||
"label": "Auto-Detect Create Render",
|
||||
"is_group": true,
|
||||
"checkbox_key": "enabled",
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "enabled",
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"label": "The creator tries to auto-detect Render Layers and Render Passes in scene. For Render Layers is used group name as a variant and for Render Passes is used TVPaint layer name.<br/><br/>Group names can be renamed by their used order in scene. The renaming template where can be used <b>{group_index}</b> formatting key which is filled by \"used position index of group\".<br/>- Template: <b>L{group_index}</b><br/>- Group offset: <b>10</b><br/>- Group padding: <b>3</b><br/>Would create group names \"<b>L010</b>\", \"<b>L020</b>\", ..."
|
||||
|
|
|
|||
|
|
@ -408,6 +408,71 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"key": "custom_staging_dir_profiles",
|
||||
"label": "Custom Staging Dir Profiles",
|
||||
"use_label_wrap": true,
|
||||
"docstring": "Profiles to specify special location and persistence for staging dir. Could be used in Creators and Publish phase!",
|
||||
"object_type": {
|
||||
"type": "dict",
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "active",
|
||||
"label": "Is active",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"key": "hosts",
|
||||
"label": "Host names",
|
||||
"type": "hosts-enum",
|
||||
"multiselection": true
|
||||
},
|
||||
{
|
||||
"key": "task_types",
|
||||
"label": "Task types",
|
||||
"type": "task-types-enum"
|
||||
},
|
||||
{
|
||||
"key": "task_names",
|
||||
"label": "Task names",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
"key": "families",
|
||||
"label": "Families",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
"key": "subsets",
|
||||
"label": "Subset names",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"key": "custom_staging_dir_persistent",
|
||||
"label": "Custom Staging Folder Persistent",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"key": "template_name",
|
||||
"label": "Template Name",
|
||||
"type": "text",
|
||||
"placeholder": "transient"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,6 +91,11 @@
|
|||
"type": "boolean",
|
||||
"key": "off_screen",
|
||||
"label": " Off Screen"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "pan_zoom",
|
||||
"label": " 2D Pan/Zoom"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -156,7 +161,7 @@
|
|||
{
|
||||
"type": "boolean",
|
||||
"key": "override_viewport_options",
|
||||
"label": "override_viewport_options"
|
||||
"label": "Override Viewport Options"
|
||||
},
|
||||
{
|
||||
"type": "enum",
|
||||
|
|
|
|||
|
|
@ -862,11 +862,11 @@ class WrappedCallbackItem:
|
|||
return self._result
|
||||
|
||||
def execute(self):
|
||||
"""Execute callback and store it's result.
|
||||
"""Execute callback and store its result.
|
||||
|
||||
Method must be called from main thread. Item is marked as `done`
|
||||
when callback execution finished. Store output of callback of exception
|
||||
information when callback raise one.
|
||||
information when callback raises one.
|
||||
"""
|
||||
if self.done:
|
||||
self.log.warning("- item is already processed")
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring Pype version."""
|
||||
__version__ = "3.15.3-nightly.3"
|
||||
__version__ = "3.15.3-nightly.4"
|
||||
|
|
|
|||
|
|
@ -110,6 +110,41 @@ or Deadlines **Draft Tile Assembler**.
|
|||
This is useful to fix some specific renderer glitches and advanced hacking of Maya Scene files. `Patch name` is label for patch for easier orientation.
|
||||
`Patch regex` is regex used to find line in file, after `Patch line` string is inserted. Note that you need to add line ending.
|
||||
|
||||
### Extract Playblast Settings (review)
|
||||
These settings provide granular control over how the playblasts or reviews are produced in Maya.
|
||||
|
||||
Some of these settings are also available on the instance itself, in which case these settings will become the default value when creating the review instance.
|
||||
|
||||

|
||||
|
||||
- **Compression type** which file encoding to use.
|
||||
- **Data format** what format is the file encoding.
|
||||
- **Quality** lets you control the compression value for the output. Results can vary depending on the compression you selected. Quality values can range from 0 to 100, with a default value of 95.
|
||||
- **Background Color** the viewports background color.
|
||||
- **Background Bottom** the viewports background bottom color.
|
||||
- **Background Top** the viewports background top color.
|
||||
- **Override display options** override the viewports display options to use what is set in the settings.
|
||||
- **Isolate view** isolates the view to what is in the review instance. If only a camera is present in the review instance, all nodes are displayed in view.
|
||||
- **Off Screen** records the playblast hidden from the user.
|
||||
- **2D Pan/Zoom** enables the 2D Pan/Zoom functionality of the camera.
|
||||
- **Renderer name** which renderer to use for playblasting.
|
||||
- **Width** width of the output resolution. If this value is `0`, the asset's width is used.
|
||||
- **Height** height of the output resolution. If this value is `0`, the asset's height is used.
|
||||
|
||||
#### Viewport Options
|
||||
|
||||
Most settings to override in the viewport are self explanatory and can be found in Maya.
|
||||
|
||||

|
||||
|
||||
- **Override Viewport Options** enable to use the settings below for the viewport when publishing the review.
|
||||
|
||||
#### Camera Options
|
||||
|
||||
These options are set on the camera shape when publishing the review. They correspond to attributes on the Maya camera shape node.
|
||||
|
||||

|
||||
|
||||
## Custom Menu
|
||||
You can add your custom tools menu into Maya by extending definitions in **Maya -> Scripts Menu Definition**.
|
||||

|
||||
|
|
|
|||
BIN
website/docs/assets/maya-admin_extract_playblast_settings.png
Normal file
BIN
website/docs/assets/maya-admin_extract_playblast_settings.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.7 KiB |
|
|
@ -82,8 +82,8 @@ All context filters are lists which may contain strings or Regular expressions (
|
|||
- **`tasks`** - Currently processed task. `["modeling", "animation"]`
|
||||
|
||||
:::important Filtering
|
||||
Filters are optional. In case when multiple profiles match current context, profile with higher number of matched filters has higher priority that profile without filters.
|
||||
(Eg. order of when filter is added doesn't matter, only the precision of matching does.)
|
||||
Filters are optional. In case when multiple profiles match current context, profile with higher number of matched filters has higher priority than profile without filters.
|
||||
(The order the profiles in settings doesn't matter, only the precision of matching does.)
|
||||
:::
|
||||
|
||||
## Publish plugins
|
||||
|
|
@ -94,7 +94,7 @@ Publish plugins used across all integrations.
|
|||
### Extract Review
|
||||
Plugin responsible for automatic FFmpeg conversion to variety of formats.
|
||||
|
||||
Extract review is using [profile filtering](#profile-filters) to be able render different outputs for different situations.
|
||||
Extract review uses [profile filtering](#profile-filters) to render different outputs for different situations.
|
||||
|
||||
Applicable context filters:
|
||||
**`hosts`** - Host from which publishing was triggered. `["maya", "nuke"]`
|
||||
|
|
@ -104,7 +104,7 @@ Applicable context filters:
|
|||
|
||||
**Output Definitions**
|
||||
|
||||
Profile may generate multiple outputs from a single input. Each output must define unique name and output extension (use the extension without a dot e.g. **mp4**). All other settings of output definition are optional.
|
||||
A profile may generate multiple outputs from a single input. Each output must define unique name and output extension (use the extension without a dot e.g. **mp4**). All other settings of output definition are optional.
|
||||
|
||||

|
||||
- **`Tags`**
|
||||
|
|
@ -118,7 +118,7 @@ Profile may generate multiple outputs from a single input. Each output must defi
|
|||
- **Output arguments** other FFmpeg output arguments like codec definition.
|
||||
|
||||
- **`Output width`** and **`Output height`**
|
||||
- it is possible to rescale output to specified resolution and keep aspect ratio.
|
||||
- It is possible to rescale output to specified resolution and keep aspect ratio.
|
||||
- If value is set to 0, source resolution will be used.
|
||||
|
||||
- **`Overscan crop`**
|
||||
|
|
@ -230,10 +230,10 @@ Applicable context filters:
|
|||
## Tools
|
||||
Settings for OpenPype tools.
|
||||
|
||||
## Creator
|
||||
### Creator
|
||||
Settings related to [Creator tool](artist_tools_creator).
|
||||
|
||||
### Subset name profiles
|
||||
#### Subset name profiles
|
||||

|
||||
|
||||
Subset name helps to identify published content. More specific name helps with organization and avoid mixing of published content. Subset name is defined using one of templates defined in **Subset name profiles settings**. The template is filled with context information at the time of creation.
|
||||
|
|
@ -263,10 +263,31 @@ Template may look like `"{family}{Task}{Variant}"`.
|
|||
Some creators may have other keys as their context may require more information or more specific values. Make sure you've read documentation of host you're using.
|
||||
|
||||
|
||||
## Workfiles
|
||||
### Publish
|
||||
|
||||
#### Custom Staging Directory Profiles
|
||||
With this feature, users can specify a custom data folder path based on presets, which can be used during the creation and publishing stages.
|
||||
|
||||

|
||||
|
||||
Staging directories are used as a destination for intermediate files (as renders) before they are renamed and copied to proper location during the integration phase. They could be created completely dynamically in the temp folder or for some DCCs in the `work` area.
|
||||
Example could be Nuke where artist might want to temporarily render pictures into `work` area to check them before they get published with the choice of "Use existing frames" on the write node.
|
||||
|
||||
One of the key advantages of this feature is that it allows users to choose the folder for writing such intermediate files to take advantage of faster storage for rendering, which can help improve workflow efficiency. Additionally, this feature allows users to keep their intermediate extracted data persistent, and use their own infrastructure for regular cleaning.
|
||||
|
||||
In some cases, these DCCs (Nuke, Houdini, Maya) automatically add a rendering path during the creation stage, which is then used in publishing. Creators and extractors of such DCCs need to use these profiles to fill paths in DCC's nodes to use this functionality.
|
||||
|
||||
The custom staging folder uses a path template configured in `project_anatomy/templates/others` with `transient` being a default example path that could be used. The template requires a 'folder' key for it to be usable as custom staging folder.
|
||||
|
||||
##### Known issues
|
||||
- Any DCC that uses prefilled paths and store them inside of workfile nodes needs to implement resolving these paths with a configured profiles.
|
||||
- If studio uses Site Sync remote artists need to have access to configured custom staging folder!
|
||||
- Each node on the rendering farm must have access to configured custom staging folder!
|
||||
|
||||
### Workfiles
|
||||
All settings related to Workfile tool.
|
||||
|
||||
### Open last workfile at launch
|
||||
#### Open last workfile at launch
|
||||
This feature allows you to define a rule for each task/host or toggle the feature globally to all tasks as they are visible in the picture.
|
||||
|
||||

|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ All context filters are lists which may contain strings or Regular expressions (
|
|||
- **families** - Main family of processed instance. `["plate", "model"]`
|
||||
|
||||
:::important Filtering
|
||||
Filters are optional and may not be set. In case when multiple profiles match current context, profile with filters has higher priority that profile without filters.
|
||||
Filters are optional and may not be set. In case when multiple profiles match current context, profile with filters has higher priority than profile without filters.
|
||||
:::
|
||||
|
||||
#### Profile outputs
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue