Merge develop

This commit is contained in:
Roy Nieterau 2023-03-29 14:46:41 +02:00
parent 6405700ed9
commit 669a2256ef
248 changed files with 13531 additions and 8058 deletions

22
.github/workflows/project_actions.yml vendored Normal file
View file

@ -0,0 +1,22 @@
name: project-actions
on:
pull_request:
types: [review_requested]
pull_request_review:
types: [submitted]
jobs:
pr_review_requested:
name: pr_review_requested
runs-on: ubuntu-latest
if: github.event_name == 'pull_request_review' && github.event.review.state == 'changes_requested'
steps:
- name: Move PR to 'Change Requested'
uses: leonsteinhaeuser/project-beta-automations@v2.1.0
with:
gh_token: ${{ secrets.YNPUT_BOT_TOKEN }}
organization: ynput
project_id: 11
resource_node_id: ${{ github.event.pull_request.node_id }}
status_value: Change Requested

77
ARCHITECTURE.md Normal file
View file

@ -0,0 +1,77 @@
# Architecture
OpenPype is a monolithic Python project that bundles several parts, this document will try to give a birds eye overview of the project and, to a certain degree, each of the sub-projects.
The current file structure looks like this:
```
.
├── common - Code in this folder is backend portion of Addon distribution logic for v4 server.
├── docs - Documentation of the source code.
├── igniter - The OpenPype bootstrapper, deals with running version resolution and setting up the connection to the mongodb.
├── openpype - The actual OpenPype core package.
├── schema - Collection of JSON files describing schematics of objects. This follows Avalon's convention.
├── tests - Integration and unit tests.
├── tools - Conveninece scripts to perform common actions (in both bash and ps1).
├── vendor - When using the igniter, it deploys third party tools in here, such as ffmpeg.
└── website - Source files for https://openpype.io/ which is Docusaursus (https://docusaurus.io/).
```
The core functionality of the pipeline can be found in `igniter` and `openpype`, which in turn rely on the `schema` files, whenever you build (or download a pre-built) version of OpenPype, these two are bundled in there, and `Igniter` is the entry point.
## Igniter
It's the setup and update tool for OpenPype, unless you want to package `openpype` separately and deal with all the config manually, this will most likely be your entry point.
```
igniter/
├── bootstrap_repos.py - Module that will find or install OpenPype versions in the system.
├── __init__.py - Igniter entry point.
├── install_dialog.py- Show dialog for choosing central pype repository.
├── install_thread.py - Threading helpers for the install process.
├── __main__.py - Like `__init__.py` ?
├── message_dialog.py - Qt Dialog with a message and "Ok" button.
├── nice_progress_bar.py - Fancy Qt progress bar.
├── splash.txt - ASCII art for the terminal installer.
├── stylesheet.css - Installer Qt styles.
├── terminal_splash.py - Terminal installer animation, relies in `splash.txt`.
├── tools.py - Collection of methods that don't fit in other modules.
├── update_thread.py - Threading helper to update existing OpenPype installs.
├── update_window.py - Qt UI to update OpenPype installs.
├── user_settings.py - Interface for the OpenPype user settings.
└── version.py - Igniter's version number.
```
## OpenPype
This is the main package of the OpenPype logic, it could be loosely described as a combination of [Avalon](https://getavalon.github.io), [Pyblish](https://pyblish.com/) and glue around those with custom OpenPype only elements, things are in progress of being moved around to better prepare for V4, which will be released under a new name AYON.
```
openpype/
├── client - Interface for the MongoDB.
├── hooks - Hooks to be executed on certain OpenPype Applications defined in `openpype.lib.applications`.
├── host - Base class for the different hosts.
├── hosts - Integration with the different DCCs (hosts) using the `host` base class.
├── lib - Libraries that stitch together the package, some have been moved into other parts.
├── modules - OpenPype modules should contain separated logic of specific kind of implementation, such as Ftrack connection and its python API.
├── pipeline - Core of the OpenPype pipeline, handles creation of data, publishing, etc.
├── plugins - Global/core plugins for loader and publisher tool.
├── resources - Icons, fonts, etc.
├── scripts - Loose scipts that get run by tools/publishers.
├── settings - OpenPype settings interface.
├── style - Qt styling.
├── tests - Unit tests.
├── tools - Core tools, check out https://openpype.io/docs/artist_tools.
├── vendor - Vendoring of needed required Python packes.
├── widgets - Common re-usable Qt Widgets.
├── action.py - LEGACY: Lives now in `openpype.pipeline.publish.action` Pyblish actions.
├── cli.py - Command line interface, leverages `click`.
├── __init__.py - Sets two constants.
├── __main__.py - Entry point, calls the `cli.py`
├── plugin.py - Pyblish plugins.
├── pype_commands.py - Implementation of OpenPype commands.
└── version.py - Current version number.
```

File diff suppressed because it is too large Load diff

View file

@ -367,11 +367,15 @@ def run(script):
"--timeout",
help="Provide specific timeout value for test case",
default=None)
@click.option("-so",
"--setup_only",
help="Only create dbs, do not run tests",
default=None)
def runtests(folder, mark, pyargs, test_data_folder, persist, app_variant,
timeout):
timeout, setup_only):
"""Run all automatic tests after proper initialization via start.py"""
PypeCommands().run_tests(folder, mark, pyargs, test_data_folder,
persist, app_variant, timeout)
persist, app_variant, timeout, setup_only)
@main.command()

View file

