mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-01 16:34:53 +01:00
Merge branch 'ynput:develop' into feature/setup-fusion-profile-on-prelaunch
This commit is contained in:
commit
aade2eb315
86 changed files with 7633 additions and 5675 deletions
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ class ConnectGeometry(InventoryAction):
|
|||
bool
|
||||
"""
|
||||
|
||||
from Qt import QtWidgets
|
||||
from qtpy import QtWidgets
|
||||
|
||||
accept = QtWidgets.QMessageBox.Ok
|
||||
if show_cancel:
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ class ConnectXgen(InventoryAction):
|
|||
bool
|
||||
"""
|
||||
|
||||
from Qt import QtWidgets
|
||||
from qtpy import QtWidgets
|
||||
|
||||
accept = QtWidgets.QMessageBox.Ok
|
||||
if show_cancel:
|
||||
|
|
|
|||
|
|
@ -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", {})
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
|
|||
"rig",
|
||||
"camerarig",
|
||||
"staticMesh",
|
||||
"skeletalMesh",
|
||||
"mvLook"]
|
||||
|
||||
representations = ["ma", "abc", "fbx", "mb"]
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
27
openpype/hosts/maya/tools/__init__.py
Normal file
27
openpype/hosts/maya/tools/__init__.py
Normal 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()
|
||||
21
openpype/hosts/maya/tools/mayalookassigner/LICENSE
Normal file
21
openpype/hosts/maya/tools/mayalookassigner/LICENSE
Normal 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.
|
||||
9
openpype/hosts/maya/tools/mayalookassigner/__init__.py
Normal file
9
openpype/hosts/maya/tools/mayalookassigner/__init__.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
from .app import (
|
||||
MayaLookAssignerWindow,
|
||||
show
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"MayaLookAssignerWindow",
|
||||
"show"]
|
||||
287
openpype/hosts/maya/tools/mayalookassigner/app.py
Normal file
287
openpype/hosts/maya/tools/mayalookassigner/app.py
Normal 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
|
||||
217
openpype/hosts/maya/tools/mayalookassigner/commands.py
Normal file
217
openpype/hosts/maya/tools/mayalookassigner/commands.py
Normal 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)")
|
||||
129
openpype/hosts/maya/tools/mayalookassigner/models.py
Normal file
129
openpype/hosts/maya/tools/mayalookassigner/models.py
Normal 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()
|
||||
47
openpype/hosts/maya/tools/mayalookassigner/views.py
Normal file
47
openpype/hosts/maya/tools/mayalookassigner/views.py
Normal 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)
|
||||
305
openpype/hosts/maya/tools/mayalookassigner/vray_proxies.py
Normal file
305
openpype/hosts/maya/tools/mayalookassigner/vray_proxies.py
Normal 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)
|
||||
257
openpype/hosts/maya/tools/mayalookassigner/widgets.py
Normal file
257
openpype/hosts/maya/tools/mayalookassigner/widgets.py
Normal 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)
|
||||
|
|
@ -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."""
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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'):
|
||||
|
|
|
|||
335
openpype/hosts/unreal/ue_workers.py
Normal file
335
openpype/hosts/unreal/ue_workers.py
Normal 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")
|
||||
Loading…
Add table
Add a link
Reference in a new issue