Merge branch 'ynput:develop' into feature/setup-fusion-profile-on-prelaunch

This commit is contained in:
Alexey Bogomolov 2023-03-16 20:18:36 +03:00 committed by GitHub
commit aade2eb315
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
86 changed files with 7633 additions and 5675 deletions

View file

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

@ -39,3 +39,5 @@ class CollectFusionCompFrameRanges(pyblish.api.ContextPlugin):
context.data["frameEnd"] = int(end)
context.data["frameStartHandle"] = int(global_start)
context.data["frameEndHandle"] = int(global_end)
context.data["handleStart"] = int(start) - int(global_start)
context.data["handleEnd"] = int(global_end) - int(end)

View file

@ -39,6 +39,8 @@ class CollectInstanceData(pyblish.api.InstancePlugin):
"frameEnd": context.data["frameEnd"],
"frameStartHandle": context.data["frameStartHandle"],
"frameEndHandle": context.data["frameStartHandle"],
"handleStart": context.data["handleStart"],
"handleEnd": context.data["handleEnd"],
"fps": context.data["fps"],
})

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

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

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

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

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

@ -26,6 +26,7 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
"rig",
"camerarig",
"staticMesh",
"skeletalMesh",
"mvLook"]
representations = ["ma", "abc", "fbx", "mb"]

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

@ -23,6 +23,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,

View file

@ -16,6 +16,7 @@ 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
@ -367,16 +368,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 +492,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 +535,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 cmds.loadPlugin("mtoa", quiet=True):
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 cmds.loadPlugin("mtoa", quiet=True):
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