@ -14,6 +14,7 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook):
# Execute after workfile template copy
order = 10
app_groups = [
"3dsmax",
"maya",
"nuke",
"nukex",

View file

@ -3,10 +3,13 @@ from openpype.lib import PreLaunchHook
from openpype.pipeline.workfile import create_workdir_extra_folders
class AddLastWorkfileToLaunchArgs(PreLaunchHook):
"""Add last workfile path to launch arguments.
class CreateWorkdirExtraFolders(PreLaunchHook):
"""Create extra folders for the work directory.
Based on setting `project_settings/global/tools/Workfiles/extra_folders`
profile filtering will decide whether extra folders need to be created in
the work directory.
This is not possible to do for all applications the same way.
"""
# Execute after workfile template copy

View file

@ -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({

View file

@ -39,7 +39,6 @@ class HostDirmap(object):
self._project_settings = project_settings
self._sync_module = sync_module # to limit reinit of Modules
self._log = None
self._mapping = None # cache mapping
@property
def sync_module(self):
@ -70,29 +69,28 @@ class HostDirmap(object):
"""Run host dependent remapping from source_path to destination_path"""
pass
def process_dirmap(self):
def process_dirmap(self, mapping=None):
# type: (dict) -> None
"""Go through all paths in Settings and set them using `dirmap`.
If artists has Site Sync enabled, take dirmap mapping directly from
Local Settings when artist is syncing workfile locally.
Args:
project_settings (dict): Settings for current project.
"""
if not self._mapping:
self._mapping = self.get_mappings(self.project_settings)
if not self._mapping:
if not mapping:
mapping = self.get_mappings()
if not mapping:
return
self.log.info("Processing directory mapping ...")
self.on_enable_dirmap()
self.log.info("mapping:: {}".format(self._mapping))
for k, sp in enumerate(self._mapping["source-path"]):
dst = self._mapping["destination-path"][k]
for k, sp in enumerate(mapping["source-path"]):
dst = mapping["destination-path"][k]
try:
# add trailing slash if missing
sp = os.path.join(sp, '')
dst = os.path.join(dst, '')
print("{} -> {}".format(sp, dst))
self.dirmap_routine(sp, dst)
except IndexError:
@ -110,28 +108,24 @@ class HostDirmap(object):
)
continue
def get_mappings(self, project_settings):
def get_mappings(self):
"""Get translation from source-path to destination-path.
It checks if Site Sync is enabled and user chose to use local
site, in that case configuration in Local Settings takes precedence
"""
local_mapping = self._get_local_sync_dirmap(project_settings)
dirmap_label = "{}-dirmap".format(self.host_name)
if (
not self.project_settings[self.host_name].get(dirmap_label)
and not local_mapping
):
return {}
mapping_settings = self.project_settings[self.host_name][dirmap_label]
mapping_enabled = mapping_settings["enabled"] or bool(local_mapping)
mapping_sett = self.project_settings[self.host_name].get(dirmap_label,
{})
local_mapping = self._get_local_sync_dirmap()
mapping_enabled = mapping_sett.get("enabled") or bool(local_mapping)
if not mapping_enabled:
return {}
mapping = (
local_mapping
or mapping_settings["paths"]
or mapping_sett["paths"]
or {}
)
@ -141,28 +135,27 @@ class HostDirmap(object):
or not mapping.get("source-path")
):
return {}
self.log.info("Processing directory mapping ...")
self.log.info("mapping:: {}".format(mapping))
return mapping
def _get_local_sync_dirmap(self, project_settings):
def _get_local_sync_dirmap(self):
"""
Returns dirmap if synch to local project is enabled.
Only valid mapping is from roots of remote site to local site set
in Local Settings.
Args:
project_settings (dict)
Returns:
dict : { "source-path": [XXX], "destination-path": [YYYY]}
"""
project_name = os.getenv("AVALON_PROJECT")
mapping = {}
if not project_settings["global"]["sync_server"]["enabled"]:
if (not self.sync_module.enabled or
project_name not in self.sync_module.get_enabled_projects()):
return mapping
project_name = os.getenv("AVALON_PROJECT")
active_site = self.sync_module.get_local_normalized_site(
self.sync_module.get_active_site(project_name))
remote_site = self.sync_module.get_local_normalized_site(
@ -171,11 +164,7 @@ class HostDirmap(object):
"active {} - remote {}".format(active_site, remote_site)
)
if (
active_site == "local"
and project_name in self.sync_module.get_enabled_projects()
and active_site != remote_site
):
if active_site == "local" and active_site != remote_site:
sync_settings = self.sync_module.get_sync_project_setting(
project_name,
exclude_locals=False,
@ -188,7 +177,15 @@ class HostDirmap(object):
self.log.debug("local overrides {}".format(active_overrides))
self.log.debug("remote overrides {}".format(remote_overrides))
current_platform = platform.system().lower()
remote_provider = self.sync_module.get_provider_for_site(
project_name, remote_site
)
# dirmap has sense only with regular disk provider, in the workfile
# wont be root on cloud or sftp provider
if remote_provider != "local_drive":
remote_site = "studio"
for root_name, active_site_dir in active_overrides.items():
remote_site_dir = (
remote_overrides.get(root_name)

View file

@ -31,10 +31,13 @@ from .lib import (
lsattrs,
read,
maintained_selection,
maintained_time,
get_selection,
# unique_name,
)
from .capture import capture
__all__ = [
"install",
@ -56,9 +59,11 @@ __all__ = [
# Utility functions
"maintained_selection",
"maintained_time",
"lsattr",
"lsattrs",
"read",
"get_selection",
"capture",
# "unique_name",
]

View file

@ -0,0 +1,278 @@
"""Blender Capture
Playblasting with independent viewport, camera and display options
"""
import contextlib
import bpy
from .lib import maintained_time
from .plugin import deselect_all, create_blender_context
def capture(
camera=None,
width=None,
height=None,
filename=None,
start_frame=None,
end_frame=None,
step_frame=None,
sound=None,
isolate=None,
maintain_aspect_ratio=True,
overwrite=False,
image_settings=None,
display_options=None
):
"""Playblast in an independent windows
Arguments:
camera (str, optional): Name of camera, defaults to "Camera"
width (int, optional): Width of output in pixels
height (int, optional): Height of output in pixels
filename (str, optional): Name of output file path. Defaults to current
render output path.
start_frame (int, optional): Defaults to current start frame.
end_frame (int, optional): Defaults to current end frame.
step_frame (int, optional): Defaults to 1.
sound (str, optional): Specify the sound node to be used during
playblast. When None (default) no sound will be used.
isolate (list): List of nodes to isolate upon capturing
maintain_aspect_ratio (bool, optional): Modify height in order to
maintain aspect ratio.
overwrite (bool, optional): Whether or not to overwrite if file
already exists. If disabled and file exists and error will be
raised.
image_settings (dict, optional): Supplied image settings for render,
using `ImageSettings`
display_options (dict, optional): Supplied display options for render
"""
scene = bpy.context.scene
camera = camera or "Camera"
# Ensure camera exists.
if camera not in scene.objects and camera != "AUTO":
raise RuntimeError("Camera does not exist: {0}".format(camera))
# Ensure resolution.
if width and height:
maintain_aspect_ratio = False
width = width or scene.render.resolution_x
height = height or scene.render.resolution_y
if maintain_aspect_ratio:
ratio = scene.render.resolution_x / scene.render.resolution_y
height = round(width / ratio)
# Get frame range.
if start_frame is None:
start_frame = scene.frame_start
if end_frame is None:
end_frame = scene.frame_end
if step_frame is None:
step_frame = 1
frame_range = (start_frame, end_frame, step_frame)
if filename is None:
filename = scene.render.filepath
render_options = {
"filepath": "{}.".format(filename.rstrip(".")),
"resolution_x": width,
"resolution_y": height,
"use_overwrite": overwrite,
}
with _independent_window() as window:
applied_view(window, camera, isolate, options=display_options)
with contextlib.ExitStack() as stack:
stack.enter_context(maintain_camera(window, camera))
stack.enter_context(applied_frame_range(window, *frame_range))
stack.enter_context(applied_render_options(window, render_options))
stack.enter_context(applied_image_settings(window, image_settings))
stack.enter_context(maintained_time())
bpy.ops.render.opengl(
animation=True,
render_keyed_only=False,
sequencer=False,
write_still=False,
view_context=True
)
return filename
ImageSettings = {
"file_format": "FFMPEG",
"color_mode": "RGB",
"ffmpeg": {
"format": "QUICKTIME",
"use_autosplit": False,
"codec": "H264",
"constant_rate_factor": "MEDIUM",
"gopsize": 18,
"use_max_b_frames": False,
},
}
def isolate_objects(window, objects):
"""Isolate selection"""
deselect_all()
for obj in objects:
obj.select_set(True)
context = create_blender_context(selected=objects, window=window)
bpy.ops.view3d.view_axis(context, type="FRONT")
bpy.ops.view3d.localview(context)
deselect_all()
def _apply_options(entity, options):
for option, value in options.items():
if isinstance(value, dict):
_apply_options(getattr(entity, option), value)
else:
setattr(entity, option, value)
def applied_view(window, camera, isolate=None, options=None):
"""Apply view options to window."""
area = window.screen.areas[0]
space = area.spaces[0]
area.ui_type = "VIEW_3D"
meshes = [obj for obj in window.scene.objects if obj.type == "MESH"]
if camera == "AUTO":
space.region_3d.view_perspective = "ORTHO"
isolate_objects(window, isolate or meshes)
else:
isolate_objects(window, isolate or meshes)
space.camera = window.scene.objects.get(camera)
space.region_3d.view_perspective = "CAMERA"
if isinstance(options, dict):
_apply_options(space, options)
else:
space.shading.type = "SOLID"
space.shading.color_type = "MATERIAL"
space.show_gizmo = False
space.overlay.show_overlays = False
@contextlib.contextmanager
def applied_frame_range(window, start, end, step):
"""Context manager for setting frame range."""
# Store current frame range
current_frame_start = window.scene.frame_start
current_frame_end = window.scene.frame_end
current_frame_step = window.scene.frame_step
# Apply frame range
window.scene.frame_start = start
window.scene.frame_end = end
window.scene.frame_step = step
try:
yield
finally:
# Restore frame range
window.scene.frame_start = current_frame_start
window.scene.frame_end = current_frame_end
window.scene.frame_step = current_frame_step
@contextlib.contextmanager
def applied_render_options(window, options):
"""Context manager for setting render options."""
render = window.scene.render
# Store current settings
original = {}
for opt in options.copy():
try:
original[opt] = getattr(render, opt)
except ValueError:
options.pop(opt)
# Apply settings
_apply_options(render, options)
try:
yield
finally:
# Restore previous settings
_apply_options(render, original)
@contextlib.contextmanager
def applied_image_settings(window, options):
"""Context manager to override image settings."""
options = options or ImageSettings.copy()
ffmpeg = options.pop("ffmpeg", {})
render = window.scene.render
# Store current image settings
original = {}
for opt in options.copy():
try:
original[opt] = getattr(render.image_settings, opt)
except ValueError:
options.pop(opt)
# Store current ffmpeg settings
original_ffmpeg = {}
for opt in ffmpeg.copy():
try:
original_ffmpeg[opt] = getattr(render.ffmpeg, opt)
except ValueError:
ffmpeg.pop(opt)
# Apply image settings
for opt, value in options.items():
setattr(render.image_settings, opt, value)
# Apply ffmpeg settings
for opt, value in ffmpeg.items():
setattr(render.ffmpeg, opt, value)
try:
yield
finally:
# Restore previous settings
for opt, value in original.items():
setattr(render.image_settings, opt, value)
for opt, value in original_ffmpeg.items():
setattr(render.ffmpeg, opt, value)
@contextlib.contextmanager
def maintain_camera(window, camera):
"""Context manager to override camera."""
current_camera = window.scene.camera
if camera in window.scene.objects:
window.scene.camera = window.scene.objects.get(camera)
try:
yield
finally:
window.scene.camera = current_camera
@contextlib.contextmanager
def _independent_window():
"""Create capture-window context."""
context = create_blender_context()
current_windows = set(bpy.context.window_manager.windows)
bpy.ops.wm.window_new(context)
window = list(set(bpy.context.window_manager.windows) - current_windows)[0]
context["window"] = window
try:
yield window
finally:
bpy.ops.wm.window_close(context)

View file

@ -284,3 +284,13 @@ def maintained_selection():
# This could happen if the active node was deleted during the
# context.
log.exception("Failed to set active object.")
@contextlib.contextmanager
def maintained_time():
"""Maintain current frame during context."""
current_time = bpy.context.scene.frame_current
try:
yield
finally:
bpy.context.scene.frame_current = current_time

View file

@ -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:
@ -382,8 +382,8 @@ class TOPBAR_MT_avalon(bpy.types.Menu):
layout.operator(LaunchLibrary.bl_idname, text="Library...")
layout.separator()
layout.operator(LaunchWorkFiles.bl_idname, text="Work Files...")
# TODO (jasper): maybe add 'Reload Pipeline', 'Reset Frame Range' and
# 'Reset Resolution'?
# TODO (jasper): maybe add 'Reload Pipeline', 'Set Frame Range' and
# 'Set Resolution'?
def draw_avalon_menu(self, context):

View file

@ -62,7 +62,8 @@ def prepare_data(data, container_name=None):
def create_blender_context(active: Optional[bpy.types.Object] = None,
selected: Optional[bpy.types.Object] = None,):
selected: Optional[bpy.types.Object] = None,
window: Optional[bpy.types.Window] = None):
"""Create a new Blender context. If an object is passed as
parameter, it is set as selected and active.
"""
@ -72,7 +73,9 @@ def create_blender_context(active: Optional[bpy.types.Object] = None,
override_context = bpy.context.copy()
for win in bpy.context.window_manager.windows:
windows = [window] if window else bpy.context.window_manager.windows
for win in windows:
for area in win.screen.areas:
if area.type == 'VIEW_3D':
for region in area.regions:

View file

@ -0,0 +1,47 @@
"""Create review."""
import bpy
from openpype.pipeline import legacy_io
from openpype.hosts.blender.api import plugin, lib, ops
from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES
class CreateReview(plugin.Creator):
"""Single baked camera"""
name = "reviewDefault"
label = "Review"
family = "review"
icon = "video-camera"
def process(self):
""" Run the creator on Blender main thread"""
mti = ops.MainThreadItem(self._process)
ops.execute_in_main_thread(mti)
def _process(self):
# Get Instance Container or create it if it does not exist
instances = bpy.data.collections.get(AVALON_INSTANCES)
if not instances:
instances = bpy.data.collections.new(name=AVALON_INSTANCES)
bpy.context.scene.collection.children.link(instances)
# Create instance object
asset = self.data["asset"]
subset = self.data["subset"]
name = plugin.asset_name(asset, subset)
asset_group = bpy.data.collections.new(name=name)
instances.children.link(asset_group)
self.data['task'] = legacy_io.Session.get('AVALON_TASK')
lib.imprint(asset_group, self.data)
if (self.options or {}).get("useSelection"):
selected = lib.get_selection()
for obj in selected:
asset_group.objects.link(obj)
elif (self.options or {}).get("asset_group"):
obj = (self.options or {}).get("asset_group")
asset_group.objects.link(obj)
return asset_group

View file

@ -0,0 +1,64 @@
import bpy
import pyblish.api
from openpype.pipeline import legacy_io
class CollectReview(pyblish.api.InstancePlugin):
"""Collect Review data
"""
order = pyblish.api.CollectorOrder + 0.3
label = "Collect Review Data"
families = ["review"]
def process(self, instance):
self.log.debug(f"instance: {instance}")
# get cameras
cameras = [
obj
for obj in instance
if isinstance(obj, bpy.types.Object) and obj.type == "CAMERA"
]
assert len(cameras) == 1, (
f"Not a single camera found in extraction: {cameras}"
)
camera = cameras[0].name
self.log.debug(f"camera: {camera}")
# get isolate objects list from meshes instance members .
isolate_objects = [
obj
for obj in instance
if isinstance(obj, bpy.types.Object) and obj.type == "MESH"
]
if not instance.data.get("remove"):
task = legacy_io.Session.get("AVALON_TASK")
instance.data.update({
"subset": f"{task}Review",
"review_camera": camera,
"frameStart": instance.context.data["frameStart"],
"frameEnd": instance.context.data["frameEnd"],
"fps": instance.context.data["fps"],
"isolate": isolate_objects,
})
self.log.debug(f"instance data: {instance.data}")
# TODO : Collect audio
audio_tracks = []
instance.data["audio"] = []
for track in audio_tracks:
instance.data["audio"].append(
{
"offset": track.offset.get(),
"filename": track.filename.get(),
}
)

View file

@ -0,0 +1,123 @@
import os
import clique
import bpy
import pyblish.api
from openpype.pipeline import publish
from openpype.hosts.blender.api import capture
from openpype.hosts.blender.api.lib import maintained_time
class ExtractPlayblast(publish.Extractor):
"""
Extract viewport playblast.
Takes review camera and creates review Quicktime video based on viewport
capture.
"""
label = "Extract Playblast"
hosts = ["blender"]
families = ["review"]
optional = True
order = pyblish.api.ExtractorOrder + 0.01
def process(self, instance):
self.log.info("Extracting capture..")
self.log.info(instance.data)
# get scene fps
fps = instance.data.get("fps")
if fps is None:
fps = bpy.context.scene.render.fps
instance.data["fps"] = fps
self.log.info(f"fps: {fps}")
# If start and end frames cannot be determined,
# get them from Blender timeline.
start = instance.data.get("frameStart", bpy.context.scene.frame_start)
end = instance.data.get("frameEnd", bpy.context.scene.frame_end)
self.log.info(f"start: {start}, end: {end}")
assert end > start, "Invalid time range !"
# get cameras
camera = instance.data("review_camera", None)
# get isolate objects list
isolate = instance.data("isolate", None)
# get ouput path
stagingdir = self.staging_dir(instance)
filename = instance.name
path = os.path.join(stagingdir, filename)
self.log.info(f"Outputting images to {path}")
project_settings = instance.context.data["project_settings"]["blender"]
presets = project_settings["publish"]["ExtractPlayblast"]["presets"]
preset = presets.get("default")
preset.update({
"camera": camera,
"start_frame": start,
"end_frame": end,
"filename": path,
"overwrite": True,
"isolate": isolate,
})
preset.setdefault(
"image_settings",
{
"file_format": "PNG",
"color_mode": "RGB",
"color_depth": "8",
"compression": 15,
},
)
with maintained_time():
path = capture(**preset)
self.log.debug(f"playblast path {path}")
collected_files = os.listdir(stagingdir)
collections, remainder = clique.assemble(
collected_files,
patterns=[f"{filename}\\.{clique.DIGITS_PATTERN}\\.png$"],
)
if len(collections) > 1:
raise RuntimeError(
f"More than one collection found in stagingdir: {stagingdir}"
)
elif len(collections) == 0:
raise RuntimeError(
f"No collection found in stagingdir: {stagingdir}"
)
frame_collection = collections[0]
self.log.info(f"We found collection of interest {frame_collection}")
instance.data.setdefault("representations", [])
tags = ["review"]
if not instance.data.get("keepImages"):
tags.append("delete")
representation = {
"name": "png",
"ext": "png",
"files": list(frame_collection),
"stagingDir": stagingdir,
"frameStart": start,
"frameEnd": end,
"fps": fps,
"preview": True,
"tags": tags,
"camera_name": camera
}
instance.data["representations"].append(representation)

View file

@ -0,0 +1,99 @@
import os
import glob
import pyblish.api
from openpype.pipeline import publish
from openpype.hosts.blender.api import capture
from openpype.hosts.blender.api.lib import maintained_time
import bpy
class ExtractThumbnail(publish.Extractor):
"""Extract viewport thumbnail.
Takes review camera and creates a thumbnail based on viewport
capture.
"""
label = "Extract Thumbnail"
hosts = ["blender"]
families = ["review"]
order = pyblish.api.ExtractorOrder + 0.01
presets = {}
def process(self, instance):
self.log.info("Extracting capture..")
stagingdir = self.staging_dir(instance)
filename = instance.name
path = os.path.join(stagingdir, filename)
self.log.info(f"Outputting images to {path}")
camera = instance.data.get("review_camera", "AUTO")
start = instance.data.get("frameStart", bpy.context.scene.frame_start)
family = instance.data.get("family")
isolate = instance.data("isolate", None)
preset = self.presets.get(family, {})
preset.update({
"camera": camera,
"start_frame": start,
"end_frame": start,
"filename": path,
"overwrite": True,
"isolate": isolate,
})
preset.setdefault(
"image_settings",
{
"file_format": "JPEG",
"color_mode": "RGB",
"quality": 100,
},
)
with maintained_time():
path = capture(**preset)
thumbnail = os.path.basename(self._fix_output_path(path))
self.log.info(f"thumbnail: {thumbnail}")
instance.data.setdefault("representations", [])
representation = {
"name": "thumbnail",
"ext": "jpg",
"files": thumbnail,
"stagingDir": stagingdir,
"thumbnail": True
}
instance.data["representations"].append(representation)
def _fix_output_path(self, filepath):
""""Workaround to return correct filepath.
To workaround this we just glob.glob() for any file extensions and
assume the latest modified file is the correct file and return it.
"""
# Catch cancelled playblast
if filepath is None:
self.log.warning(
"Playblast did not result in output path. "
"Playblast is probably interrupted."
)
return None
if not os.path.exists(filepath):
files = glob.glob(f"{filepath}.*.jpg")
if not files:
raise RuntimeError(f"Couldn't find playblast from: {filepath}")
filepath = max(files, key=os.path.getmtime)
return filepath

View file

@ -38,8 +38,9 @@ class CelactionPrelaunchHook(PreLaunchHook):
)
path_to_cli = os.path.join(CELACTION_SCRIPTS_DIR, "publish_cli.py")
subproces_args = get_openpype_execute_args("run", path_to_cli)
openpype_executable = subproces_args.pop(0)
subprocess_args = get_openpype_execute_args("run", path_to_cli)
openpype_executable = subprocess_args.pop(0)
workfile_settings = self.get_workfile_settings()
winreg.SetValueEx(
hKey,
@ -49,20 +50,34 @@ class CelactionPrelaunchHook(PreLaunchHook):
openpype_executable
)
parameters = subproces_args + [
"--currentFile", "*SCENE*",
"--chunk", "*CHUNK*",
"--frameStart", "*START*",
"--frameEnd", "*END*",
"--resolutionWidth", "*X*",
"--resolutionHeight", "*Y*"
# add required arguments for workfile path
parameters = subprocess_args + [
"--currentFile", "*SCENE*"
]
# Add custom parameters from workfile settings
if "render_chunk" in workfile_settings["submission_overrides"]:
parameters += [
"--chunk", "*CHUNK*"
]
if "resolution" in workfile_settings["submission_overrides"]:
parameters += [
"--resolutionWidth", "*X*",
"--resolutionHeight", "*Y*"
]
if "frame_range" in workfile_settings["submission_overrides"]:
parameters += [
"--frameStart", "*START*",
"--frameEnd", "*END*"
]
winreg.SetValueEx(
hKey, "SubmitParametersTitle", 0, winreg.REG_SZ,
subprocess.list2cmdline(parameters)
)
self.log.debug(f"__ parameters: \"{parameters}\"")
# setting resolution parameters
path_submit = "\\".join([
path_user_settings, "Dialogs", "SubmitOutput"
@ -135,3 +150,6 @@ class CelactionPrelaunchHook(PreLaunchHook):
self.log.info(f"Workfile to open: \"{workfile_path}\"")
return workfile_path
def get_workfile_settings(self):
return self.data["project_settings"]["celaction"]["workfile"]

View file

@ -39,7 +39,7 @@ class CollectCelactionCliKwargs(pyblish.api.Collector):
passing_kwargs[key] = value
if missing_kwargs:
raise RuntimeError("Missing arguments {}".format(
self.log.debug("Missing arguments {}".format(
", ".join(
[f'"{key}"' for key in missing_kwargs]
)

View file

@ -1,10 +1,14 @@
from .addon import (
get_fusion_version,
FusionAddon,
FUSION_HOST_DIR,
FUSION_VERSIONS_DICT,
)
__all__ = (
"get_fusion_version",
"FusionAddon",
"FUSION_HOST_DIR",
"FUSION_VERSIONS_DICT",
)

View file

@ -1,8 +1,52 @@
import os
import re
from openpype.modules import OpenPypeModule, IHostAddon
from openpype.lib import Logger
FUSION_HOST_DIR = os.path.dirname(os.path.abspath(__file__))
# FUSION_VERSIONS_DICT is used by the pre-launch hooks
# The keys correspond to all currently supported Fusion versions
# Each value is a list of corresponding Python home variables and a profile
# number, which is used by the profile hook to set Fusion profile variables.
FUSION_VERSIONS_DICT = {
9: ("FUSION_PYTHON36_HOME", 9),
16: ("FUSION16_PYTHON36_HOME", 16),
17: ("FUSION16_PYTHON36_HOME", 16),
18: ("FUSION_PYTHON3_HOME", 16),
}
def get_fusion_version(app_name):
"""
The function is triggered by the prelaunch hooks to get the fusion version.
`app_name` is obtained by prelaunch hooks from the
`launch_context.env.get("AVALON_APP_NAME")`.
To get a correct Fusion version, a version number should be present
in the `applications/fusion/variants` key
of the Blackmagic Fusion Application Settings.
"""
log = Logger.get_logger(__name__)
if not app_name:
return
app_version_candidates = re.findall(r"\d+", app_name)
if not app_version_candidates:
return
for app_version in app_version_candidates:
if int(app_version) in FUSION_VERSIONS_DICT:
return int(app_version)
else:
log.info(
"Unsupported Fusion version: {app_version}".format(
app_version=app_version
)
)
class FusionAddon(OpenPypeModule, IHostAddon):
name = "fusion"
@ -14,15 +58,11 @@ class FusionAddon(OpenPypeModule, IHostAddon):
def get_launch_hook_paths(self, app):
if app.host_name != self.host_name:
return []
return [
os.path.join(FUSION_HOST_DIR, "hooks")
]
return [os.path.join(FUSION_HOST_DIR, "hooks")]
def add_implementation_envs(self, env, _app):
# Set default values if are not already set via settings
defaults = {
"OPENPYPE_LOG_NO_COLORS": "Yes"
}
defaults = {"OPENPYPE_LOG_NO_COLORS": "Yes"}
for key, value in defaults.items():
if not env.get(key):
env[key] = value

View file

@ -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))
)

View file

@ -303,10 +303,18 @@ def get_frame_path(path):
return filename, padding, ext
def get_current_comp():
"""Hack to get current comp in this session"""
def get_fusion_module():
"""Get current Fusion instance"""
fusion = getattr(sys.modules["__main__"], "fusion", None)
return fusion.CurrentComp if fusion else None
return fusion
def get_current_comp():
"""Get current comp in this session"""
fusion = get_fusion_module()
if fusion is not None:
comp = fusion.CurrentComp
return comp
@contextlib.contextmanager

View file

@ -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()

View file

@ -1,19 +1,19 @@
{
Locked = true,
Global = {
Paths = {
Map = {
["OpenPype:"] = "$(OPENPYPE_FUSION)/deploy",
["Reactor:"] = "$(REACTOR)",
["Config:"] = "UserPaths:Config;OpenPype:Config",
["Scripts:"] = "UserPaths:Scripts;Reactor:System/Scripts;OpenPype:Scripts",
["UserPaths:"] = "UserData:;AllData:;Fusion:;Reactor:Deploy"
},
},
Script = {
PythonVersion = 3,
Python3Forced = true
},
Paths = {
Map = {
["OpenPype:"] = "$(OPENPYPE_FUSION)/deploy",
["Config:"] = "UserPaths:Config;OpenPype:Config",
["Scripts:"] = "UserPaths:Scripts;Reactor:System/Scripts;OpenPype:Scripts",
},
}
},
Script = {
PythonVersion = 3,
Python3Forced = true
},
UserInterface = {
Language = "en_US"
},
},
}

View file

@ -0,0 +1,161 @@
import os
import shutil
import platform
from pathlib import Path
from openpype.lib import PreLaunchHook, ApplicationLaunchFailed
from openpype.hosts.fusion import (
FUSION_HOST_DIR,
FUSION_VERSIONS_DICT,
get_fusion_version,
)
class FusionCopyPrefsPrelaunch(PreLaunchHook):
"""
Prepares local Fusion profile directory, copies existing Fusion profile.
This also sets FUSION MasterPrefs variable, which is used
to apply Master.prefs file to override some Fusion profile settings to:
- enable the OpenPype menu
- force Python 3 over Python 2
- force English interface
Master.prefs is defined in openpype/hosts/fusion/deploy/fusion_shared.prefs
"""
app_groups = ["fusion"]
order = 2
def get_fusion_profile_name(self, profile_version) -> str:
# Returns 'Default', unless FUSION16_PROFILE is set
return os.getenv(f"FUSION{profile_version}_PROFILE", "Default")
def get_fusion_profile_dir(self, profile_version) -> Path:
# Get FUSION_PROFILE_DIR variable
fusion_profile = self.get_fusion_profile_name(profile_version)
fusion_var_prefs_dir = os.getenv(
f"FUSION{profile_version}_PROFILE_DIR"
)
# Check if FUSION_PROFILE_DIR exists
if fusion_var_prefs_dir and Path(fusion_var_prefs_dir).is_dir():
fu_prefs_dir = Path(fusion_var_prefs_dir, fusion_profile)
self.log.info(f"{fusion_var_prefs_dir} is set to {fu_prefs_dir}")
return fu_prefs_dir
def get_profile_source(self, profile_version) -> Path:
"""Get Fusion preferences profile location.
See Per-User_Preferences_and_Paths on VFXpedia for reference.
"""
fusion_profile = self.get_fusion_profile_name(profile_version)
profile_source = self.get_fusion_profile_dir(profile_version)
if profile_source:
return profile_source
# otherwise get default location of the profile folder
fu_prefs_dir = f"Blackmagic Design/Fusion/Profiles/{fusion_profile}"
if platform.system() == "Windows":
profile_source = Path(os.getenv("AppData"), fu_prefs_dir)
elif platform.system() == "Darwin":
profile_source = Path(
"~/Library/Application Support/", fu_prefs_dir
).expanduser()
elif platform.system() == "Linux":
profile_source = Path("~/.fusion", fu_prefs_dir).expanduser()
self.log.info(
f"Locating source Fusion prefs directory: {profile_source}"
)
return profile_source
def get_copy_fusion_prefs_settings(self):
# Get copy preferences options from the global application settings
copy_fusion_settings = self.data["project_settings"]["fusion"].get(
"copy_fusion_settings", {}
)
if not copy_fusion_settings:
self.log.error("Copy prefs settings not found")
copy_status = copy_fusion_settings.get("copy_status", False)
force_sync = copy_fusion_settings.get("force_sync", False)
copy_path = copy_fusion_settings.get("copy_path") or None
if copy_path:
copy_path = Path(copy_path).expanduser()
return copy_status, copy_path, force_sync
def copy_fusion_profile(
self, copy_from: Path, copy_to: Path, force_sync: bool
) -> None:
"""On the first Fusion launch copy the contents of Fusion profile
directory to the working predefined location. If the Openpype profile
folder exists, skip copying, unless re-sync is checked.
If the prefs were not copied on the first launch,
clean Fusion profile will be created in fu_profile_dir.
"""
if copy_to.exists() and not force_sync:
self.log.info(
"Destination Fusion preferences folder already exists: "
f"{copy_to} "
)
return
self.log.info("Starting copying Fusion preferences")
self.log.debug(f"force_sync option is set to {force_sync}")
try:
copy_to.mkdir(exist_ok=True, parents=True)
except PermissionError:
self.log.warning(f"Creating the folder not permitted at {copy_to}")
return
if not copy_from.exists():
self.log.warning(f"Fusion preferences not found in {copy_from}")
return
for file in copy_from.iterdir():
if file.suffix in (
".prefs",
".def",
".blocklist",
".fu",
".toolbars",
):
# convert Path to str to be compatible with Python 3.6+
shutil.copy(str(file), str(copy_to))
self.log.info(
f"Successfully copied preferences: {copy_from} to {copy_to}"
)
def execute(self):
(
copy_status,
fu_profile_dir,
force_sync,
) = self.get_copy_fusion_prefs_settings()
# Get launched application context and return correct app version
app_name = self.launch_context.env.get("AVALON_APP_NAME")
app_version = get_fusion_version(app_name)
if app_version is None:
version_names = ", ".join(str(x) for x in FUSION_VERSIONS_DICT)
raise ApplicationLaunchFailed(
"Unable to detect valid Fusion version number from app "
f"name: {app_name}.\nMake sure to include at least a digit "
"to indicate the Fusion version like '18'.\n"
f"Detectable Fusion versions are: {version_names}"
)
_, profile_version = FUSION_VERSIONS_DICT[app_version]
fu_profile = self.get_fusion_profile_name(profile_version)
# do a copy of Fusion profile if copy_status toggle is enabled
if copy_status and fu_profile_dir is not None:
profile_source = self.get_profile_source(profile_version)
dest_folder = Path(fu_profile_dir, fu_profile)
self.copy_fusion_profile(profile_source, dest_folder, force_sync)
# Add temporary profile directory variables to customize Fusion
# to define where it can read custom scripts and tools from
fu_profile_dir_variable = f"FUSION{profile_version}_PROFILE_DIR"
self.log.info(f"Setting {fu_profile_dir_variable}: {fu_profile_dir}")
self.launch_context.env[fu_profile_dir_variable] = str(fu_profile_dir)
# Add custom Fusion Master Prefs and the temporary
# profile directory variables to customize Fusion
# to define where it can read custom scripts and tools from
master_prefs_variable = f"FUSION{profile_version}_MasterPrefs"
master_prefs = Path(FUSION_HOST_DIR, "deploy", "fusion_shared.prefs")
self.log.info(f"Setting {master_prefs_variable}: {master_prefs}")
self.launch_context.env[master_prefs_variable] = str(master_prefs)

View file

@ -1,32 +1,43 @@
import os
from openpype.lib import PreLaunchHook, ApplicationLaunchFailed
from openpype.hosts.fusion import FUSION_HOST_DIR
from openpype.hosts.fusion import (
FUSION_HOST_DIR,
FUSION_VERSIONS_DICT,
get_fusion_version,
)
class FusionPrelaunch(PreLaunchHook):
"""Prepares OpenPype Fusion environment
Requires FUSION_PYTHON3_HOME to be defined in the environment for Fusion
to point at a valid Python 3 build for Fusion. That is Python 3.3-3.10
for Fusion 18 and Fusion 3.6 for Fusion 16 and 17.
This also sets FUSION16_MasterPrefs to apply the fusion master prefs
as set in openpype/hosts/fusion/deploy/fusion_shared.prefs to enable
the OpenPype menu and force Python 3 over Python 2.
"""
Prepares OpenPype Fusion environment.
Requires correct Python home variable to be defined in the environment
settings for Fusion to point at a valid Python 3 build for Fusion.
Python3 versions that are supported by Fusion:
Fusion 9, 16, 17 : Python 3.6
Fusion 18 : Python 3.6 - 3.10
"""
app_groups = ["fusion"]
order = 1
def execute(self):
# making sure python 3 is installed at provided path
# Py 3.3-3.10 for Fusion 18+ or Py 3.6 for Fu 16-17
py3_var = "FUSION_PYTHON3_HOME"
app_data = self.launch_context.env.get("AVALON_APP_NAME")
app_version = get_fusion_version(app_data)
if not app_version:
raise ApplicationLaunchFailed(
"Fusion version information not found in System settings.\n"
"The key field in the 'applications/fusion/variants' should "
"consist a number, corresponding to major Fusion version."
)
py3_var, _ = FUSION_VERSIONS_DICT[app_version]
fusion_python3_home = self.launch_context.env.get(py3_var, "")
self.log.info(f"Looking for Python 3 in: {fusion_python3_home}")
for path in fusion_python3_home.split(os.pathsep):
# Allow defining multiple paths to allow "fallback" to other
# path. But make to set only a single path as final variable.
# Allow defining multiple paths, separated by os.pathsep,
# to allow "fallback" to other path.
# But make to set only a single path as final variable.
py3_dir = os.path.normpath(path)
if os.path.isdir(py3_dir):
break
@ -43,19 +54,10 @@ class FusionPrelaunch(PreLaunchHook):
self.launch_context.env[py3_var] = py3_dir
# Fusion 18+ requires FUSION_PYTHON3_HOME to also be on PATH
self.launch_context.env["PATH"] += ";" + py3_dir
if app_version >= 18:
self.launch_context.env["PATH"] += os.pathsep + py3_dir
# Fusion 16 and 17 use FUSION16_PYTHON36_HOME instead of
# FUSION_PYTHON3_HOME and will only work with a Python 3.6 version
# TODO: Detect Fusion version to only set for specific Fusion build
self.launch_context.env["FUSION16_PYTHON36_HOME"] = py3_dir
self.launch_context.env[py3_var] = py3_dir
# Add our Fusion Master Prefs which is the only way to customize
# Fusion to define where it can read custom scripts and tools from
self.log.info(f"Setting OPENPYPE_FUSION: {FUSION_HOST_DIR}")
self.launch_context.env["OPENPYPE_FUSION"] = FUSION_HOST_DIR
pref_var = "FUSION16_MasterPrefs" # used by Fusion 16, 17 and 18
prefs = os.path.join(FUSION_HOST_DIR, "deploy", "fusion_shared.prefs")
self.log.info(f"Setting {pref_var}: {prefs}")
self.launch_context.env[pref_var] = prefs

View file

@ -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",
)

View file

@ -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)

View file

@ -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)

View 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)
)

View 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")

View file

@ -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")

View file

@ -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):

View file

@ -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"
)

View file

@ -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

View file

@ -1221,7 +1221,7 @@ def set_track_color(track_item, color):
def check_inventory_versions(track_items=None):
"""
Actual version color idetifier of Loaded containers
Actual version color identifier of Loaded containers
Check all track items and filter only
Loader nodes for its version. It will get all versions from database
@ -1249,10 +1249,10 @@ def check_inventory_versions(track_items=None):
project_name = legacy_io.active_project()
filter_result = filter_containers(containers, project_name)
for container in filter_result.latest:
set_track_color(container["_item"], clip_color)
set_track_color(container["_item"], clip_color_last)
for container in filter_result.outdated:
set_track_color(container["_item"], clip_color_last)
set_track_color(container["_item"], clip_color)
def selection_changed_timeline(event):

View file

@ -146,6 +146,8 @@ class CreatorWidget(QtWidgets.QDialog):
return " ".join([str(m.group(0)).capitalize() for m in matches])
def create_row(self, layout, type, text, **kwargs):
value_keys = ["setText", "setCheckState", "setValue", "setChecked"]
# get type attribute from qwidgets
attr = getattr(QtWidgets, type)
@ -167,14 +169,27 @@ class CreatorWidget(QtWidgets.QDialog):
# assign the created attribute to variable
item = getattr(self, attr_name)
# set attributes to item which are not values
for func, val in kwargs.items():
if func in value_keys:
continue
if getattr(item, func):
log.debug("Setting {} to {}".format(func, val))
func_attr = getattr(item, func)
if isinstance(val, tuple):
func_attr(*val)
else:
func_attr(val)
# set values to item
for value_item in value_keys:
if value_item not in kwargs:
continue
if getattr(item, value_item):
getattr(item, value_item)(kwargs[value_item])
# add to layout
layout.addRow(label, item)
@ -276,8 +291,11 @@ class CreatorWidget(QtWidgets.QDialog):
elif v["type"] == "QSpinBox":
data[k]["value"] = self.create_row(
content_layout, "QSpinBox", v["label"],
setValue=v["value"], setMinimum=0,
setValue=v["value"],
setDisplayIntegerBase=10000,
setRange=(0, 99999), setMinimum=0,
setMaximum=100000, setToolTip=tool_tip)
return data

View file

@ -10,7 +10,7 @@
import hou
from openpype.tools.utils import host_tools
parent = hou.qt.mainWindow()
host_tools.show_creator(parent)
host_tools.show_publisher(parent, tab="create")
]]></scriptCode>
</scriptItem>
@ -30,7 +30,7 @@ host_tools.show_loader(parent=parent, use_context=True)
import hou
from openpype.tools.utils import host_tools
parent = hou.qt.mainWindow()
host_tools.show_publisher(parent)
host_tools.show_publisher(parent, tab="publish")
]]></scriptCode>
</scriptItem>
@ -66,8 +66,8 @@ host_tools.show_workfiles(parent)
]]></scriptCode>
</scriptItem>
<scriptItem id="reset_frame_range">
<label>Reset Frame Range</label>
<scriptItem id="set_frame_range">
<label>Set Frame Range</label>
<scriptCode><![CDATA[
import openpype.hosts.houdini.api.lib
openpype.hosts.houdini.api.lib.reset_framerange()

View file

@ -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,112 @@ 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 get_frame_range() -> dict:
"""Get the current assets frame range and handles.
Returns:
dict: with frame start, frame end, handle start, handle end.
"""
# Set frame start/end
asset = get_current_project_asset()
frame_start = asset["data"].get("frameStart")
frame_end = asset["data"].get("frameEnd")
# Backwards compatibility
if frame_start is None or frame_end is None:
frame_start = asset["data"].get("edit_in")
frame_end = asset["data"].get("edit_out")
if frame_start is None or frame_end is None:
return
handles = asset["data"].get("handles") or 0
handle_start = asset["data"].get("handleStart")
if handle_start is None:
handle_start = handles
handle_end = asset["data"].get("handleEnd")
if handle_end is None:
handle_end = handles
return {
"frameStart": frame_start,
"frameEnd": frame_end,
"handleStart": handle_start,
"handleEnd": handle_end
}
def reset_frame_range(fps: bool = True):
"""Set frame range to current asset.
This is part of 3dsmax documentation:
animationRange: A System Global variable which lets you get and
set an Interval value that defines the start and end frames
of the Active Time Segment.
frameRate: A System Global variable which lets you get
and set an Integer value that defines the current
scene frame rate in frames-per-second.
"""
if fps:
data_fps = get_current_project(fields=["data.fps"])
fps_number = float(data_fps["data"]["fps"])
rt.frameRate = fps_number
frame_range = get_frame_range()
frame_start = frame_range["frameStart"] - int(frame_range["handleStart"])
frame_end = frame_range["frameEnd"] + int(frame_range["handleEnd"])
frange_cmd = f"animationRange = interval {frame_start} {frame_end}"
rt.execute(frange_cmd)
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:

View file

@ -8,6 +8,7 @@ from openpype.hosts.max.api.lib import (
get_current_renderer,
get_default_render_folder
)
from openpype.pipeline.context_tools import get_current_project_asset
from openpype.settings import get_project_settings
from openpype.pipeline import legacy_io
@ -34,14 +35,20 @@ class RenderProducts(object):
filename,
container)
context = get_current_project_asset()
startFrame = context["data"].get("frameStart")
endFrame = context["data"].get("frameEnd") + 1
img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa
full_render_list = []
beauty = self.beauty_render_product(output_file, img_fmt)
full_render_list.append(beauty)
full_render_list = self.beauty_render_product(output_file,
startFrame,
endFrame,
img_fmt)
renderer_class = get_current_renderer()
renderer = str(renderer_class).split(":")[0]
if renderer == "VUE_File_Renderer":
return full_render_list
@ -54,6 +61,8 @@ class RenderProducts(object):
"Quicksilver_Hardware_Renderer",
]:
render_elem_list = self.render_elements_product(output_file,
startFrame,
endFrame,
img_fmt)
if render_elem_list:
full_render_list.extend(iter(render_elem_list))
@ -61,18 +70,24 @@ class RenderProducts(object):
if renderer == "Arnold":
aov_list = self.arnold_render_product(output_file,
startFrame,
endFrame,
img_fmt)
if aov_list:
full_render_list.extend(iter(aov_list))
return full_render_list
def beauty_render_product(self, folder, fmt):
beauty_output = f"{folder}.####.{fmt}"
beauty_output = beauty_output.replace("\\", "/")
return beauty_output
def beauty_render_product(self, folder, startFrame, endFrame, fmt):
beauty_frame_range = []
for f in range(startFrame, endFrame):
beauty_output = f"{folder}.{f}.{fmt}"
beauty_output = beauty_output.replace("\\", "/")
beauty_frame_range.append(beauty_output)
return beauty_frame_range
# TODO: Get the arnold render product
def arnold_render_product(self, folder, fmt):
def arnold_render_product(self, folder, startFrame, endFrame, fmt):
"""Get all the Arnold AOVs"""
aovs = []
@ -85,15 +100,17 @@ class RenderProducts(object):
for i in range(aov_group_num):
# get the specific AOV group
for aov in aov_mgr.drivers[i].aov_list:
render_element = f"{folder}_{aov.name}.####.{fmt}"
render_element = render_element.replace("\\", "/")
aovs.append(render_element)
for f in range(startFrame, endFrame):
render_element = f"{folder}_{aov.name}.{f}.{fmt}"
render_element = render_element.replace("\\", "/")
aovs.append(render_element)
# close the AOVs manager window
amw.close()
return aovs
def render_elements_product(self, folder, fmt):
def render_elements_product(self, folder, startFrame, endFrame, fmt):
"""Get all the render element output files. """
render_dirname = []
@ -104,9 +121,10 @@ class RenderProducts(object):
renderlayer_name = render_elem.GetRenderElement(i)
target, renderpass = str(renderlayer_name).split(":")
if renderlayer_name.enabled:
render_element = f"{folder}_{renderpass}.####.{fmt}"
render_element = render_element.replace("\\", "/")
render_dirname.append(render_element)
for f in range(startFrame, endFrame):
render_element = f"{folder}_{renderpass}.{f}.{fmt}"
render_element = render_element.replace("\\", "/")
render_dirname.append(render_element)
return render_dirname

View file

@ -4,6 +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):
@ -107,6 +108,17 @@ 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)
frame_action = QtWidgets.QAction("Set Frame Range", openpype_menu)
frame_action.triggered.connect(self.frame_range_callback)
openpype_menu.addAction(frame_action)
return openpype_menu
def load_callback(self):
@ -128,3 +140,11 @@ 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()
def frame_range_callback(self):
"""Callback to reset frame range"""
return lib.reset_frame_range()

View file

@ -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

View file

@ -101,7 +101,9 @@ class MaxCreator(Creator, MaxCreatorBase):
instance_node = rt.getNodeByName(
instance.data.get("instance_node"))
if instance_node:
rt.delete(rt.getNodeByName(instance_node))
rt.select(instance_node)
rt.execute(f'for o in selection do for c in o.children do c.parent = undefined') # noqa
rt.delete(instance_node)
self._remove_instance_from_context(instance)

View file

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""Creator plugin for creating raw max scene."""
from openpype.hosts.max.api import plugin
from openpype.pipeline import CreatedInstance
class CreateMaxScene(plugin.MaxCreator):
identifier = "io.openpype.creators.max.maxScene"
label = "Max Scene"
family = "maxScene"
icon = "gear"
def create(self, subset_name, instance_data, pre_create_data):
from pymxs import runtime as rt
sel_obj = list(rt.selection)
instance = super(CreateMaxScene, self).create(
subset_name,
instance_data,
pre_create_data) # type: CreatedInstance
container = rt.getNodeByName(instance.data.get("instance_node"))
# TODO: Disable "Add to Containers?" Panel
# parent the selected cameras into the container
for obj in sel_obj:
obj.parent = container
# for additional work on the node:
# instance_node = rt.getNodeByName(instance.get("instance_node"))