@ -118,7 +118,6 @@ class ExtractPlayblast(publish.Extractor):
# Need to explicitly enable some viewport changes so the viewport is
# refreshed ahead of playblasting.
panel = cmds.getPanel(withFocus=True)
keys = [
"useDefaultMaterial",
"wireframeOnShaded",
@ -129,10 +128,12 @@ 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']
@ -147,12 +148,10 @@ class ExtractPlayblast(publish.Extractor):
# 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()
if not override_viewport_options:
panel_preset = capture.parse_view(instance.data["panel"])
panel_preset.pop("camera")
preset.update(panel_preset)
cmds.setFocus(panel)
self.log.info(
"Using preset:\n{}".format(
@ -163,7 +162,10 @@ class ExtractPlayblast(publish.Extractor):
path = capture.capture(log=self.log, **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)

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

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

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Colorbleed
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,9 @@
from .app import (
MayaLookAssignerWindow,
show
)
__all__ = [
"MayaLookAssignerWindow",
"show"]

View file

@ -0,0 +1,287 @@
import sys
import time
import logging
from qtpy import QtWidgets, QtCore
from openpype.client import get_last_version_by_subset_id
from openpype import style
from openpype.pipeline import legacy_io
from openpype.tools.utils.lib import qt_app_context
from openpype.hosts.maya.api.lib import assign_look_by_version
from maya import cmds
# old api for MFileIO
import maya.OpenMaya
import maya.api.OpenMaya as om
from .widgets import (
AssetOutliner,
LookOutliner
)
from .commands import (
get_workfile,
remove_unused_looks
)
from .vray_proxies import vrayproxy_assign_look
module = sys.modules[__name__]
module.window = None
class MayaLookAssignerWindow(QtWidgets.QWidget):
def __init__(self, parent=None):
super(MayaLookAssignerWindow, self).__init__(parent=parent)
self.log = logging.getLogger(__name__)
# Store callback references
self._callbacks = []
self._connections_set_up = False
filename = get_workfile()
self.setObjectName("lookManager")
self.setWindowTitle("Look Manager 1.3.0 - [{}]".format(filename))
self.setWindowFlags(QtCore.Qt.Window)
self.setParent(parent)
self.resize(750, 500)
self.setup_ui()
# Force refresh check on initialization
self._on_renderlayer_switch()
def setup_ui(self):
"""Build the UI"""
main_splitter = QtWidgets.QSplitter(self)
# Assets (left)
asset_outliner = AssetOutliner(main_splitter)
# Looks (right)
looks_widget = QtWidgets.QWidget(main_splitter)
look_outliner = LookOutliner(looks_widget) # Database look overview
assign_selected = QtWidgets.QCheckBox(
"Assign to selected only", looks_widget
)
assign_selected.setToolTip("Whether to assign only to selected nodes "
"or to the full asset")
remove_unused_btn = QtWidgets.QPushButton(
"Remove Unused Looks", looks_widget
)
looks_layout = QtWidgets.QVBoxLayout(looks_widget)
looks_layout.addWidget(look_outliner)
looks_layout.addWidget(assign_selected)
looks_layout.addWidget(remove_unused_btn)
main_splitter.addWidget(asset_outliner)
main_splitter.addWidget(looks_widget)
main_splitter.setSizes([350, 200])
# Footer
status = QtWidgets.QStatusBar(self)
status.setSizeGripEnabled(False)
status.setFixedHeight(25)
warn_layer = QtWidgets.QLabel(
"Current Layer is not defaultRenderLayer", self
)
warn_layer.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
warn_layer.setStyleSheet("color: #DD5555; font-weight: bold;")
warn_layer.setFixedHeight(25)
footer = QtWidgets.QHBoxLayout()
footer.setContentsMargins(0, 0, 0, 0)
footer.addWidget(status)
footer.addWidget(warn_layer)
# Build up widgets
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.setSpacing(0)
main_layout.addWidget(main_splitter)
main_layout.addLayout(footer)
# Set column width
asset_outliner.view.setColumnWidth(0, 200)
look_outliner.view.setColumnWidth(0, 150)
asset_outliner.selection_changed.connect(
self.on_asset_selection_changed)
asset_outliner.refreshed.connect(
lambda: self.echo("Loaded assets..")
)
look_outliner.menu_apply_action.connect(self.on_process_selected)
remove_unused_btn.clicked.connect(remove_unused_looks)
# Open widgets
self.asset_outliner = asset_outliner
self.look_outliner = look_outliner
self.status = status
self.warn_layer = warn_layer
# Buttons
self.remove_unused = remove_unused_btn
self.assign_selected = assign_selected
self._first_show = True
def setup_connections(self):
"""Connect interactive widgets with actions"""
if self._connections_set_up:
return
# Maya renderlayer switch callback
callback = om.MEventMessage.addEventCallback(
"renderLayerManagerChange",
self._on_renderlayer_switch
)
self._callbacks.append(callback)
self._connections_set_up = True
def remove_connection(self):
# Delete callbacks
for callback in self._callbacks:
om.MMessage.removeCallback(callback)
self._callbacks = []
self._connections_set_up = False
def showEvent(self, event):
self.setup_connections()
super(MayaLookAssignerWindow, self).showEvent(event)
if self._first_show:
self._first_show = False
self.setStyleSheet(style.load_stylesheet())
def closeEvent(self, event):
self.remove_connection()
super(MayaLookAssignerWindow, self).closeEvent(event)
def _on_renderlayer_switch(self, *args):
"""Callback that updates on Maya renderlayer switch"""
if maya.OpenMaya.MFileIO.isNewingFile():
# Don't perform a check during file open or file new as
# the renderlayers will not be in a valid state yet.
return
layer = cmds.editRenderLayerGlobals(query=True,
currentRenderLayer=True)
if layer != "defaultRenderLayer":
self.warn_layer.show()
else:
self.warn_layer.hide()
def echo(self, message):
self.status.showMessage(message, 1500)
def refresh(self):
"""Refresh the content"""
# Get all containers and information
self.asset_outliner.clear()
found_items = self.asset_outliner.get_all_assets()
if not found_items:
self.look_outliner.clear()
def on_asset_selection_changed(self):
"""Get selected items from asset loader and fill look outliner"""
items = self.asset_outliner.get_selected_items()
self.look_outliner.clear()
self.look_outliner.add_items(items)
def on_process_selected(self):
"""Process all selected looks for the selected assets"""
assets = self.asset_outliner.get_selected_items()
assert assets, "No asset selected"
# Collect the looks we want to apply (by name)
look_items = self.look_outliner.get_selected_items()
looks = {look["subset"] for look in look_items}
selection = self.assign_selected.isChecked()
asset_nodes = self.asset_outliner.get_nodes(selection=selection)
project_name = legacy_io.active_project()
start = time.time()
for i, (asset, item) in enumerate(asset_nodes.items()):
# Label prefix
prefix = "({}/{})".format(i + 1, len(asset_nodes))
# Assign the first matching look relevant for this asset
# (since assigning multiple to the same nodes makes no sense)
assign_look = next((subset for subset in item["looks"]
if subset["name"] in looks), None)
if not assign_look:
self.echo(
"{} No matching selected look for {}".format(prefix, asset)
)
continue
# Get the latest version of this asset's look subset
version = get_last_version_by_subset_id(
project_name, assign_look["_id"], fields=["_id"]
)
subset_name = assign_look["name"]
self.echo("{} Assigning {} to {}\t".format(
prefix, subset_name, asset
))
nodes = item["nodes"]
if cmds.pluginInfo('vrayformaya', query=True, loaded=True):
self.echo("Getting vray proxy nodes ...")
vray_proxies = set(cmds.ls(type="VRayProxy", long=True))
if vray_proxies:
for vp in vray_proxies:
if vp in nodes:
vrayproxy_assign_look(vp, subset_name)
nodes = list(set(item["nodes"]).difference(vray_proxies))
# Assign look
if nodes:
assign_look_by_version(nodes, version_id=version["_id"])
end = time.time()
self.echo("Finished assigning.. ({0:.3f}s)".format(end - start))
def show():
"""Display Loader GUI
Arguments:
debug (bool, optional): Run loader in debug-mode,
defaults to False
"""
try:
module.window.close()
del module.window
except (RuntimeError, AttributeError):
pass
# Get Maya main window
top_level_widgets = QtWidgets.QApplication.topLevelWidgets()
mainwindow = next(widget for widget in top_level_widgets
if widget.objectName() == "MayaWindow")
with qt_app_context():
window = MayaLookAssignerWindow(parent=mainwindow)
window.show()
module.window = window

View file

@ -0,0 +1,217 @@
from collections import defaultdict
import logging
import os
import maya.cmds as cmds
from openpype.client import get_asset_by_id
from openpype.pipeline import (
legacy_io,
remove_container,
registered_host,
)
from openpype.hosts.maya.api import lib
from .vray_proxies import get_alembic_ids_cache
log = logging.getLogger(__name__)
def get_workfile():
path = cmds.file(query=True, sceneName=True) or "untitled"
return os.path.basename(path)
def get_workfolder():
return os.path.dirname(cmds.file(query=True, sceneName=True))
def select(nodes):
cmds.select(nodes)
def get_namespace_from_node(node):
"""Get the namespace from the given node
Args:
node (str): name of the node
Returns:
namespace (str)
"""
parts = node.rsplit("|", 1)[-1].rsplit(":", 1)
return parts[0] if len(parts) > 1 else u":"
def list_descendents(nodes):
"""Include full descendant hierarchy of given nodes.
This is a workaround to cmds.listRelatives(allDescendents=True) because
this way correctly keeps children instance paths (see Maya documentation)
This fixes LKD-26: assignments not working as expected on instanced shapes.
Return:
list: List of children descendents of nodes
"""
result = []
while True:
nodes = cmds.listRelatives(nodes,
fullPath=True)
if nodes:
result.extend(nodes)
else:
return result
def get_selected_nodes():
"""Get information from current selection"""
selection = cmds.ls(selection=True, long=True)
hierarchy = list_descendents(selection)
return list(set(selection + hierarchy))
def get_all_asset_nodes():
"""Get all assets from the scene, container based
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
def create_asset_id_hash(nodes):
"""Create a hash based on cbId attribute value
Args:
nodes (list): a list of nodes
Returns:
dict
"""
node_id_hash = defaultdict(list)
for node in nodes:
# iterate over content of reference node
if cmds.nodeType(node) == "reference":
ref_hashes = create_asset_id_hash(
list(set(cmds.referenceQuery(node, nodes=True, dp=True))))
for asset_id, ref_nodes in ref_hashes.items():
node_id_hash[asset_id] += ref_nodes
elif cmds.pluginInfo('vrayformaya', query=True,
loaded=True) and cmds.nodeType(
node) == "VRayProxy":
path = cmds.getAttr("{}.fileName".format(node))
ids = get_alembic_ids_cache(path)
for k, _ in ids.items():
pid = k.split(":")[0]
if node not in node_id_hash[pid]:
node_id_hash[pid].append(node)
else:
value = lib.get_id(node)
if value is None:
continue
asset_id = value.split(":")[0]
node_id_hash[asset_id].append(node)
return dict(node_id_hash)
def create_items_from_nodes(nodes):
"""Create an item for the view based the container and content of it
It fetches the look document based on the asset ID found in the content.
The item will contain all important information for the tool to work.
If there is an asset ID which is not registered in the project's collection
it will log a warning message.
Args:
nodes (list): list of maya nodes
Returns:
list of dicts
"""
asset_view_items = []
id_hashes = create_asset_id_hash(nodes)
if not id_hashes:
log.warning("No id hashes")
return asset_view_items
project_name = legacy_io.active_project()
for _id, id_nodes in id_hashes.items():
asset = get_asset_by_id(project_name, _id, fields=["name"])
# Skip if asset id is not found
if not asset:
log.warning("Id not found in the database, skipping '%s'." % _id)
log.warning("Nodes: %s" % id_nodes)
continue
# Collect available look subsets for this asset
looks = lib.list_looks(asset["_id"])
# Collect namespaces the asset is found in
namespaces = set()
for node in id_nodes:
namespace = get_namespace_from_node(node)
namespaces.add(namespace)
asset_view_items.append({
"label": asset["name"],
"asset": asset,
"looks": looks,
"namespaces": namespaces
})
return asset_view_items
def remove_unused_looks():
"""Removes all loaded looks for which none of the shaders are used.
This will cleanup all loaded "LookLoader" containers that are unused in
the current scene.
"""
host = registered_host()
unused = []
for container in host.ls():
if container['loader'] == "LookLoader":
members = lib.get_container_members(container['objectName'])
look_sets = cmds.ls(members, type="objectSet")
for look_set in look_sets:
# If the set is used than we consider this look *in use*
if cmds.sets(look_set, query=True):
break
else:
unused.append(container)
for container in unused:
log.info("Removing unused look container: %s", container['objectName'])
remove_container(container)
log.info("Finished removing unused looks. (see log for details)")

View file

@ -0,0 +1,129 @@
from collections import defaultdict
from qtpy import QtCore
import qtawesome
from openpype.tools.utils import models
from openpype.style import get_default_entity_icon_color
class AssetModel(models.TreeModel):
Columns = ["label"]
def __init__(self, *args, **kwargs):
super(AssetModel, self).__init__(*args, **kwargs)
self._icon_color = get_default_entity_icon_color()
def add_items(self, items):
"""
Add items to model with needed data
Args:
items(list): collection of item data
Returns:
None
"""
self.beginResetModel()
# Add the items sorted by label
sorter = lambda x: x["label"]
for item in sorted(items, key=sorter):
asset_item = models.Item()
asset_item.update(item)
asset_item["icon"] = "folder"
# Add namespace children
namespaces = item["namespaces"]
for namespace in sorted(namespaces):
child = models.Item()
child.update(item)
child.update({
"label": (namespace if namespace != ":"
else "(no namespace)"),
"namespace": namespace,
"looks": item["looks"],
"icon": "folder-o"
})
asset_item.add_child(child)
self.add_child(asset_item)
self.endResetModel()
def data(self, index, role):
if not index.isValid():
return
if role == models.TreeModel.ItemRole:
node = index.internalPointer()
return node
# Add icon
if role == QtCore.Qt.DecorationRole:
if index.column() == 0:
node = index.internalPointer()
icon = node.get("icon")
if icon:
return qtawesome.icon(
"fa.{0}".format(icon),
color=self._icon_color
)
return super(AssetModel, self).data(index, role)
class LookModel(models.TreeModel):
"""Model displaying a list of looks and matches for assets"""
Columns = ["label", "match"]
def add_items(self, items):
"""Add items to model with needed data
An item exists of:
{
"subset": 'name of subset',
"asset": asset_document
}
Args:
items(list): collection of item data
Returns:
None
"""
self.beginResetModel()
# Collect the assets per look name (from the items of the AssetModel)
look_subsets = defaultdict(list)
for asset_item in items:
asset = asset_item["asset"]
for look in asset_item["looks"]:
look_subsets[look["name"]].append(asset)
for subset in sorted(look_subsets.keys()):
assets = look_subsets[subset]
# Define nice label without "look" prefix for readability
label = subset if not subset.startswith("look") else subset[4:]
item_node = models.Item()
item_node["label"] = label
item_node["subset"] = subset
# Amount of matching assets for this look
item_node["match"] = len(assets)
# Store the assets that have this subset available
item_node["assets"] = assets
self.add_child(item_node)
self.endResetModel()

View file

@ -0,0 +1,47 @@
from qtpy import QtWidgets, QtCore
class View(QtWidgets.QTreeView):
data_changed = QtCore.Signal()
def __init__(self, parent=None):
super(View, self).__init__(parent=parent)
# view settings
self.setAlternatingRowColors(False)
self.setSortingEnabled(True)
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
def get_indices(self):
"""Get the selected rows"""
selection_model = self.selectionModel()
return selection_model.selectedRows()
def extend_to_children(self, indices):
"""Extend the indices to the children indices.
Top-level indices are extended to its children indices. Sub-items
are kept as is.
:param indices: The indices to extend.
:type indices: list
:return: The children indices
:rtype: list
"""
subitems = set()
for i in indices:
valid_parent = i.parent().isValid()
if valid_parent and i not in subitems:
subitems.add(i)
else:
# is top level node
model = i.model()
rows = model.rowCount(parent=i)
for row in range(rows):
child = model.index(row, 0, parent=i)
subitems.add(child)
return list(subitems)

View file

@ -0,0 +1,305 @@
# -*- coding: utf-8 -*-
"""Tools for loading looks to vray proxies."""
import os
from collections import defaultdict
import logging
import json
import six
import alembic.Abc
from maya import cmds
from openpype.client import (
get_representation_by_name,
get_last_version_by_subset_name,
)
from openpype.pipeline import (
legacy_io,
load_container,
loaders_from_representation,
discover_loader_plugins,
get_representation_path,
registered_host,
)
from openpype.hosts.maya.api import lib
log = logging.getLogger(__name__)
def get_alembic_paths_by_property(filename, attr, verbose=False):
# type: (str, str, bool) -> dict
"""Return attribute value per objects in the Alembic file.
Reads an Alembic archive hierarchy and retrieves the
value from the `attr` properties on the objects.
Args:
filename (str): Full path to Alembic archive to read.
attr (str): Id attribute.
verbose (bool): Whether to verbosely log missing attributes.
Returns:
dict: Mapping of node full path with its id
"""
# Normalize alembic path
filename = os.path.normpath(filename)
filename = filename.replace("\\", "/")
filename = str(filename) # path must be string
try:
archive = alembic.Abc.IArchive(filename)
except RuntimeError:
# invalid alembic file - probably vrmesh
log.warning("{} is not an alembic file".format(filename))
return {}
root = archive.getTop()
iterator = list(root.children)
obj_ids = {}
for obj in iterator:
name = obj.getFullName()
# include children for coming iterations
iterator.extend(obj.children)
props = obj.getProperties()
if props.getNumProperties() == 0:
# Skip those without properties, e.g. '/materials' in a gpuCache
continue
# THe custom attribute is under the properties' first container under
# the ".arbGeomParams"
prop = props.getProperty(0) # get base property
_property = None
try:
geo_params = prop.getProperty('.arbGeomParams')
_property = geo_params.getProperty(attr)
except KeyError:
if verbose:
log.debug("Missing attr on: {0}".format(name))
continue
if not _property.isConstant():
log.warning("Id not constant on: {0}".format(name))
# Get first value sample
value = _property.getValue()[0]
obj_ids[name] = value
return obj_ids
def get_alembic_ids_cache(path):
# type: (str) -> dict
"""Build a id to node mapping in Alembic file.
Nodes without IDs are ignored.
Returns:
dict: Mapping of id to nodes in the Alembic.
"""
node_ids = get_alembic_paths_by_property(path, attr="cbId")
id_nodes = defaultdict(list)
for node, _id in six.iteritems(node_ids):
id_nodes[_id].append(node)
return dict(six.iteritems(id_nodes))
def assign_vrayproxy_shaders(vrayproxy, assignments):
# type: (str, dict) -> None
"""Assign shaders to content of Vray Proxy.
This will create shader overrides on Vray Proxy to assign shaders to its
content.
Todo:
Allow to optimize and assign a single shader to multiple shapes at
once or maybe even set it to the highest available path?
Args:
vrayproxy (str): Name of Vray Proxy
assignments (dict): Mapping of shader assignments.
Returns:
None
"""
# Clear all current shader assignments
plug = vrayproxy + ".shaders"
num = cmds.getAttr(plug, size=True)
for i in reversed(range(num)):
cmds.removeMultiInstance("{}[{}]".format(plug, i), b=True)
# Create new assignment overrides
index = 0
for material, paths in assignments.items():
for path in paths:
plug = "{}.shaders[{}]".format(vrayproxy, index)
cmds.setAttr(plug + ".shadersNames", path, type="string")
cmds.connectAttr(material + ".outColor",
plug + ".shadersConnections", force=True)
index += 1
def get_look_relationships(version_id):
# type: (str) -> dict
"""Get relations for the look.
Args:
version_id (str): Parent version Id.
Returns:
dict: Dictionary of relations.
"""
project_name = legacy_io.active_project()
json_representation = get_representation_by_name(
project_name, representation_name="json", version_id=version_id
)
# Load relationships
shader_relation = get_representation_path(json_representation)
with open(shader_relation, "r") as f:
relationships = json.load(f)
return relationships
def load_look(version_id):
# type: (str) -> list
"""Load look from version.
Get look from version and invoke Loader for it.
Args:
version_id (str): Version ID
Returns:
list of shader nodes.
"""
project_name = legacy_io.active_project()
# Get representations of shader file and relationships
look_representation = get_representation_by_name(
project_name, representation_name="ma", version_id=version_id
)
# See if representation is already loaded, if so reuse it.
host = registered_host()
representation_id = str(look_representation['_id'])
for container in host.ls():
if (container['loader'] == "LookLoader" and
container['representation'] == representation_id):
log.info("Reusing loaded look ...")
container_node = container['objectName']
break
else:
log.info("Using look for the first time ...")
# Load file
all_loaders = discover_loader_plugins()
loaders = loaders_from_representation(all_loaders, representation_id)
loader = next(
(i for i in loaders if i.__name__ == "LookLoader"), None)
if loader is None:
raise RuntimeError("Could not find LookLoader, this is a bug")
# Reference the look file
with lib.maintained_selection():
container_node = load_container(loader, look_representation)
# Get container members
shader_nodes = lib.get_container_members(container_node)
return shader_nodes
def vrayproxy_assign_look(vrayproxy, subset="lookDefault"):
# type: (str, str) -> None
"""Assign look to vray proxy.
Args:
vrayproxy (str): Name of vrayproxy to apply look to.
subset (str): Name of look subset.
Returns:
None
"""
path = cmds.getAttr(vrayproxy + ".fileName")
nodes_by_id = get_alembic_ids_cache(path)
if not nodes_by_id:
log.warning("Alembic file has no cbId attributes: %s" % path)
return
# Group by asset id so we run over the look per asset
node_ids_by_asset_id = defaultdict(set)
for node_id in nodes_by_id:
asset_id = node_id.split(":", 1)[0]
node_ids_by_asset_id[asset_id].add(node_id)
project_name = legacy_io.active_project()
for asset_id, node_ids in node_ids_by_asset_id.items():
# Get latest look version
version = get_last_version_by_subset_name(
project_name,
subset_name=subset,
asset_id=asset_id,
fields=["_id"]
)
if not version:
print("Didn't find last version for subset name {}".format(
subset
))
continue
relationships = get_look_relationships(version["_id"])
shadernodes = load_look(version["_id"])
# Get only the node ids and paths related to this asset
# And get the shader edits the look supplies
asset_nodes_by_id = {
node_id: nodes_by_id[node_id] for node_id in node_ids
}
edits = list(
lib.iter_shader_edits(
relationships, shadernodes, asset_nodes_by_id))
# Create assignments
assignments = {}
for edit in edits:
if edit["action"] == "assign":
nodes = edit["nodes"]
shader = edit["shader"]
if not cmds.ls(shader, type="shadingEngine"):
print("Skipping non-shader: %s" % shader)
continue
inputs = cmds.listConnections(
shader + ".surfaceShader", source=True)
if not inputs:
print("Shading engine missing material: %s" % shader)
# Strip off component assignments
for i, node in enumerate(nodes):
if "." in node:
log.warning(
("Converting face assignment to full object "
"assignment. This conversion can be lossy: "
"{}").format(node))
nodes[i] = node.split(".")[0]
material = inputs[0]
assignments[material] = nodes
assign_vrayproxy_shaders(vrayproxy, assignments)

View file

@ -0,0 +1,257 @@
import logging
from collections import defaultdict
from qtpy import QtWidgets, QtCore
from openpype.tools.utils.models import TreeModel
from openpype.tools.utils.lib import (
preserve_expanded_rows,
preserve_selection,
)
from .models import (
AssetModel,
LookModel
)
from . import commands
from .views import View
from maya import cmds
class AssetOutliner(QtWidgets.QWidget):
refreshed = QtCore.Signal()
selection_changed = QtCore.Signal()
def __init__(self, parent=None):
super(AssetOutliner, self).__init__(parent)
title = QtWidgets.QLabel("Assets", self)
title.setAlignment(QtCore.Qt.AlignCenter)
title.setStyleSheet("font-weight: bold; font-size: 12px")
model = AssetModel()
view = View(self)
view.setModel(model)
view.customContextMenuRequested.connect(self.right_mouse_menu)
view.setSortingEnabled(False)
view.setHeaderHidden(True)
view.setIndentation(10)
from_all_asset_btn = QtWidgets.QPushButton(
"Get All Assets", self
)
from_selection_btn = QtWidgets.QPushButton(
"Get Assets From Selection", self
)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(title)
layout.addWidget(from_all_asset_btn)
layout.addWidget(from_selection_btn)
layout.addWidget(view)
# Build connections
from_selection_btn.clicked.connect(self.get_selected_assets)
from_all_asset_btn.clicked.connect(self.get_all_assets)
selection_model = view.selectionModel()
selection_model.selectionChanged.connect(self.selection_changed)
self.view = view
self.model = model
self.log = logging.getLogger(__name__)
def clear(self):
self.model.clear()
# fix looks remaining visible when no items present after "refresh"
# todo: figure out why this workaround is needed.
self.selection_changed.emit()
def add_items(self, items):
"""Add new items to the outliner"""
self.model.add_items(items)
self.refreshed.emit()
def get_selected_items(self):
"""Get current selected items from view
Returns:
list: list of dictionaries
"""
selection_model = self.view.selectionModel()
return [row.data(TreeModel.ItemRole)
for row in selection_model.selectedRows(0)]
def get_all_assets(self):
"""Add all items from the current scene"""
items = []
with preserve_expanded_rows(self.view):
with preserve_selection(self.view):
self.clear()
nodes = commands.get_all_asset_nodes()
items = commands.create_items_from_nodes(nodes)
self.add_items(items)
return len(items) > 0
def get_selected_assets(self):
"""Add all selected items from the current scene"""
with preserve_expanded_rows(self.view):
with preserve_selection(self.view):
self.clear()
nodes = commands.get_selected_nodes()
items = commands.create_items_from_nodes(nodes)
self.add_items(items)
def get_nodes(self, selection=False):
"""Find the nodes in the current scene per asset."""
items = self.get_selected_items()
# Collect all nodes by hash (optimization)
if not selection:
nodes = cmds.ls(dag=True, long=True)
else:
nodes = commands.get_selected_nodes()
id_nodes = commands.create_asset_id_hash(nodes)
# Collect the asset item entries per asset
# and collect the namespaces we'd like to apply
assets = {}
asset_namespaces = defaultdict(set)
for item in items:
asset_id = str(item["asset"]["_id"])
asset_name = item["asset"]["name"]
asset_namespaces[asset_name].add(item.get("namespace"))
if asset_name in assets:
continue
assets[asset_name] = item
assets[asset_name]["nodes"] = id_nodes.get(asset_id, [])
# Filter nodes to namespace (if only namespaces were selected)
for asset_name in assets:
namespaces = asset_namespaces[asset_name]
# When None is present there should be no filtering
if None in namespaces:
continue
# Else only namespaces are selected and *not* the top entry so
# we should filter to only those namespaces.
nodes = assets[asset_name]["nodes"]
nodes = [node for node in nodes if
commands.get_namespace_from_node(node) in namespaces]
assets[asset_name]["nodes"] = nodes
return assets
def select_asset_from_items(self):
"""Select nodes from listed asset"""
items = self.get_nodes(selection=False)
nodes = []
for item in items.values():
nodes.extend(item["nodes"])
commands.select(nodes)
def right_mouse_menu(self, pos):
"""Build RMB menu for asset outliner"""
active = self.view.currentIndex() # index under mouse
active = active.sibling(active.row(), 0) # get first column
globalpos = self.view.viewport().mapToGlobal(pos)
menu = QtWidgets.QMenu(self.view)
# Direct assignment
apply_action = QtWidgets.QAction(menu, text="Select nodes")
apply_action.triggered.connect(self.select_asset_from_items)
if not active.isValid():
apply_action.setEnabled(False)
menu.addAction(apply_action)
menu.exec_(globalpos)
class LookOutliner(QtWidgets.QWidget):
menu_apply_action = QtCore.Signal()
def __init__(self, parent=None):
super(LookOutliner, self).__init__(parent)
# Looks from database
title = QtWidgets.QLabel("Looks", self)
title.setAlignment(QtCore.Qt.AlignCenter)
title.setStyleSheet("font-weight: bold; font-size: 12px")
title.setAlignment(QtCore.Qt.AlignCenter)
model = LookModel()
# Proxy for dynamic sorting
proxy = QtCore.QSortFilterProxyModel()
proxy.setSourceModel(model)
view = View(self)
view.setModel(proxy)
view.setMinimumHeight(180)
view.setToolTip("Use right mouse button menu for direct actions")
view.customContextMenuRequested.connect(self.right_mouse_menu)
view.sortByColumn(0, QtCore.Qt.AscendingOrder)
# look manager layout
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(10)
layout.addWidget(title)
layout.addWidget(view)
self.view = view
self.model = model
def clear(self):
self.model.clear()
def add_items(self, items):
self.model.add_items(items)
def get_selected_items(self):
"""Get current selected items from view
Returns:
list: list of dictionaries
"""
items = [i.data(TreeModel.ItemRole) for i in self.view.get_indices()]
return [item for item in items if item is not None]
def right_mouse_menu(self, pos):
"""Build RMB menu for look view"""
active = self.view.currentIndex() # index under mouse
active = active.sibling(active.row(), 0) # get first column
globalpos = self.view.viewport().mapToGlobal(pos)
if not active.isValid():
return
menu = QtWidgets.QMenu(self.view)
# Direct assignment
apply_action = QtWidgets.QAction(menu, text="Assign looks..")
apply_action.triggered.connect(self.menu_apply_action)
menu.addAction(apply_action)
menu.exec_(globalpos)

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

@ -222,18 +222,21 @@ class LoadClip(plugin.NukeLoader):
"""
representation = deepcopy(representation)
context = representation["context"]
template = representation["data"]["template"]
# Get the frame from the context and hash it
frame = context["frame"]
hashed_frame = "#" * len(str(frame))
# Replace the frame with the hash in the originalBasename
if (
"{originalBasename}" in template
and "frame" in context
"{originalBasename}" in representation["data"]["template"]
):
frame = context["frame"]
hashed_frame = "#" * len(str(frame))
origin_basename = context["originalBasename"]
context["originalBasename"] = origin_basename.replace(
frame, hashed_frame
)
# Replace the frame with the hash in the frame
representation["context"]["frame"] = hashed_frame
return representation

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

@ -3,7 +3,14 @@
import os
import copy
from pathlib import Path
from openpype.widgets.splash_screen import SplashScreen
from qtpy import QtCore
from openpype.hosts.unreal.ue_workers import (
UEProjectGenerationWorker,
UEPluginInstallWorker
)
from openpype import resources
from openpype.lib import (
PreLaunchHook,
ApplicationLaunchFailed,
@ -22,6 +29,7 @@ class UnrealPrelaunchHook(PreLaunchHook):
shell script.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -58,6 +66,78 @@ class UnrealPrelaunchHook(PreLaunchHook):
# Return filename
return filled_anatomy[workfile_template_key]["file"]
def exec_plugin_install(self, engine_path: Path, env: dict = None):
# set up the QThread and worker with necessary signals
env = env or os.environ
q_thread = QtCore.QThread()
ue_plugin_worker = UEPluginInstallWorker()
q_thread.started.connect(ue_plugin_worker.run)
ue_plugin_worker.setup(engine_path, env)
ue_plugin_worker.moveToThread(q_thread)
splash_screen = SplashScreen(
"Installing plugin",
resources.get_resource("app_icons", "ue4.png")
)
# set up the splash screen with necessary triggers
ue_plugin_worker.installing.connect(
splash_screen.update_top_label_text
)
ue_plugin_worker.progress.connect(splash_screen.update_progress)
ue_plugin_worker.log.connect(splash_screen.append_log)
ue_plugin_worker.finished.connect(splash_screen.quit_and_close)
ue_plugin_worker.failed.connect(splash_screen.fail)
splash_screen.start_thread(q_thread)
splash_screen.show_ui()
if not splash_screen.was_proc_successful():
raise ApplicationLaunchFailed("Couldn't run the application! "
"Plugin failed to install!")
def exec_ue_project_gen(self,
engine_version: str,
unreal_project_name: str,
engine_path: Path,
project_dir: Path):
self.log.info((
f"{self.signature} Creating unreal "
f"project [ {unreal_project_name} ]"
))
q_thread = QtCore.QThread()
ue_project_worker = UEProjectGenerationWorker()
ue_project_worker.setup(
engine_version,
unreal_project_name,
engine_path,
project_dir
)
ue_project_worker.moveToThread(q_thread)
q_thread.started.connect(ue_project_worker.run)
splash_screen = SplashScreen(
"Initializing UE project",
resources.get_resource("app_icons", "ue4.png")
)
ue_project_worker.stage_begin.connect(
splash_screen.update_top_label_text
)
ue_project_worker.progress.connect(splash_screen.update_progress)
ue_project_worker.log.connect(splash_screen.append_log)
ue_project_worker.finished.connect(splash_screen.quit_and_close)
ue_project_worker.failed.connect(splash_screen.fail)
splash_screen.start_thread(q_thread)
splash_screen.show_ui()
if not splash_screen.was_proc_successful():
raise ApplicationLaunchFailed("Couldn't run the application! "
"Failed to generate the project!")
def execute(self):
"""Hook entry method."""
workdir = self.launch_context.env["AVALON_WORKDIR"]
@ -137,23 +217,18 @@ class UnrealPrelaunchHook(PreLaunchHook):
if self.launch_context.env.get(env_key):
os.environ[env_key] = self.launch_context.env[env_key]
engine_path = detected[engine_version]
engine_path: Path = Path(detected[engine_version])
unreal_lib.try_installing_plugin(Path(engine_path), os.environ)
if not unreal_lib.check_plugin_existence(engine_path):
self.exec_plugin_install(engine_path)
project_file = project_path / unreal_project_filename
if not project_file.is_file():
self.log.info((
f"{self.signature} creating unreal "
f"project [ {unreal_project_name} ]"
))
unreal_lib.create_unreal_project(
unreal_project_name,
engine_version,
project_path,
engine_path=Path(engine_path)
)
if not project_file.is_file():
self.exec_ue_project_gen(engine_version,
unreal_project_name,
engine_path,
project_path)
self.launch_context.env["OPENPYPE_UNREAL_VERSION"] = engine_version
# Append project file to launch arguments

View file

@ -30,7 +30,7 @@ void UAssetContainer::OnAssetAdded(const FAssetData& AssetData)
// get asset path and class
FString assetPath = AssetData.GetFullName();
FString assetFName = AssetData.AssetClassPath.ToString();
FString assetFName = AssetData.ObjectPath.ToString();
UE_LOG(LogTemp, Log, TEXT("asset name %s"), *assetFName);
// split path
assetPath.ParseIntoArray(split, TEXT(" "), true);
@ -60,7 +60,7 @@ void UAssetContainer::OnAssetRemoved(const FAssetData& AssetData)
// get asset path and class
FString assetPath = AssetData.GetFullName();
FString assetFName = AssetData.AssetClassPath.ToString();
FString assetFName = AssetData.ObjectPath.ToString();
// split path
assetPath.ParseIntoArray(split, TEXT(" "), true);
@ -93,7 +93,7 @@ void UAssetContainer::OnAssetRenamed(const FAssetData& AssetData, const FString&
// get asset path and class
FString assetPath = AssetData.GetFullName();
FString assetFName = AssetData.AssetClassPath.ToString();
FString assetFName = AssetData.ObjectPath.ToString();
// split path
assetPath.ParseIntoArray(split, TEXT(" "), true);

View file

@ -252,7 +252,7 @@ def create_unreal_project(project_name: str,
with open(project_file.as_posix(), mode="r+") as pf:
pf_json = json.load(pf)
pf_json["EngineAssociation"] = _get_build_id(engine_path, ue_version)
pf_json["EngineAssociation"] = get_build_id(engine_path, ue_version)
pf.seek(0)
json.dump(pf_json, pf, indent=4)
pf.truncate()
@ -338,7 +338,7 @@ def get_path_to_ubt(engine_path: Path, ue_version: str) -> Path:
return Path(u_build_tool_path)
def _get_build_id(engine_path: Path, ue_version: str) -> str:
def get_build_id(engine_path: Path, ue_version: str) -> str:
ue_modules = Path()
if platform.system().lower() == "windows":
ue_modules_path = engine_path / "Engine/Binaries/Win64"
@ -365,6 +365,26 @@ def _get_build_id(engine_path: Path, ue_version: str) -> str:
return "{" + loaded_modules.get("BuildId") + "}"
def check_plugin_existence(engine_path: Path, env: dict = None) -> bool:
env = env or os.environ
integration_plugin_path: Path = Path(env.get("OPENPYPE_UNREAL_PLUGIN", ""))
if not os.path.isdir(integration_plugin_path):
raise RuntimeError("Path to the integration plugin is null!")
# Create a path to the plugin in the engine
op_plugin_path: Path = engine_path / "Engine/Plugins/Marketplace/OpenPype"
if not op_plugin_path.is_dir():
return False
if not (op_plugin_path / "Binaries").is_dir() \
or not (op_plugin_path / "Intermediate").is_dir():
return False
return True
def try_installing_plugin(engine_path: Path, env: dict = None) -> None:
env = env or os.environ
@ -377,7 +397,6 @@ def try_installing_plugin(engine_path: Path, env: dict = None) -> None:
op_plugin_path: Path = engine_path / "Engine/Plugins/Marketplace/OpenPype"
if not op_plugin_path.is_dir():
print("--- OpenPype Plugin is not present. Installing ...")
op_plugin_path.mkdir(parents=True, exist_ok=True)
engine_plugin_config_path: Path = op_plugin_path / "Config"
@ -387,7 +406,6 @@ def try_installing_plugin(engine_path: Path, env: dict = None) -> None:
if not (op_plugin_path / "Binaries").is_dir() \
or not (op_plugin_path / "Intermediate").is_dir():
print("--- Binaries are not present. Building the plugin ...")
_build_and_move_plugin(engine_path, op_plugin_path, env)

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
"""Loader for layouts."""
import json
import collections
from pathlib import Path
import unreal
@ -12,9 +13,7 @@ from unreal import FBXImportType
from unreal import MovieSceneLevelVisibilityTrack
from unreal import MovieSceneSubTrack
from bson.objectid import ObjectId
from openpype.client import get_asset_by_name, get_assets
from openpype.client import get_asset_by_name, get_assets, get_representations
from openpype.pipeline import (
discover_loader_plugins,
loaders_from_representation,
@ -410,6 +409,30 @@ class LayoutLoader(plugin.Loader):
return sequence, (min_frame, max_frame)
def _get_repre_docs_by_version_id(self, data):
version_ids = {
element.get("version")
for element in data
if element.get("representation")
}
version_ids.discard(None)
output = collections.defaultdict(list)
if not version_ids:
return output
project_name = legacy_io.active_project()
repre_docs = get_representations(
project_name,
representation_names=["fbx", "abc"],
version_ids=version_ids,
fields=["_id", "parent", "name"]
)
for repre_doc in repre_docs:
version_id = str(repre_doc["parent"])
output[version_id].append(repre_doc)
return output
def _process(self, lib_path, asset_dir, sequence, repr_loaded=None):
ar = unreal.AssetRegistryHelpers.get_asset_registry()
@ -429,31 +452,21 @@ class LayoutLoader(plugin.Loader):
loaded_assets = []
repre_docs_by_version_id = self._get_repre_docs_by_version_id(data)
for element in data:
representation = None
repr_format = None
if element.get('representation'):
# representation = element.get('representation')
self.log.info(element.get("version"))
valid_formats = ['fbx', 'abc']
repr_data = legacy_io.find_one({
"type": "representation",
"parent": ObjectId(element.get("version")),
"name": {"$in": valid_formats}
})
repr_format = repr_data.get('name')
if not repr_data:
repre_docs = repre_docs_by_version_id[element.get("version")]
if not repre_docs:
self.log.error(
f"No valid representation found for version "
f"{element.get('version')}")
continue
repre_doc = repre_docs[0]
representation = str(repre_doc["_id"])
repr_format = repre_doc["name"]
representation = str(repr_data.get('_id'))
print(representation)
# This is to keep compatibility with old versions of the
# json format.
elif element.get('reference_fbx'):

View file

@ -0,0 +1,335 @@
import json
import os
import platform
import re
import subprocess
from distutils import dir_util
from pathlib import Path
from typing import List
import openpype.hosts.unreal.lib as ue_lib
from qtpy import QtCore
def parse_comp_progress(line: str, progress_signal: QtCore.Signal(int)) -> int:
match = re.search('\[[1-9]+/[0-9]+\]', line)
if match is not None:
split: list[str] = match.group().split('/')
curr: float = float(split[0][1:])
total: float = float(split[1][:-1])
progress_signal.emit(int((curr / total) * 100.0))
def parse_prj_progress(line: str, progress_signal: QtCore.Signal(int)) -> int:
match = re.search('@progress', line)
if match is not None:
percent_match = re.search('\d{1,3}', line)
progress_signal.emit(int(percent_match.group()))
class UEProjectGenerationWorker(QtCore.QObject):
finished = QtCore.Signal(str)
failed = QtCore.Signal(str)
progress = QtCore.Signal(int)
log = QtCore.Signal(str)
stage_begin = QtCore.Signal(str)
ue_version: str = None
project_name: str = None
env = None
engine_path: Path = None
project_dir: Path = None
dev_mode = False
def setup(self, ue_version: str,
project_name,
engine_path: Path,
project_dir: Path,
dev_mode: bool = False,
env: dict = None):
self.ue_version = ue_version
self.project_dir = project_dir
self.env = env or os.environ
preset = ue_lib.get_project_settings(
project_name
)["unreal"]["project_setup"]
if dev_mode or preset["dev_mode"]:
self.dev_mode = True
self.project_name = project_name
self.engine_path = engine_path
def run(self):
# engine_path should be the location of UE_X.X folder
ue_editor_exe = ue_lib.get_editor_exe_path(self.engine_path,
self.ue_version)
cmdlet_project = ue_lib.get_path_to_cmdlet_project(self.ue_version)
project_file = self.project_dir / f"{self.project_name}.uproject"
print("--- Generating a new project ...")
# 1st stage
stage_count = 2
if self.dev_mode:
stage_count = 4
self.stage_begin.emit(f'Generating a new UE project ... 1 out of '
f'{stage_count}')
commandlet_cmd = [f'{ue_editor_exe.as_posix()}',
f'{cmdlet_project.as_posix()}',
f'-run=OPGenerateProject',
f'{project_file.resolve().as_posix()}']
if self.dev_mode:
commandlet_cmd.append('-GenerateCode')
gen_process = subprocess.Popen(commandlet_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
for line in gen_process.stdout:
decoded_line = line.decode(errors="replace")
print(decoded_line, end='')
self.log.emit(decoded_line)
gen_process.stdout.close()
return_code = gen_process.wait()
if return_code and return_code != 0:
msg = 'Failed to generate ' + self.project_name \
+ f' project! Exited with return code {return_code}'
self.failed.emit(msg, return_code)
raise RuntimeError(msg)
print("--- Project has been generated successfully.")
self.stage_begin.emit(f'Writing the Engine ID of the build UE ... 1'
f' out of {stage_count}')
if not project_file.is_file():
msg = "Failed to write the Engine ID into .uproject file! Can " \
"not read!"
self.failed.emit(msg)
raise RuntimeError(msg)
with open(project_file.as_posix(), mode="r+") as pf:
pf_json = json.load(pf)
pf_json["EngineAssociation"] = ue_lib.get_build_id(
self.engine_path,
self.ue_version
)
print(pf_json["EngineAssociation"])
pf.seek(0)
json.dump(pf_json, pf, indent=4)
pf.truncate()
print(f'--- Engine ID has been written into the project file')
self.progress.emit(90)
if self.dev_mode:
# 2nd stage
self.stage_begin.emit(f'Generating project files ... 2 out of '
f'{stage_count}')
self.progress.emit(0)
ubt_path = ue_lib.get_path_to_ubt(self.engine_path,
self.ue_version)
arch = "Win64"
if platform.system().lower() == "windows":
arch = "Win64"
elif platform.system().lower() == "linux":
arch = "Linux"
elif platform.system().lower() == "darwin":
# we need to test this out
arch = "Mac"
gen_prj_files_cmd = [ubt_path.as_posix(),
"-projectfiles",
f"-project={project_file}",
"-progress"]
gen_proc = subprocess.Popen(gen_prj_files_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
for line in gen_proc.stdout:
decoded_line: str = line.decode(errors='replace')
print(decoded_line, end='')
self.log.emit(decoded_line)
parse_prj_progress(decoded_line, self.progress)
gen_proc.stdout.close()
return_code = gen_proc.wait()
if return_code and return_code != 0:
msg = 'Failed to generate project files! ' \
f'Exited with return code {return_code}'
self.failed.emit(msg, return_code)
raise RuntimeError(msg)
self.stage_begin.emit(f'Building the project ... 3 out of '
f'{stage_count}')
self.progress.emit(0)
# 3rd stage
build_prj_cmd = [ubt_path.as_posix(),
f"-ModuleWithSuffix={self.project_name},3555",
arch,
"Development",
"-TargetType=Editor",
f'-Project={project_file}',
f'{project_file}',
"-IgnoreJunk"]
build_prj_proc = subprocess.Popen(build_prj_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
for line in build_prj_proc.stdout:
decoded_line: str = line.decode(errors='replace')
print(decoded_line, end='')
self.log.emit(decoded_line)
parse_comp_progress(decoded_line, self.progress)
build_prj_proc.stdout.close()
return_code = build_prj_proc.wait()
if return_code and return_code != 0:
msg = 'Failed to build project! ' \
f'Exited with return code {return_code}'
self.failed.emit(msg, return_code)
raise RuntimeError(msg)
# ensure we have PySide2 installed in engine
self.progress.emit(0)
self.stage_begin.emit(f'Checking PySide2 installation... {stage_count}'
f' out of {stage_count}')
python_path = None
if platform.system().lower() == "windows":
python_path = self.engine_path / ("Engine/Binaries/ThirdParty/"
"Python3/Win64/python.exe")
if platform.system().lower() == "linux":
python_path = self.engine_path / ("Engine/Binaries/ThirdParty/"
"Python3/Linux/bin/python3")
if platform.system().lower() == "darwin":
python_path = self.engine_path / ("Engine/Binaries/ThirdParty/"
"Python3/Mac/bin/python3")
if not python_path:
msg = "Unsupported platform"
self.failed.emit(msg, 1)
raise NotImplementedError(msg)
if not python_path.exists():
msg = f"Unreal Python not found at {python_path}"
self.failed.emit(msg, 1)
raise RuntimeError(msg)
subprocess.check_call(
[python_path.as_posix(), "-m", "pip", "install", "pyside2"]
)
self.progress.emit(100)
self.finished.emit("Project successfully built!")
class UEPluginInstallWorker(QtCore.QObject):
finished = QtCore.Signal(str)
installing = QtCore.Signal(str)
failed = QtCore.Signal(str, int)
progress = QtCore.Signal(int)
log = QtCore.Signal(str)
engine_path: Path = None
env = None
def setup(self, engine_path: Path, env: dict = None, ):
self.engine_path = engine_path
self.env = env or os.environ
def _build_and_move_plugin(self, plugin_build_path: Path):
uat_path: Path = ue_lib.get_path_to_uat(self.engine_path)
src_plugin_dir = Path(self.env.get("OPENPYPE_UNREAL_PLUGIN", ""))
if not os.path.isdir(src_plugin_dir):
msg = "Path to the integration plugin is null!"
self.failed.emit(msg, 1)
raise RuntimeError(msg)
if not uat_path.is_file():
msg = "Building failed! Path to UAT is invalid!"
self.failed.emit(msg, 1)
raise RuntimeError(msg)
temp_dir: Path = src_plugin_dir.parent / "Temp"
temp_dir.mkdir(exist_ok=True)
uplugin_path: Path = src_plugin_dir / "OpenPype.uplugin"
# in order to successfully build the plugin,
# It must be built outside the Engine directory and then moved
build_plugin_cmd: List[str] = [f'{uat_path.as_posix()}',
'BuildPlugin',
f'-Plugin={uplugin_path.as_posix()}',
f'-Package={temp_dir.as_posix()}']
build_proc = subprocess.Popen(build_plugin_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
for line in build_proc.stdout:
decoded_line: str = line.decode(errors='replace')
print(decoded_line, end='')
self.log.emit(decoded_line)
parse_comp_progress(decoded_line, self.progress)
build_proc.stdout.close()
return_code = build_proc.wait()
if return_code and return_code != 0:
msg = 'Failed to build plugin' \
f' project! Exited with return code {return_code}'
self.failed.emit(msg, return_code)
raise RuntimeError(msg)
# Copy the contents of the 'Temp' dir into the
# 'OpenPype' directory in the engine
dir_util.copy_tree(temp_dir.as_posix(),
plugin_build_path.as_posix())
# We need to also copy the config folder.
# The UAT doesn't include the Config folder in the build
plugin_install_config_path: Path = plugin_build_path / "Config"
src_plugin_config_path = src_plugin_dir / "Config"
dir_util.copy_tree(src_plugin_config_path.as_posix(),
plugin_install_config_path.as_posix())
dir_util.remove_tree(temp_dir.as_posix())
def run(self):
src_plugin_dir = Path(self.env.get("OPENPYPE_UNREAL_PLUGIN", ""))
if not os.path.isdir(src_plugin_dir):
msg = "Path to the integration plugin is null!"
self.failed.emit(msg, 1)
raise RuntimeError(msg)
# Create a path to the plugin in the engine
op_plugin_path = self.engine_path / "Engine/Plugins/Marketplace" \
"/OpenPype"
if not op_plugin_path.is_dir():
self.installing.emit("Installing and building the plugin ...")
op_plugin_path.mkdir(parents=True, exist_ok=True)
engine_plugin_config_path = op_plugin_path / "Config"
engine_plugin_config_path.mkdir(exist_ok=True)
dir_util._path_created = {}
if not (op_plugin_path / "Binaries").is_dir() \
or not (op_plugin_path / "Intermediate").is_dir():
self.installing.emit("Building the plugin ...")
print("--- Building the plugin...")
self._build_and_move_plugin(op_plugin_path)
self.finished.emit("Plugin successfully installed")