View file

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""Creator plugin for creating point cloud."""
from openpype.hosts.max.api import plugin
from openpype.pipeline import CreatedInstance
class CreatePointCloud(plugin.MaxCreator):
identifier = "io.openpype.creators.max.pointcloud"
label = "Point Cloud"
family = "pointcloud"
icon = "gear"
def create(self, subset_name, instance_data, pre_create_data):
from pymxs import runtime as rt
sel_obj = list(rt.selection)
instance = super(CreatePointCloud, self).create(
subset_name,
instance_data,
pre_create_data) # type: CreatedInstance
container = rt.getNodeByName(instance.data.get("instance_node"))
# TODO: Disable "Add to Containers?" Panel
# parent the selected cameras into the container
for obj in sel_obj:
obj.parent = container
# for additional work on the node:
# instance_node = rt.getNodeByName(instance.get("instance_node"))

View file

@ -9,7 +9,8 @@ from openpype.hosts.max.api import lib
class MaxSceneLoader(load.LoaderPlugin):
"""Max Scene Loader"""
families = ["camera"]
families = ["camera",
"maxScene"]
representations = ["max"]
order = -8
icon = "code-fork"
@ -46,8 +47,7 @@ class MaxSceneLoader(load.LoaderPlugin):
path = get_representation_path(representation)
node = rt.getNodeByName(container["instance_node"])
max_objects = self.get_container_children(node)
max_objects = node.Children
for max_object in max_objects:
max_object.source = path

View file

@ -0,0 +1,51 @@
import os
from openpype.pipeline import (
load, get_representation_path
)
from openpype.hosts.max.api.pipeline import containerise
from openpype.hosts.max.api import lib
class PointCloudLoader(load.LoaderPlugin):
"""Point Cloud Loader"""
families = ["pointcloud"]
representations = ["prt"]
order = -8
icon = "code-fork"
color = "green"
def load(self, context, name=None, namespace=None, data=None):
"""load point cloud by tyCache"""
from pymxs import runtime as rt
filepath = os.path.normpath(self.fname)
obj = rt.tyCache()
obj.filename = filepath
prt_container = rt.getNodeByName(f"{obj.name}")
return containerise(
name, [prt_container], context, loader=self.__class__.__name__)
def update(self, container, representation):
"""update the container"""
from pymxs import runtime as rt
path = get_representation_path(representation)
node = rt.getNodeByName(container["instance_node"])
prt_objects = self.get_container_children(node)
for prt_object in prt_objects:
prt_object.source = path
lib.imprint(container["instance_node"], {
"representation": str(representation["_id"])
})
def remove(self, container):
"""remove the container"""
from pymxs import runtime as rt
node = rt.getNodeByName(container["instance_node"])
rt.delete(node)

View file

@ -61,7 +61,7 @@ class CollectRender(pyblish.api.InstancePlugin):
"plugin": "3dsmax",
"frameStart": context.data['frameStart'],
"frameEnd": context.data['frameEnd'],
"version": version_int
"version": version_int,
}
self.log.info("data: {0}".format(data))
instance.data.update(data)

View file

@ -20,7 +20,8 @@ class ExtractMaxSceneRaw(publish.Extractor,
order = pyblish.api.ExtractorOrder - 0.2
label = "Extract Max Scene (Raw)"
hosts = ["max"]
families = ["camera"]
families = ["camera",
"maxScene"]
optional = True
def process(self, instance):

View file

@ -0,0 +1,207 @@
import os
import pyblish.api
from openpype.pipeline import publish
from pymxs import runtime as rt
from openpype.hosts.max.api import (
maintained_selection
)
from openpype.settings import get_project_settings
from openpype.pipeline import legacy_io
def get_setting(project_setting=None):
project_setting = get_project_settings(
legacy_io.Session["AVALON_PROJECT"]
)
return (project_setting["max"]["PointCloud"])
class ExtractPointCloud(publish.Extractor):
"""
Extract PRT format with tyFlow operators
Notes:
Currently only works for the default partition setting
Args:
export_particle(): sets up all job arguments for attributes
to be exported in MAXscript
get_operators(): get the export_particle operator
get_custom_attr(): get all custom channel attributes from Openpype
setting and sets it as job arguments before exporting
get_files(): get the files with tyFlow naming convention
before publishing
partition_output_name(): get the naming with partition settings.
get_partition(): get partition value
"""
order = pyblish.api.ExtractorOrder - 0.2
label = "Extract Point Cloud"
hosts = ["max"]
families = ["pointcloud"]
def process(self, instance):
start = int(instance.context.data.get("frameStart"))
end = int(instance.context.data.get("frameEnd"))
container = instance.data["instance_node"]
self.log.info("Extracting PRT...")
stagingdir = self.staging_dir(instance)
filename = "{name}.prt".format(**instance.data)
path = os.path.join(stagingdir, filename)
with maintained_selection():
job_args = self.export_particle(container,
start,
end,
path)
for job in job_args:
rt.execute(job)
self.log.info("Performing Extraction ...")
if "representations" not in instance.data:
instance.data["representations"] = []
self.log.info("Writing PRT with TyFlow Plugin...")
filenames = self.get_files(container, path, start, end)
self.log.debug("filenames: {0}".format(filenames))
partition = self.partition_output_name(container)
representation = {
'name': 'prt',
'ext': 'prt',
'files': filenames if len(filenames) > 1 else filenames[0],
"stagingDir": stagingdir,
"outputName": partition # partition value
}
instance.data["representations"].append(representation)
self.log.info("Extracted instance '%s' to: %s" % (instance.name,
path))
def export_particle(self,
container,
start,
end,
filepath):
job_args = []
opt_list = self.get_operators(container)
for operator in opt_list:
start_frame = "{0}.frameStart={1}".format(operator,
start)
job_args.append(start_frame)
end_frame = "{0}.frameEnd={1}".format(operator,
end)
job_args.append(end_frame)
filepath = filepath.replace("\\", "/")
prt_filename = '{0}.PRTFilename="{1}"'.format(operator,
filepath)
job_args.append(prt_filename)
# Partition
mode = "{0}.PRTPartitionsMode=2".format(operator)
job_args.append(mode)
additional_args = self.get_custom_attr(operator)
for args in additional_args:
job_args.append(args)
prt_export = "{0}.exportPRT()".format(operator)
job_args.append(prt_export)
return job_args
def get_operators(self, container):
"""Get Export Particles Operator"""
opt_list = []
node = rt.getNodebyName(container)
selection_list = list(node.Children)
for sel in selection_list:
obj = sel.baseobject
# TODO: to see if it can be used maxscript instead
anim_names = rt.getsubanimnames(obj)
for anim_name in anim_names:
sub_anim = rt.getsubanim(obj, anim_name)
boolean = rt.isProperty(sub_anim, "Export_Particles")
event_name = sub_anim.name
if boolean:
opt = "${0}.{1}.export_particles".format(sel.name,
event_name)
opt_list.append(opt)
return opt_list
def get_custom_attr(self, operator):
"""Get Custom Attributes"""
custom_attr_list = []
attr_settings = get_setting()["attribute"]
for key, value in attr_settings.items():
custom_attr = "{0}.PRTChannels_{1}=True".format(operator,
value)
self.log.debug(
"{0} will be added as custom attribute".format(key)
)
custom_attr_list.append(custom_attr)
return custom_attr_list
def get_files(self,
container,
path,
start_frame,
end_frame):
"""
Note:
Set the filenames accordingly to the tyFlow file
naming extension for the publishing purpose
Actual File Output from tyFlow:
<SceneFile>__part<PartitionStart>of<PartitionCount>.<frame>.prt
e.g. tyFlow_cloth_CCCS_blobbyFill_001__part1of1_00004.prt
"""
filenames = []
filename = os.path.basename(path)
orig_name, ext = os.path.splitext(filename)
partition_count, partition_start = self.get_partition(container)
for frame in range(int(start_frame), int(end_frame) + 1):
actual_name = "{}__part{:03}of{}_{:05}".format(orig_name,
partition_start,
partition_count,
frame)
actual_filename = path.replace(orig_name, actual_name)
filenames.append(os.path.basename(actual_filename))
return filenames
def partition_output_name(self, container):
"""
Notes:
Partition output name set for mapping
the published file output
todo:
Customizes the setting for the output
"""
partition_count, partition_start = self.get_partition(container)
partition = "_part{:03}of{}".format(partition_start,
partition_count)
return partition
def get_partition(self, container):
"""
Get Partition Value
"""
opt_list = self.get_operators(container)
for operator in opt_list:
count = rt.execute(f'{operator}.PRTPartitionsCount')
start = rt.execute(f'{operator}.PRTPartitionsFrom')
return count, start

View file

@ -0,0 +1,19 @@
import pyblish.api
from openpype.lib import version_up
from pymxs import runtime as rt
class IncrementWorkfileVersion(pyblish.api.ContextPlugin):
"""Increment current workfile version."""
order = pyblish.api.IntegratorOrder + 0.9
label = "Increment Workfile Version"
hosts = ["max"]
families = ["workfile"]
def process(self, context):
path = context.data["currentFile"]
filepath = version_up(path)
rt.saveMaxFile(filepath)
self.log.info("Incrementing file version")

View file

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
import pyblish.api
from openpype.pipeline import PublishValidationError
from pymxs import runtime as rt
class ValidateMaxContents(pyblish.api.InstancePlugin):
"""Validates Max contents.
Check if MaxScene container includes any contents underneath.
"""
order = pyblish.api.ValidatorOrder
families = ["camera",
"maxScene",
"maxrender"]
hosts = ["max"]
label = "Max Scene Contents"
def process(self, instance):
container = rt.getNodeByName(instance.data["instance_node"])
if not list(container.Children):
raise PublishValidationError("No content found in the container")

View file

@ -0,0 +1,191 @@
import pyblish.api
from openpype.pipeline import PublishValidationError
from pymxs import runtime as rt
from openpype.settings import get_project_settings
from openpype.pipeline import legacy_io
def get_setting(project_setting=None):
project_setting = get_project_settings(
legacy_io.Session["AVALON_PROJECT"]
)
return (project_setting["max"]["PointCloud"])
class ValidatePointCloud(pyblish.api.InstancePlugin):
"""Validate that workfile was saved."""
order = pyblish.api.ValidatorOrder
families = ["pointcloud"]
hosts = ["max"]
label = "Validate Point Cloud"
def process(self, instance):
"""
Notes:
1. Validate the container only include tyFlow objects
2. Validate if tyFlow operator Export Particle exists
3. Validate if the export mode of Export Particle is at PRT format
4. Validate the partition count and range set as default value
Partition Count : 100
Partition Range : 1 to 1
5. Validate if the custom attribute(s) exist as parameter(s)
of export_particle operator
"""
invalid = self.get_tyFlow_object(instance)
if invalid:
raise PublishValidationError("Non tyFlow object "
"found: {}".format(invalid))
invalid = self.get_tyFlow_operator(instance)
if invalid:
raise PublishValidationError("tyFlow ExportParticle operator "
"not found: {}".format(invalid))
invalid = self.validate_export_mode(instance)
if invalid:
raise PublishValidationError("The export mode is not at PRT")
invalid = self.validate_partition_value(instance)
if invalid:
raise PublishValidationError("tyFlow Partition setting is "
"not at the default value")
invalid = self.validate_custom_attribute(instance)
if invalid:
raise PublishValidationError("Custom Attribute not found "
":{}".format(invalid))
def get_tyFlow_object(self, instance):
invalid = []
container = instance.data["instance_node"]
self.log.info("Validating tyFlow container "
"for {}".format(container))
con = rt.getNodeByName(container)
selection_list = list(con.Children)
for sel in selection_list:
sel_tmp = str(sel)
if rt.classOf(sel) in [rt.tyFlow,
rt.Editable_Mesh]:
if "tyFlow" not in sel_tmp:
invalid.append(sel)
else:
invalid.append(sel)
return invalid
def get_tyFlow_operator(self, instance):
invalid = []
container = instance.data["instance_node"]
self.log.info("Validating tyFlow object "
"for {}".format(container))
con = rt.getNodeByName(container)
selection_list = list(con.Children)
bool_list = []
for sel in selection_list:
obj = sel.baseobject
anim_names = rt.getsubanimnames(obj)
for anim_name in anim_names:
# get all the names of the related tyFlow nodes
sub_anim = rt.getsubanim(obj, anim_name)
# check if there is export particle operator
boolean = rt.isProperty(sub_anim, "Export_Particles")
bool_list.append(str(boolean))
# if the export_particles property is not there
# it means there is not a "Export Particle" operator
if "True" not in bool_list:
self.log.error("Operator 'Export Particles' not found!")
invalid.append(sel)
return invalid
def validate_custom_attribute(self, instance):
invalid = []
container = instance.data["instance_node"]
self.log.info("Validating tyFlow custom "
"attributes for {}".format(container))
con = rt.getNodeByName(container)
selection_list = list(con.Children)
for sel in selection_list:
obj = sel.baseobject
anim_names = rt.getsubanimnames(obj)
for anim_name in anim_names:
# get all the names of the related tyFlow nodes
sub_anim = rt.getsubanim(obj, anim_name)
# check if there is export particle operator
boolean = rt.isProperty(sub_anim, "Export_Particles")
event_name = sub_anim.name
if boolean:
opt = "${0}.{1}.export_particles".format(sel.name,
event_name)
attributes = get_setting()["attribute"]
for key, value in attributes.items():
custom_attr = "{0}.PRTChannels_{1}".format(opt,
value)
try:
rt.execute(custom_attr)
except RuntimeError:
invalid.add(key)
return invalid
def validate_partition_value(self, instance):
invalid = []
container = instance.data["instance_node"]
self.log.info("Validating tyFlow partition "
"value for {}".format(container))
con = rt.getNodeByName(container)
selection_list = list(con.Children)
for sel in selection_list:
obj = sel.baseobject
anim_names = rt.getsubanimnames(obj)
for anim_name in anim_names:
# get all the names of the related tyFlow nodes
sub_anim = rt.getsubanim(obj, anim_name)
# check if there is export particle operator
boolean = rt.isProperty(sub_anim, "Export_Particles")
event_name = sub_anim.name
if boolean:
opt = "${0}.{1}.export_particles".format(sel.name,
event_name)
count = rt.execute(f'{opt}.PRTPartitionsCount')
if count != 100:
invalid.append(count)
start = rt.execute(f'{opt}.PRTPartitionsFrom')
if start != 1:
invalid.append(start)
end = rt.execute(f'{opt}.PRTPartitionsTo')
if end != 1:
invalid.append(end)
return invalid
def validate_export_mode(self, instance):
invalid = []
container = instance.data["instance_node"]
self.log.info("Validating tyFlow export "
"mode for {}".format(container))
con = rt.getNodeByName(container)
selection_list = list(con.Children)
for sel in selection_list:
obj = sel.baseobject
anim_names = rt.getsubanimnames(obj)
for anim_name in anim_names:
# get all the names of the related tyFlow nodes
sub_anim = rt.getsubanim(obj, anim_name)
# check if there is export particle operator
boolean = rt.isProperty(sub_anim, "Export_Particles")
event_name = sub_anim.name
if boolean:
opt = "${0}.{1}.export_particles".format(sel.name,
event_name)
export_mode = rt.execute(f'{opt}.exportMode')
if export_mode != 1:
invalid.append(export_mode)
return invalid

View file

@ -11,6 +11,7 @@ import maya.mel as mel
from openpype import resources
from openpype.tools.utils import host_tools
from .lib import get_main_window
from ..tools import show_look_assigner
log = logging.getLogger(__name__)
@ -112,7 +113,7 @@ def override_toolbox_ui():
annotation="Look Manager",
label="Look Manager",
image=os.path.join(icons, "lookmanager.png"),
command=host_tools.show_look_assigner,
command=show_look_assigner,
width=icon_size,
height=icon_size,
parent=parent

View file

@ -2099,29 +2099,40 @@ def get_frame_range():
}
def reset_frame_range():
"""Set frame range to current asset"""
def reset_frame_range(playback=True, render=True, fps=True):
"""Set frame range to current asset
fps = convert_to_maya_fps(
float(legacy_io.Session.get("AVALON_FPS", 25))
)
set_scene_fps(fps)
Args:
playback (bool, Optional): Whether to set the maya timeline playback
frame range. Defaults to True.
render (bool, Optional): Whether to set the maya render frame range.
Defaults to True.
fps (bool, Optional): Whether to set scene FPS. Defaults to True.
"""
if fps:
fps = convert_to_maya_fps(
float(legacy_io.Session.get("AVALON_FPS", 25))
)
set_scene_fps(fps)
frame_range = get_frame_range()
frame_start = frame_range["frameStart"] - int(frame_range["handleStart"])
frame_end = frame_range["frameEnd"] + int(frame_range["handleEnd"])
cmds.playbackOptions(minTime=frame_start)
cmds.playbackOptions(maxTime=frame_end)
cmds.playbackOptions(animationStartTime=frame_start)
cmds.playbackOptions(animationEndTime=frame_end)
cmds.playbackOptions(minTime=frame_start)
cmds.playbackOptions(maxTime=frame_end)
cmds.currentTime(frame_start)
if playback:
cmds.playbackOptions(minTime=frame_start)
cmds.playbackOptions(maxTime=frame_end)
cmds.playbackOptions(animationStartTime=frame_start)
cmds.playbackOptions(animationEndTime=frame_end)
cmds.playbackOptions(minTime=frame_start)
cmds.playbackOptions(maxTime=frame_end)
cmds.currentTime(frame_start)
cmds.setAttr("defaultRenderGlobals.startFrame", frame_start)
cmds.setAttr("defaultRenderGlobals.endFrame", frame_end)
if render:
cmds.setAttr("defaultRenderGlobals.startFrame", frame_start)
cmds.setAttr("defaultRenderGlobals.endFrame", frame_end)
def reset_scene_resolution():
@ -3576,6 +3587,65 @@ def get_color_management_output_transform():
return colorspace
def image_info(file_path):
# type: (str) -> dict
"""Based on tha texture path, get its bit depth and format information.
Take reference from makeTx.py in Arnold:
ImageInfo(filename): Get Image Information for colorspace
AiTextureGetFormat(filename): Get Texture Format
AiTextureGetBitDepth(filename): Get Texture bit depth
Args:
file_path (str): Path to the texture file.
Returns:
dict: Dictionary with the information about the texture file.
"""
from arnold import (
AiTextureGetBitDepth,
AiTextureGetFormat
)
# Get Texture Information
img_info = {'filename': file_path}
if os.path.isfile(file_path):
img_info['bit_depth'] = AiTextureGetBitDepth(file_path) # noqa
img_info['format'] = AiTextureGetFormat(file_path) # noqa
else:
img_info['bit_depth'] = 8
img_info['format'] = "unknown"
return img_info
def guess_colorspace(img_info):
# type: (dict) -> str
"""Guess the colorspace of the input image filename.
Note:
Reference from makeTx.py
Args:
img_info (dict): Image info generated by :func:`image_info`
Returns:
str: color space name use in the `--colorconvert`
option of maketx.
"""
from arnold import (
AiTextureInvalidate,
# types
AI_TYPE_BYTE,
AI_TYPE_INT,
AI_TYPE_UINT
)
try:
if img_info['bit_depth'] <= 16:
if img_info['format'] in (AI_TYPE_BYTE, AI_TYPE_INT, AI_TYPE_UINT): # noqa
return 'sRGB'
else:
return 'linear'
# now discard the image file as AiTextureGetFormat has loaded it
AiTextureInvalidate(img_info['filename']) # noqa
except ValueError:
print(("[maketx] Error: Could not guess"
"colorspace for {}").format(img_info["filename"]))
return "linear"
def len_flattened(components):
"""Return the length of the list as if it was flattened.

View file

@ -158,7 +158,7 @@ class RenderSettings(object):
cmds.setAttr(
"defaultArnoldDriver.mergeAOVs", multi_exr)
self._additional_attribs_setter(additional_options)
reset_frame_range()
reset_frame_range(playback=False, fps=False, render=True)
def _set_redshift_settings(self, width, height):
"""Sets settings for Redshift."""
@ -336,7 +336,8 @@ class RenderSettings(object):
)
# Set render file format to exr
cmds.setAttr("{}.imageFormatStr".format(node), "exr", type="string")
ext = vray_render_presets["image_format"]
cmds.setAttr("{}.imageFormatStr".format(node), ext, type="string")
# animType
cmds.setAttr("{}.animType".format(node), 1)

View file

@ -12,6 +12,7 @@ from openpype.pipeline.workfile import BuildWorkfile
from openpype.tools.utils import host_tools
from openpype.hosts.maya.api import lib, lib_rendersettings
from .lib import get_main_window, IS_HEADLESS
from ..tools import show_look_assigner
from .workfile_template_builder import (
create_placeholder,
@ -111,12 +112,12 @@ def install():
)
cmds.menuItem(
"Reset Frame Range",
"Set Frame Range",
command=lambda *args: lib.reset_frame_range()
)
cmds.menuItem(
"Reset Resolution",
"Set Resolution",
command=lambda *args: lib.reset_scene_resolution()
)
@ -139,7 +140,7 @@ def install():
cmds.menuItem(
"Look assigner...",
command=lambda *args: host_tools.show_look_assigner(
command=lambda *args: show_look_assigner(
parent_widget
)
)

View file

@ -13,6 +13,7 @@ class CreateAnimation(plugin.Creator):
icon = "male"
write_color_sets = False
write_face_sets = False
include_parent_hierarchy = False
include_user_defined_attributes = False
def __init__(self, *args, **kwargs):
@ -37,7 +38,7 @@ class CreateAnimation(plugin.Creator):
self.data["visibleOnly"] = False
# Include the groups above the out_SET content
self.data["includeParentHierarchy"] = False # Include parent groups
self.data["includeParentHierarchy"] = self.include_parent_hierarchy
# Default to exporting world-space
self.data["worldSpace"] = True

View file

@ -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

View file

@ -134,7 +134,7 @@ class ConnectGeometry(InventoryAction):
bool
"""
from Qt import QtWidgets
from qtpy import QtWidgets
accept = QtWidgets.QMessageBox.Ok
if show_cancel:

View file

@ -149,7 +149,7 @@ class ConnectXgen(InventoryAction):
bool
"""
from Qt import QtWidgets
from qtpy import QtWidgets
accept = QtWidgets.QMessageBox.Ok
if show_cancel:

View file

@ -0,0 +1,178 @@
import os
import json
from collections import defaultdict
from maya import cmds
from openpype.pipeline import (
InventoryAction, get_representation_context, get_representation_path
)
from openpype.hosts.maya.api.lib import get_container_members, get_id
class ConnectYetiRig(InventoryAction):
"""Connect Yeti Rig with an animation or pointcache."""
label = "Connect Yeti Rig"
icon = "link"
color = "white"
def process(self, containers):
# Validate selection is more than 1.
message = (
"Only 1 container selected. 2+ containers needed for this action."
)
if len(containers) == 1:
self.display_warning(message)
return
# Categorize containers by family.
containers_by_family = defaultdict(list)
for container in containers:
family = get_representation_context(
container["representation"]
)["subset"]["data"]["family"]
containers_by_family[family].append(container)
# Validate to only 1 source container.
source_containers = containers_by_family.get("animation", [])
source_containers += containers_by_family.get("pointcache", [])
source_container_namespaces = [
x["namespace"] for x in source_containers
]
message = (
"{} animation containers selected:\n\n{}\n\nOnly select 1 of type "
"\"animation\" or \"pointcache\".".format(
len(source_containers), source_container_namespaces
)
)
if len(source_containers) != 1:
self.display_warning(message)
return
source_container = source_containers[0]
source_ids = self.nodes_by_id(source_container)
# Target containers.
target_ids = {}
inputs = []
yeti_rig_containers = containers_by_family.get("yetiRig")
if not yeti_rig_containers:
self.display_warning(
"Select at least one yetiRig container"
)
return
for container in yeti_rig_containers:
target_ids.update(self.nodes_by_id(container))
maya_file = get_representation_path(
get_representation_context(
container["representation"]
)["representation"]
)
_, ext = os.path.splitext(maya_file)
settings_file = maya_file.replace(ext, ".rigsettings")
if not os.path.exists(settings_file):
continue
with open(settings_file) as f:
inputs.extend(json.load(f)["inputs"])
# Compare loaded connections to scene.
for input in inputs:
source_node = source_ids.get(input["sourceID"])
target_node = target_ids.get(input["destinationID"])
if not source_node or not target_node:
self.log.debug(
"Could not find nodes for input:\n" +
json.dumps(input, indent=4, sort_keys=True)
)
continue
source_attr, target_attr = input["connections"]
if not cmds.attributeQuery(
source_attr, node=source_node, exists=True
):
self.log.debug(
"Could not find attribute {} on node {} for "
"input:\n{}".format(
source_attr,
source_node,
json.dumps(input, indent=4, sort_keys=True)
)
)
continue
if not cmds.attributeQuery(
target_attr, node=target_node, exists=True
):
self.log.debug(
"Could not find attribute {} on node {} for "
"input:\n{}".format(
target_attr,
target_node,
json.dumps(input, indent=4, sort_keys=True)
)
)
continue
source_plug = "{}.{}".format(
source_node, source_attr
)
target_plug = "{}.{}".format(
target_node, target_attr
)
if cmds.isConnected(
source_plug, target_plug, ignoreUnitConversion=True
):
self.log.debug(
"Connection already exists: {} -> {}".format(
source_plug, target_plug
)
)
continue
cmds.connectAttr(source_plug, target_plug, force=True)
self.log.debug(
"Connected attributes: {} -> {}".format(
source_plug, target_plug
)
)
def nodes_by_id(self, container):
ids = {}
for member in get_container_members(container):
id = get_id(member)
if not id:
continue
ids[id] = member
return ids
def display_warning(self, message, show_cancel=False):
"""Show feedback to user.
Returns:
bool
"""
from qtpy import QtWidgets
accept = QtWidgets.QMessageBox.Ok
if show_cancel:
buttons = accept | QtWidgets.QMessageBox.Cancel
else:
buttons = accept
state = QtWidgets.QMessageBox.warning(
None,
"",
message,
buttons=buttons,
defaultButton=accept
)
return state == accept

View file

@ -2,7 +2,6 @@ import os
import clique
import maya.cmds as cmds
import mtoa.ui.arnoldmenu
from openpype.settings import get_project_settings
from openpype.pipeline import (
@ -36,6 +35,11 @@ class ArnoldStandinLoader(load.LoaderPlugin):
color = "orange"
def load(self, context, name, namespace, options):
# Make sure to load arnold before importing `mtoa.ui.arnoldmenu`
cmds.loadPlugin("mtoa", quiet=True)
import mtoa.ui.arnoldmenu
version = context['version']
version_data = version.get("data", {})

View file

@ -0,0 +1,332 @@
import os
import copy
from openpype.lib import EnumDef
from openpype.pipeline import (
load,
get_representation_context
)
from openpype.pipeline.load.utils import get_representation_path_from_context
from openpype.pipeline.colorspace import (
get_imageio_colorspace_from_filepath,
get_imageio_config,
get_imageio_file_rules
)
from openpype.settings import get_project_settings
from openpype.hosts.maya.api.pipeline import containerise
from openpype.hosts.maya.api.lib import (
unique_namespace,
namespaced
)
from maya import cmds
def create_texture():
"""Create place2dTexture with file node with uv connections
Mimics Maya "file [Texture]" creation.
"""
place = cmds.shadingNode("place2dTexture", asUtility=True, name="place2d")
file = cmds.shadingNode("file", asTexture=True, name="file")
connections = ["coverage", "translateFrame", "rotateFrame", "rotateUV",
"mirrorU", "mirrorV", "stagger", "wrapV", "wrapU",
"repeatUV", "offset", "noiseUV", "vertexUvThree",
"vertexUvTwo", "vertexUvOne", "vertexCameraOne"]
for attr in connections:
src = "{}.{}".format(place, attr)
dest = "{}.{}".format(file, attr)
cmds.connectAttr(src, dest)
cmds.connectAttr(place + '.outUV', file + '.uvCoord')
cmds.connectAttr(place + '.outUvFilterSize', file + '.uvFilterSize')
return file, place
def create_projection():
"""Create texture with place3dTexture and projection
Mimics Maya "file [Projection]" creation.
"""
file, place = create_texture()
projection = cmds.shadingNode("projection", asTexture=True,
name="projection")
place3d = cmds.shadingNode("place3dTexture", asUtility=True,
name="place3d")
cmds.connectAttr(place3d + '.worldInverseMatrix[0]',
projection + ".placementMatrix")
cmds.connectAttr(file + '.outColor', projection + ".image")
return file, place, projection, place3d
def create_stencil():
"""Create texture with extra place2dTexture offset and stencil
Mimics Maya "file [Stencil]" creation.
"""
file, place = create_texture()
place_stencil = cmds.shadingNode("place2dTexture", asUtility=True,
name="place2d_stencil")
stencil = cmds.shadingNode("stencil", asTexture=True, name="stencil")
for src_attr, dest_attr in [
("outUV", "uvCoord"),
("outUvFilterSize", "uvFilterSize")
]:
src_plug = "{}.{}".format(place_stencil, src_attr)
cmds.connectAttr(src_plug, "{}.{}".format(place, dest_attr))
cmds.connectAttr(src_plug, "{}.{}".format(stencil, dest_attr))
return file, place, stencil, place_stencil
class FileNodeLoader(load.LoaderPlugin):
"""File node loader."""
families = ["image", "plate", "render"]
label = "Load file node"
representations = ["exr", "tif", "png", "jpg"]
icon = "image"
color = "orange"
order = 2
options = [
EnumDef(
"mode",
items={
"texture": "Texture",
"projection": "Projection",
"stencil": "Stencil"
},
default="texture",
label="Texture Mode"
)
]
def load(self, context, name, namespace, data):
asset = context['asset']['name']
namespace = namespace or unique_namespace(
asset + "_",
prefix="_" if asset[0].isdigit() else "",
suffix="_",
)
with namespaced(namespace, new=True) as namespace:
# Create the nodes within the namespace
nodes = {
"texture": create_texture,
"projection": create_projection,
"stencil": create_stencil
}[data.get("mode", "texture")]()
file_node = cmds.ls(nodes, type="file")[0]
self._apply_representation_context(context, file_node)
# For ease of access for the user select all the nodes and select
# the file node last so that UI shows its attributes by default
cmds.select(list(nodes) + [file_node], replace=True)
return containerise(
name=name,
namespace=namespace,
nodes=nodes,
context=context,
loader=self.__class__.__name__
)
def update(self, container, representation):
members = cmds.sets(container['objectName'], query=True)
file_node = cmds.ls(members, type="file")[0]
context = get_representation_context(representation)
self._apply_representation_context(context, file_node)
# Update representation
cmds.setAttr(
container["objectName"] + ".representation",
str(representation["_id"]),
type="string"
)
def switch(self, container, representation):
self.update(container, representation)
def remove(self, container):
members = cmds.sets(container['objectName'], query=True)
cmds.lockNode(members, lock=False)
cmds.delete([container['objectName']] + members)
# Clean up the namespace
try:
cmds.namespace(removeNamespace=container['namespace'],
deleteNamespaceContent=True)
except RuntimeError:
pass
def _apply_representation_context(self, context, file_node):
"""Update the file node to match the context.
This sets the file node's attributes for:
- file path
- udim tiling mode (if it is an udim tile)
- use frame extension (if it is a sequence)
- colorspace
"""
repre_context = context["representation"]["context"]
has_frames = repre_context.get("frame") is not None
has_udim = repre_context.get("udim") is not None
# Set UV tiling mode if UDIM tiles
if has_udim:
cmds.setAttr(file_node + ".uvTilingMode", 3) # UDIM-tiles
else:
cmds.setAttr(file_node + ".uvTilingMode", 0) # off
# Enable sequence if publish has `startFrame` and `endFrame` and
# `startFrame != endFrame`
if has_frames and self._is_sequence(context):
# When enabling useFrameExtension maya automatically
# connects an expression to <file>.frameExtension to set
# the current frame. However, this expression is generated
# with some delay and thus it'll show a warning if frame 0
# doesn't exist because we're explicitly setting the <f>
# token.
cmds.setAttr(file_node + ".useFrameExtension", True)
else:
cmds.setAttr(file_node + ".useFrameExtension", False)
# Set the file node path attribute
path = self._format_path(context)
cmds.setAttr(file_node + ".fileTextureName", path, type="string")
# Set colorspace
colorspace = self._get_colorspace(context)
if colorspace:
cmds.setAttr(file_node + ".colorSpace", colorspace, type="string")
else:
self.log.debug("Unknown colorspace - setting colorspace skipped.")
def _is_sequence(self, context):
"""Check whether frameStart and frameEnd are not the same."""
version = context.get("version", {})
representation = context.get("representation", {})
for doc in [representation, version]:
# Frame range can be set on version or representation.
# When set on representation it overrides version data.
data = doc.get("data", {})
start = data.get("frameStartHandle", data.get("frameStart", None))
end = data.get("frameEndHandle", data.get("frameEnd", None))
if start is None or end is None:
continue
if start != end:
return True
else:
return False
return False
def _get_colorspace(self, context):
"""Return colorspace of the file to load.
Retrieves the explicit colorspace from the publish. If no colorspace
data is stored with published content then project imageio settings
are used to make an assumption of the colorspace based on the file
rules. If no file rules match then None is returned.
Returns:
str or None: The colorspace of the file or None if not detected.
"""
# We can't apply color spaces if management is not enabled
if not cmds.colorManagementPrefs(query=True, cmEnabled=True):
return
representation = context["representation"]
colorspace_data = representation.get("data", {}).get("colorspaceData")
if colorspace_data:
return colorspace_data["colorspace"]
# Assume colorspace from filepath based on project settings
project_name = context["project"]["name"]
host_name = os.environ.get("AVALON_APP")
project_settings = get_project_settings(project_name)
config_data = get_imageio_config(
project_name, host_name,
project_settings=project_settings
)
file_rules = get_imageio_file_rules(
project_name, host_name,
project_settings=project_settings
)
path = get_representation_path_from_context(context)
colorspace = get_imageio_colorspace_from_filepath(
path=path,
host_name=host_name,
project_name=project_name,
config_data=config_data,
file_rules=file_rules,
project_settings=project_settings
)
return colorspace
def _format_path(self, context):
"""Format the path with correct tokens for frames and udim tiles."""
context = copy.deepcopy(context)
representation = context["representation"]
template = representation.get("data", {}).get("template")
if not template:
# No template to find token locations for
return get_representation_path_from_context(context)
def _placeholder(key):
# Substitute with a long placeholder value so that potential
# custom formatting with padding doesn't find its way into
# our formatting, so that <f> wouldn't be padded as 0<f>
return "___{}___".format(key)
# We format UDIM and Frame numbers with their specific tokens. To do so
# we in-place change the representation context data to format the path
# with our own data
tokens = {
"frame": "<f>",
"udim": "<UDIM>"
}
has_tokens = False
repre_context = representation["context"]
for key, _token in tokens.items():
if key in repre_context:
repre_context[key] = _placeholder(key)
has_tokens = True
# Replace with our custom template that has the tokens set
representation["data"]["template"] = template
path = get_representation_path_from_context(context)
if has_tokens:
for key, token in tokens.items():
if key in repre_context:
path = path.replace(_placeholder(key), token)
return path

View file

@ -3,7 +3,7 @@ import os
import maya.cmds as cmds
import xgenm
from Qt import QtWidgets
from qtpy import QtWidgets
import openpype.hosts.maya.api.plugin
from openpype.hosts.maya.api.lib import (

View file

@ -1,17 +1,12 @@
import os
from collections import defaultdict
import maya.cmds as cmds
from openpype.settings import get_project_settings
from openpype.settings import get_current_project_settings
import openpype.hosts.maya.api.plugin
from openpype.hosts.maya.api import lib
class YetiRigLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
"""
This loader will load Yeti rig. You can select something in scene and if it
has same ID as mesh published with rig, their shapes will be linked
together.
"""
"""This loader will load Yeti rig."""
families = ["yetiRig"]
representations = ["ma"]
@ -22,72 +17,31 @@ class YetiRigLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
color = "orange"
def process_reference(
self, context, name=None, namespace=None, options=None):
self, context, name=None, namespace=None, options=None
):
import maya.cmds as cmds
# get roots of selected hierarchies
selected_roots = []
for sel in cmds.ls(sl=True, long=True):
selected_roots.append(sel.split("|")[1])
# get all objects under those roots
selected_hierarchy = []
for root in selected_roots:
selected_hierarchy.append(cmds.listRelatives(
root,
allDescendents=True) or [])
# flatten the list and filter only shapes
shapes_flat = []
for root in selected_hierarchy:
shapes = cmds.ls(root, long=True, type="mesh") or []
for shape in shapes:
shapes_flat.append(shape)
# create dictionary of cbId and shape nodes
scene_lookup = defaultdict(list)
for node in shapes_flat:
cb_id = lib.get_id(node)
scene_lookup[cb_id] = node
# load rig
group_name = "{}:{}".format(namespace, name)
with lib.maintained_selection():
file_url = self.prepare_root_value(self.fname,
context["project"]["name"])
nodes = cmds.file(file_url,
namespace=namespace,
reference=True,
returnNewNodes=True,
groupReference=True,
groupName="{}:{}".format(namespace, name))
file_url = self.prepare_root_value(
self.fname, context["project"]["name"]
)
nodes = cmds.file(
file_url,
namespace=namespace,
reference=True,
returnNewNodes=True,
groupReference=True,
groupName=group_name
)
# for every shape node we've just loaded find matching shape by its
# cbId in selection. If found outMesh of scene shape will connect to
# inMesh of loaded shape.
for destination_node in nodes:
source_node = scene_lookup[lib.get_id(destination_node)]
if source_node:
self.log.info("found: {}".format(source_node))
self.log.info(
"creating connection to {}".format(destination_node))
cmds.connectAttr("{}.outMesh".format(source_node),
"{}.inMesh".format(destination_node),
force=True)
groupName = "{}:{}".format(namespace, name)
settings = get_project_settings(os.environ['AVALON_PROJECT'])
colors = settings['maya']['load']['colors']
c = colors.get('yetiRig')
settings = get_current_project_settings()
colors = settings["maya"]["load"]["colors"]
c = colors.get("yetiRig")
if c is not None:
cmds.setAttr(groupName + ".useOutlinerColor", 1)
cmds.setAttr(groupName + ".outlinerColor",
(float(c[0])/255),
(float(c[1])/255),
(float(c[2])/255)
cmds.setAttr(group_name + ".useOutlinerColor", 1)
cmds.setAttr(
group_name + ".outlinerColor",
(float(c[0]) / 255), (float(c[1]) / 255), (float(c[2]) / 255)
)
self[:] = nodes

View file

@ -5,6 +5,7 @@ import pyblish.api
from openpype.client import get_subset_by_name
from openpype.pipeline import legacy_io
from openpype.hosts.maya.api.lib import get_attribute_input
class CollectReview(pyblish.api.InstancePlugin):
@ -23,6 +24,11 @@ class CollectReview(pyblish.api.InstancePlugin):
task = legacy_io.Session["AVALON_TASK"]
# Get panel.
instance.data["panel"] = cmds.playblast(
activeEditor=True
).split("|")[-1]
# get cameras
members = instance.data['setMembers']
cameras = cmds.ls(members, long=True,
@ -74,6 +80,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)
@ -139,3 +147,21 @@ class CollectReview(pyblish.api.InstancePlugin):
"filename": node.filename.get()
}
)
# Collect focal length.
attr = camera + ".focalLength"
focal_length = None
if get_attribute_input(attr):
start = instance.data["frameStart"]
end = instance.data["frameEnd"] + 1
focal_length = [
cmds.getAttr(attr, time=t) for t in range(int(start), int(end))
]
else:
focal_length = cmds.getAttr(attr)
key = "focalLength"
try:
instance.data["burninDataMembers"][key] = focal_length
except KeyError:
instance.data["burninDataMembers"] = {key: focal_length}

View file

@ -1,12 +1,10 @@
# -*- coding: utf-8 -*-
"""Maya look extractor."""
import os
import sys
import json
import tempfile
import platform
import contextlib
import subprocess
from collections import OrderedDict
from maya import cmds # noqa
@ -16,40 +14,20 @@ import pyblish.api
from openpype.lib import source_hash, run_subprocess
from openpype.pipeline import legacy_io, publish
from openpype.hosts.maya.api import lib
from openpype.hosts.maya.api.lib import image_info, guess_colorspace
# Modes for transfer
COPY = 1
HARDLINK = 2
def escape_space(path):
"""Ensure path is enclosed by quotes to allow paths with spaces"""
return '"{}"'.format(path) if " " in path else path
def get_ocio_config_path(profile_folder):
"""Path to OpenPype vendorized OCIO.
Vendorized OCIO config file path is grabbed from the specific path
hierarchy specified below.
"{OPENPYPE_ROOT}/vendor/OpenColorIO-Configs/{profile_folder}/config.ocio"
Args:
profile_folder (str): Name of folder to grab config file from.
Returns:
str: Path to vendorized config file.
"""
return os.path.join(
os.environ["OPENPYPE_ROOT"],
"vendor",
"bin",
"ocioconfig",
"OpenColorIOConfigs",
profile_folder,
"config.ocio"
)
def _has_arnold():
"""Return whether the arnold package is available and can be imported."""
try:
import arnold # noqa: F401
return True
except (ImportError, ModuleNotFoundError):
return False
def find_paths_by_hash(texture_hash):
@ -367,16 +345,25 @@ class ExtractLook(publish.Extractor):
for filepath in files_metadata:
linearize = False
if do_maketx and files_metadata[filepath]["color_space"].lower() == "srgb": # noqa: E501
linearize = True
# set its file node to 'raw' as tx will be linearized
files_metadata[filepath]["color_space"] = "Raw"
# if OCIO color management enabled
# it won't take the condition of the files_metadata
ocio_maya = cmds.colorManagementPrefs(q=True,
cmConfigFileEnabled=True,
cmEnabled=True)
if do_maketx and not ocio_maya:
if files_metadata[filepath]["color_space"].lower() == "srgb": # noqa: E501
linearize = True
# set its file node to 'raw' as tx will be linearized
files_metadata[filepath]["color_space"] = "Raw"
# if do_maketx:
# color_space = "Raw"
source, mode, texture_hash = self._process_texture(
filepath,
resource,
do_maketx,
staging=staging_dir,
linearize=linearize,
@ -482,7 +469,8 @@ class ExtractLook(publish.Extractor):
resources_dir, basename + ext
)
def _process_texture(self, filepath, do_maketx, staging, linearize, force):
def _process_texture(self, filepath, resource,
do_maketx, staging, linearize, force):
"""Process a single texture file on disk for publishing.
This will:
1. Check whether it's already published, if so it will do hardlink
@ -524,10 +512,47 @@ class ExtractLook(publish.Extractor):
texture_hash
]
if linearize:
self.log.info("tx: converting sRGB -> linear")
additional_args.extend(["--colorconvert", "sRGB", "linear"])
if cmds.colorManagementPrefs(query=True, cmEnabled=True):
render_colorspace = cmds.colorManagementPrefs(query=True,
renderingSpaceName=True) # noqa
config_path = cmds.colorManagementPrefs(query=True,
configFilePath=True) # noqa
if not os.path.exists(config_path):
raise RuntimeError("No OCIO config path found!")
color_space_attr = resource["node"] + ".colorSpace"
try:
color_space = cmds.getAttr(color_space_attr)
except ValueError:
# node doesn't have color space attribute
if _has_arnold():
img_info = image_info(filepath)
color_space = guess_colorspace(img_info)
else:
color_space = "Raw"
self.log.info("tx: converting {0} -> {1}".format(color_space, render_colorspace)) # noqa
additional_args.extend(["--colorconvert",
color_space,
render_colorspace])
else:
if _has_arnold():
img_info = image_info(filepath)
color_space = guess_colorspace(img_info)
if color_space == "sRGB":
self.log.info("tx: converting sRGB -> linear")
additional_args.extend(["--colorconvert",
"sRGB",
"Raw"])
else:
self.log.info("tx: texture's colorspace "
"is already linear")
else:
self.log.warning("cannot guess the colorspace"
"color conversion won't be available!") # noqa
config_path = get_ocio_config_path("nuke-default")
additional_args.extend(["--colorconfig", config_path])
# Ensure folder exists
if not os.path.exists(os.path.dirname(converted)):

View file

@ -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,11 +135,11 @@ 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.
panel = cmds.getPanel(withFocus=True)
keys = [
"useDefaultMaterial",
"wireframeOnShaded",
@ -129,56 +150,67 @@ class ExtractPlayblast(publish.Extractor):
viewport_defaults = {}
for key in keys:
viewport_defaults[key] = cmds.modelEditor(
panel, query=True, **{key: True}
instance.data["panel"], query=True, **{key: True}
)
if preset["viewport_options"][key]:
cmds.modelEditor(panel, edit=True, **{key: True})
cmds.modelEditor(
instance.data["panel"], edit=True, **{key: True}
)
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
panel = cmds.getPanel(withFocus=True) or ""
if not override_viewport_options and "modelPanel" in panel:
panel_preset = capture.parse_active_view()
panel_preset.pop("camera")
preset.update(panel_preset)
cmds.setFocus(panel)
# 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.
cmds.modelEditor(panel, edit=True, **viewport_defaults)
if viewport_defaults:
cmds.modelEditor(
instance.data["panel"], edit=True, **viewport_defaults
)
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
@ -202,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)

View file

@ -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
}

View file

@ -0,0 +1,26 @@
from maya import cmds
import pyblish.api
from openpype.pipeline.publish import ValidateContentsOrder
from openpype.pipeline import PublishValidationError
class ValidateMayaColorSpace(pyblish.api.InstancePlugin):
"""
Check if the OCIO Color Management and maketx options
enabled at the same time
"""
order = ValidateContentsOrder
families = ['look']
hosts = ['maya']
label = 'Color Management with maketx'
def process(self, instance):
ocio_maya = cmds.colorManagementPrefs(q=True,
cmConfigFileEnabled=True,
cmEnabled=True)
maketx = instance.data["maketx"]
if ocio_maya and maketx:
raise PublishValidationError("Maya is color managed and maketx option is on. OpenPype doesn't support this combination yet.") # noqa

View file

@ -13,6 +13,22 @@ from openpype.pipeline.publish import (
from openpype.hosts.maya.api import lib
def convert_to_int_or_float(string_value):
# Order of types are important here since float can convert string
# representation of integer.
types = [int, float]
for t in types:
try:
result = t(string_value)
except ValueError:
continue
else:
return result
# Neither integer or float.
return string_value
def get_redshift_image_format_labels():
"""Return nice labels for Redshift image formats."""
var = "$g_redshiftImageFormatLabels"
@ -242,10 +258,6 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
cls.DEFAULT_PADDING, "0" * cls.DEFAULT_PADDING))
# load validation definitions from settings
validation_settings = (
instance.context.data["project_settings"]["maya"]["publish"]["ValidateRenderSettings"].get( # noqa: E501
"{}_render_attributes".format(renderer)) or []
)
settings_lights_flag = instance.context.data["project_settings"].get(
"maya", {}).get(
"RenderSettings", {}).get(
@ -253,17 +265,67 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
instance_lights_flag = instance.data.get("renderSetupIncludeLights")
if settings_lights_flag != instance_lights_flag:
cls.log.warning('Instance flag for "Render Setup Include Lights" is set to {0} and Settings flag is set to {1}'.format(instance_lights_flag, settings_lights_flag)) # noqa
cls.log.warning(
"Instance flag for \"Render Setup Include Lights\" is set to "
"{} and Settings flag is set to {}".format(
instance_lights_flag, settings_lights_flag
)
)
# go through definitions and test if such node.attribute exists.
# if so, compare its value from the one required.
for attr, value in OrderedDict(validation_settings).items():
cls.log.debug("{}: {}".format(attr, value))
if "." not in attr:
cls.log.warning("Skipping invalid attribute defined in "
"validation settings: '{}'".format(attr))
for attribute, data in cls.get_nodes(instance, renderer).items():
# Validate the settings has values.
if not data["values"]:
cls.log.error(
"Settings for {}.{} is missing values.".format(
node, attribute
)
)
continue
for node in data["nodes"]:
try:
render_value = cmds.getAttr(
"{}.{}".format(node, attribute)
)
except RuntimeError:
invalid = True
cls.log.error(
"Cannot get value of {}.{}".format(node, attribute)
)
else:
if render_value not in data["values"]:
invalid = True
cls.log.error(
"Invalid value {} set on {}.{}. Expecting "
"{}".format(
render_value, node, attribute, data["values"]
)
)
return invalid
@classmethod
def get_nodes(cls, instance, renderer):
maya_settings = instance.context.data["project_settings"]["maya"]
validation_settings = (
maya_settings["publish"]["ValidateRenderSettings"].get(
"{}_render_attributes".format(renderer)
) or []
)
result = {}
for attr, values in OrderedDict(validation_settings).items():
cls.log.debug("{}: {}".format(attr, values))
if "." not in attr:
cls.log.warning(
"Skipping invalid attribute defined in validation "
"settings: \"{}\"".format(attr)
)
continue
values = [convert_to_int_or_float(v) for v in values]
node_type, attribute_name = attr.split(".", 1)
# first get node of that type
@ -271,28 +333,13 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
if not nodes:
cls.log.warning(
"No nodes of type '{}' found.".format(node_type))
"No nodes of type \"{}\" found.".format(node_type)
)
continue
for node in nodes:
try:
render_value = cmds.getAttr(
"{}.{}".format(node, attribute_name))
except RuntimeError:
invalid = True
cls.log.error(
"Cannot get value of {}.{}".format(
node, attribute_name))
else:
if str(value) != str(render_value):
invalid = True
cls.log.error(
("Invalid value {} set on {}.{}. "
"Expecting {}").format(
render_value, node, attribute_name, value)
)
result[attribute_name] = {"nodes": nodes, "values": values}
return invalid
return result
@classmethod
def repair(cls, instance):
@ -305,6 +352,12 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
"{aov_separator}", instance.data.get("aovSeparator", "_")
)
for attribute, data in cls.get_nodes(instance, renderer).items():
if not data["values"]:
continue
for node in data["nodes"]:
lib.set_attribute(attribute, data["values"][0], node)
with lib.renderlayer(layer_node):
default = lib.RENDER_ATTRS['default']
render_attrs = lib.RENDER_ATTRS.get(renderer, default)

View file

@ -48,6 +48,18 @@ class ValidateYetiRenderScriptCallbacks(pyblish.api.InstancePlugin):
yeti_loaded = cmds.pluginInfo("pgYetiMaya", query=True, loaded=True)
if not yeti_loaded and not cmds.ls(type="pgYetiMaya"):
# The yeti plug-in is available and loaded so at
# this point we don't really care whether the scene
# has any yeti callback set or not since if the callback
# is there it wouldn't error and if it weren't then
# nothing happens because there are no yeti nodes.
cls.log.info(
"Yeti is loaded but no yeti nodes were found. "
"Callback validation skipped.."
)
return False
renderer = instance.data["renderer"]
if renderer == "redshift":
cls.log.info("Redshift ignores any pre and post render callbacks")

View file

@ -0,0 +1,27 @@
from openpype.tools.utils.host_tools import qt_app_context
class MayaToolsSingleton:
_look_assigner = None
def get_look_assigner_tool(parent):
"""Create, cache and return look assigner tool window."""
if MayaToolsSingleton._look_assigner is None:
from .mayalookassigner import MayaLookAssignerWindow
mayalookassigner_window = MayaLookAssignerWindow(parent)
MayaToolsSingleton._look_assigner = mayalookassigner_window
return MayaToolsSingleton._look_assigner
def show_look_assigner(parent=None):
"""Look manager is Maya specific tool for look management."""
with qt_app_context():
look_assigner_tool = get_look_assigner_tool(parent)
look_assigner_tool.show()
# Pull window to the front.
look_assigner_tool.raise_()
look_assigner_tool.activateWindow()
look_assigner_tool.showNormal()

View file

@ -80,21 +80,7 @@ def get_all_asset_nodes():
Returns:
list: list of dictionaries
"""
host = registered_host()
nodes = []
for container in host.ls():
# We are not interested in looks but assets!
if container["loader"] == "LookLoader":
continue
# Gather all information
container_name = container["objectName"]
nodes += lib.get_container_members(container_name)
nodes = list(set(nodes))
return nodes
return cmds.ls(dag=True, noIntermediate=True, long=True)
def create_asset_id_hash(nodes):

View file

@ -2861,10 +2861,10 @@ class NukeDirmap(HostDirmap):
pass
def dirmap_routine(self, source_path, destination_path):
log.debug("{}: {}->{}".format(self.file_name,
source_path, destination_path))
source_path = source_path.lower().replace(os.sep, '/')
destination_path = destination_path.lower().replace(os.sep, '/')
log.debug("Map: {} with: {}->{}".format(self.file_name,
source_path, destination_path))
if platform.system().lower() == "windows":
self.file_name = self.file_name.lower().replace(
source_path, destination_path)
@ -2878,6 +2878,7 @@ class DirmapCache:
_project_name = None
_project_settings = None
_sync_module = None
_mapping = None
@classmethod
def project_name(cls):
@ -2897,6 +2898,36 @@ class DirmapCache:
cls._sync_module = ModulesManager().modules_by_name["sync_server"]
return cls._sync_module
@classmethod
def mapping(cls):
return cls._mapping
@classmethod
def set_mapping(cls, mapping):
cls._mapping = mapping
def dirmap_file_name_filter(file_name):
"""Nuke callback function with single full path argument.
Checks project settings for potential mapping from source to dest.
"""
dirmap_processor = NukeDirmap(
file_name,
"nuke",
DirmapCache.project_name(),
DirmapCache.project_settings(),
DirmapCache.sync_module(),
)
if not DirmapCache.mapping():
DirmapCache.set_mapping(dirmap_processor.get_mappings())
dirmap_processor.process_dirmap(DirmapCache.mapping())
if os.path.exists(dirmap_processor.file_name):
return dirmap_processor.file_name
return file_name
@contextlib.contextmanager
def node_tempfile():
@ -2942,25 +2973,6 @@ def duplicate_node(node):
return dupli_node
def dirmap_file_name_filter(file_name):
"""Nuke callback function with single full path argument.
Checks project settings for potential mapping from source to dest.
"""
dirmap_processor = NukeDirmap(
file_name,
"nuke",
DirmapCache.project_name(),
DirmapCache.project_settings(),
DirmapCache.sync_module(),
)
dirmap_processor.process_dirmap()
if os.path.exists(dirmap_processor.file_name):
return dirmap_processor.file_name
return file_name
def get_group_io_nodes(nodes):
"""Get the input and the output of a group of nodes."""

View file

@ -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}

View file

@ -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))

View file

@ -25,7 +25,7 @@ class ExtractReviewData(publish.Extractor):
# review can be removed since `ProcessSubmittedJobOnFarm` will create
# reviewable representation if needed
if (
"render.farm" in instance.data["families"]
instance.data.get("farm")
and "review" in instance.data["families"]
):
instance.data["families"].remove("review")

View file

@ -49,7 +49,12 @@ class ExtractReviewDataLut(publish.Extractor):
exporter.stagingDir, exporter.file).replace("\\", "/")
instance.data["representations"] += data["representations"]
if "render.farm" in families:
# review can be removed since `ProcessSubmittedJobOnFarm` will create
# reviewable representation if needed
if (
instance.data.get("farm")
and "review" in instance.data["families"]
):
instance.data["families"].remove("review")
self.log.debug(

View file

@ -105,10 +105,7 @@ class ExtractReviewDataMov(publish.Extractor):
self, instance, o_name, o_data["extension"],
multiple_presets)
if (
"render.farm" in families or
"prerender.farm" in families
):
if instance.data.get("farm"):
if "review" in instance.data["families"]:
instance.data["families"].remove("review")

View file

@ -31,7 +31,7 @@ class ExtractThumbnail(publish.Extractor):
def process(self, instance):
if "render.farm" in instance.data["families"]:
if instance.data.get("farm"):
return
with napi.maintained_selection():

View file

@ -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:

View file

@ -69,7 +69,7 @@ class OpenPypeMenu(QtWidgets.QWidget):
# "Set colorspace from presets", self
# )
# reset_resolution_btn = QtWidgets.QPushButton(
# "Reset Resolution from peresets", self
# "Set Resolution from presets", self
# )
layout = QtWidgets.QVBoxLayout(self)
@ -108,7 +108,7 @@ class OpenPypeMenu(QtWidgets.QWidget):
libload_btn.clicked.connect(self.on_libload_clicked)
# rename_btn.clicked.connect(self.on_rename_clicked)
# set_colorspace_btn.clicked.connect(self.on_set_colorspace_clicked)
# reset_resolution_btn.clicked.connect(self.on_reset_resolution_clicked)
# reset_resolution_btn.clicked.connect(self.on_set_resolution_clicked)
experimental_btn.clicked.connect(self.on_experimental_clicked)
def on_workfile_clicked(self):
@ -145,8 +145,8 @@ class OpenPypeMenu(QtWidgets.QWidget):
def on_set_colorspace_clicked(self):
print("Clicked Set Colorspace")
def on_reset_resolution_clicked(self):
print("Clicked Reset Resolution")
def on_set_resolution_clicked(self):
print("Clicked Set Resolution")
def on_experimental_clicked(self):
host_tools.show_experimental_tools_dialog()

View file

@ -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:

View file

@ -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:

View file

@ -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"]

View file

@ -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(

View file

@ -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")

View file

@ -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:

View file

@ -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"],

View file

@ -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"),

Some files were not shown because too many files have changed in this diff Show more