Merge branch 'develop' into bugfix/OP-3191_Flame-publishing-bugs

This commit is contained in:
Jakub Jezek 2022-05-30 17:34:07 +02:00
commit 642391b930
No known key found for this signature in database
GPG key ID: 730D7C02726179A7
101 changed files with 1445 additions and 310 deletions

View file

@ -1,8 +1,24 @@
# Changelog
## [3.10.0-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD)
## [3.10.1-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.8...HEAD)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.10.0...HEAD)
**🚀 Enhancements**
- TVPaint: Init file for TVPaint worker also handle guideline images [\#3250](https://github.com/pypeclub/OpenPype/pull/3250)
- Support for Unreal 5 [\#3122](https://github.com/pypeclub/OpenPype/pull/3122)
**🐛 Bug fixes**
- Unreal: Fix Camera Loading if Layout is missing [\#3255](https://github.com/pypeclub/OpenPype/pull/3255)
- Unreal: Fixed Animation loading in UE5 [\#3240](https://github.com/pypeclub/OpenPype/pull/3240)
- Unreal: Fixed Render creation in UE5 [\#3239](https://github.com/pypeclub/OpenPype/pull/3239)
- Unreal: Fixed Camera loading in UE5 [\#3238](https://github.com/pypeclub/OpenPype/pull/3238)
## [3.10.0](https://github.com/pypeclub/OpenPype/tree/3.10.0) (2022-05-26)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.10.0-nightly.6...3.10.0)
**🆕 New features**
@ -10,11 +26,17 @@
- General: Creator plugins from addons can be registered [\#3179](https://github.com/pypeclub/OpenPype/pull/3179)
- Ftrack: Single image reviewable [\#3157](https://github.com/pypeclub/OpenPype/pull/3157)
- Nuke: Expose write attributes to settings [\#3123](https://github.com/pypeclub/OpenPype/pull/3123)
- Hiero: Initial frame publish support [\#3106](https://github.com/pypeclub/OpenPype/pull/3106)
**🚀 Enhancements**
- Maya: FBX camera export [\#3253](https://github.com/pypeclub/OpenPype/pull/3253)
- General: updating common vendor `scriptmenu` to 1.5.2 [\#3246](https://github.com/pypeclub/OpenPype/pull/3246)
- Project Manager: Allow to paste Tasks into multiple assets at the same time [\#3226](https://github.com/pypeclub/OpenPype/pull/3226)
- Project manager: Sped up project load [\#3216](https://github.com/pypeclub/OpenPype/pull/3216)
- Loader UI: Speed issues of loader with sync server [\#3199](https://github.com/pypeclub/OpenPype/pull/3199)
- Looks: add basic support for Renderman [\#3190](https://github.com/pypeclub/OpenPype/pull/3190)
- Maya: added clean\_import option to Import loader [\#3181](https://github.com/pypeclub/OpenPype/pull/3181)
- Add the scripts menu definition to nuke [\#3168](https://github.com/pypeclub/OpenPype/pull/3168)
- Maya: add maya 2023 to default applications [\#3167](https://github.com/pypeclub/OpenPype/pull/3167)
- Compressed bgeo publishing in SAP and Houdini loader [\#3153](https://github.com/pypeclub/OpenPype/pull/3153)
- General: Add 'dataclasses' to required python modules [\#3149](https://github.com/pypeclub/OpenPype/pull/3149)
@ -25,15 +47,19 @@
- General: Simplified OP modules/addons import [\#3137](https://github.com/pypeclub/OpenPype/pull/3137)
- Terminal: Tweak coloring of TrayModuleManager logging enabled states [\#3133](https://github.com/pypeclub/OpenPype/pull/3133)
- General: Cleanup some Loader docstrings [\#3131](https://github.com/pypeclub/OpenPype/pull/3131)
- Nuke: render instance with subset name filtered overrides [\#3117](https://github.com/pypeclub/OpenPype/pull/3117)
- Unreal: Layout and Camera update and remove functions reimplemented and improvements [\#3116](https://github.com/pypeclub/OpenPype/pull/3116)
- Settings: Remove environment groups from settings [\#3115](https://github.com/pypeclub/OpenPype/pull/3115)
- TVPaint: Match renderlayer key with other hosts [\#3110](https://github.com/pypeclub/OpenPype/pull/3110)
- Ftrack: AssetVersion status on publish [\#3108](https://github.com/pypeclub/OpenPype/pull/3108)
- Tray publisher: Simple families from settings [\#3105](https://github.com/pypeclub/OpenPype/pull/3105)
**🐛 Bug fixes**
- nuke: use framerange issue [\#3254](https://github.com/pypeclub/OpenPype/pull/3254)
- Ftrack: Chunk sizes for queries has minimal condition [\#3244](https://github.com/pypeclub/OpenPype/pull/3244)
- Maya: renderman displays needs to be filtered [\#3242](https://github.com/pypeclub/OpenPype/pull/3242)
- Ftrack: Validate that the user exists on ftrack [\#3237](https://github.com/pypeclub/OpenPype/pull/3237)
- Maya: Fix support for multiple resolutions [\#3236](https://github.com/pypeclub/OpenPype/pull/3236)
- TVPaint: Look for more groups than 12 [\#3228](https://github.com/pypeclub/OpenPype/pull/3228)
- Hiero: debugging frame range and other 3.10 [\#3222](https://github.com/pypeclub/OpenPype/pull/3222)
- Project Manager: Fix persistent editors on project change [\#3218](https://github.com/pypeclub/OpenPype/pull/3218)
- Deadline: instance data overwrite fix [\#3214](https://github.com/pypeclub/OpenPype/pull/3214)
- Ftrack: Push hierarchical attributes action works [\#3210](https://github.com/pypeclub/OpenPype/pull/3210)
- Standalone Publisher: Always create new representation for thumbnail [\#3203](https://github.com/pypeclub/OpenPype/pull/3203)
- Photoshop: skip collector when automatic testing [\#3202](https://github.com/pypeclub/OpenPype/pull/3202)
- Nuke: render/workfile version sync doesn't work on farm [\#3185](https://github.com/pypeclub/OpenPype/pull/3185)
@ -47,20 +73,21 @@
- Ftrack: Action delete old versions formatting works [\#3152](https://github.com/pypeclub/OpenPype/pull/3152)
- Deadline: fix the output directory [\#3144](https://github.com/pypeclub/OpenPype/pull/3144)
- General: New Session schema [\#3141](https://github.com/pypeclub/OpenPype/pull/3141)
- General: Missing version on headless mode crash properly [\#3136](https://github.com/pypeclub/OpenPype/pull/3136)
- TVPaint: Composite layers in reversed order [\#3135](https://github.com/pypeclub/OpenPype/pull/3135)
- Nuke: fixing default settings for workfile builder loaders [\#3120](https://github.com/pypeclub/OpenPype/pull/3120)
- Nuke: fix anatomy imageio regex default [\#3119](https://github.com/pypeclub/OpenPype/pull/3119)
- General: Python 3 compatibility in queries [\#3112](https://github.com/pypeclub/OpenPype/pull/3112)
- General: Collect loaded versions skips not existing representations [\#3095](https://github.com/pypeclub/OpenPype/pull/3095)
**🔀 Refactored code**
- Avalon repo removed from Jobs workflow [\#3193](https://github.com/pypeclub/OpenPype/pull/3193)
- General: Remove remaining imports from avalon [\#3130](https://github.com/pypeclub/OpenPype/pull/3130)
**Merged pull requests:**
- Harmony: message length in 21.1 [\#3257](https://github.com/pypeclub/OpenPype/pull/3257)
- Harmony: 21.1 fix [\#3249](https://github.com/pypeclub/OpenPype/pull/3249)
- Maya: added jpg to filter for Image Plane Loader [\#3223](https://github.com/pypeclub/OpenPype/pull/3223)
- Webpublisher: replace space by underscore in subset names [\#3160](https://github.com/pypeclub/OpenPype/pull/3160)
- StandalonePublisher: removed Extract Background plugins [\#3093](https://github.com/pypeclub/OpenPype/pull/3093)
## [3.9.8](https://github.com/pypeclub/OpenPype/tree/3.9.8) (2022-05-19)
@ -69,6 +96,7 @@
**🚀 Enhancements**
- nuke: generate publishing nodes inside render group node [\#3206](https://github.com/pypeclub/OpenPype/pull/3206)
- Loader UI: Speed issues of loader with sync server [\#3200](https://github.com/pypeclub/OpenPype/pull/3200)
- Backport of fix for attaching renders to subsets [\#3195](https://github.com/pypeclub/OpenPype/pull/3195)
**🐛 Bug fixes**
@ -114,31 +142,14 @@
- Nuke: render instance with subset name filtered overrides \(3.9.x\) [\#3125](https://github.com/pypeclub/OpenPype/pull/3125)
**🚀 Enhancements**
- TVPaint: Match renderlayer key with other hosts [\#3109](https://github.com/pypeclub/OpenPype/pull/3109)
**🐛 Bug fixes**
- General: Missing version on headless mode crash properly [\#3136](https://github.com/pypeclub/OpenPype/pull/3136)
- TVPaint: Composite layers in reversed order [\#3134](https://github.com/pypeclub/OpenPype/pull/3134)
- General: Python 3 compatibility in queries [\#3111](https://github.com/pypeclub/OpenPype/pull/3111)
**Merged pull requests:**
- Ftrack: AssetVersion status on publish [\#3114](https://github.com/pypeclub/OpenPype/pull/3114)
- renderman support for 3.9.x [\#3107](https://github.com/pypeclub/OpenPype/pull/3107)
## [3.9.5](https://github.com/pypeclub/OpenPype/tree/3.9.5) (2022-04-25)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.10.0-nightly.2...3.9.5)
**🐛 Bug fixes**
- Ftrack: Update Create Folders action [\#3092](https://github.com/pypeclub/OpenPype/pull/3092)
- General: Extract review sequence is not converted with same names [\#3075](https://github.com/pypeclub/OpenPype/pull/3075)
- Webpublisher: Use variant value [\#3072](https://github.com/pypeclub/OpenPype/pull/3072)
## [3.9.4](https://github.com/pypeclub/OpenPype/tree/3.9.4) (2022-04-15)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.9.4-nightly.2...3.9.4)

View file

@ -35,7 +35,11 @@ function Client() {
self.pack = function(num) {
var ascii='';
for (var i = 3; i >= 0; i--) {
ascii += String.fromCharCode((num >> (8 * i)) & 255);
var hex = ((num >> (8 * i)) & 255).toString(16);
if (hex.length < 2){
ascii += "0";
}
ascii += hex;
}
return ascii;
};
@ -279,19 +283,22 @@ function Client() {
};
self._send = function(message) {
var data = new QByteArray();
var outstr = new QDataStream(data, QIODevice.WriteOnly);
outstr.writeInt(0);
data.append('UTF-8');
outstr.device().seek(0);
outstr.writeInt(data.size() - 4);
var codec = QTextCodec.codecForUtfText(data);
var msg = codec.fromUnicode(message);
var l = msg.size();
var coded = new QByteArray('AH').append(self.pack(l));
coded = coded.append(msg);
self.socket.write(new QByteArray(coded));
self.logDebug('Sent.');
/** Harmony 21.1 doesn't have QDataStream anymore.
This means we aren't able to write bytes into QByteArray so we had
modify how content lenght is sent do the server.
Content lenght is sent as string of 8 char convertible into integer
(instead of 0x00000001[4 bytes] > "000000001"[8 bytes]) */
var codec_name = new QByteArray().append("UTF-8");
var codec = QTextCodec.codecForName(codec_name);
var msg = codec.fromUnicode(message);
var l = msg.size();
var header = new QByteArray().append('AH').append(self.pack(l));
var coded = msg.prepend(header);
self.socket.write(coded);
self.logDebug('Sent.');
};
self.waitForLock = function() {
@ -351,7 +358,14 @@ function start() {
app.avalonClient = new Client();
app.avalonClient.socket.connectToHost(host, port);
}
var menuBar = QApplication.activeWindow().menuBar();
var mainWindow = null;
var widgets = QApplication.topLevelWidgets();
for (var i = 0 ; i < widgets.length; i++) {
if (widgets[i] instanceof QMainWindow){
mainWindow = widgets[i];
}
}
var menuBar = mainWindow.menuBar();
var actions = menuBar.actions();
app.avalonMenu = null;

View file

@ -88,21 +88,25 @@ class Server(threading.Thread):
"""
current_time = time.time()
while True:
self.log.info("wait ttt")
# Receive the data in small chunks and retransmit it
request = None
header = self.connection.recv(6)
header = self.connection.recv(10)
if len(header) == 0:
# null data received, socket is closing.
self.log.info(f"[{self.timestamp()}] Connection closing.")
break
if header[0:2] != b"AH":
self.log.error("INVALID HEADER")
length = struct.unpack(">I", header[2:])[0]
content_length_str = header[2:].decode()
length = int(content_length_str, 16)
data = self.connection.recv(length)
while (len(data) < length):
# we didn't received everything in first try, lets wait for
# all data.
self.log.info("loop")
time.sleep(0.1)
if self.connection is None:
self.log.error(f"[{self.timestamp()}] "
@ -113,7 +117,7 @@ class Server(threading.Thread):
break
data += self.connection.recv(length - len(data))
self.log.debug("data:: {} {}".format(data, type(data)))
self.received += data.decode("utf-8")
pretty = self._pretty(self.received)
self.log.debug(

View file

@ -2,7 +2,6 @@
Host specific functions where host api is connected
"""
import contextlib
from copy import deepcopy
import os
import re
@ -171,7 +170,7 @@ def get_track_items(
# get selected track items or all in active sequence
if selection:
with contextlib.suppress(AttributeError):
try:
for track_item in selection:
log.info("___ track_item: {}".format(track_item))
# make sure only trackitems are selected
@ -188,6 +187,8 @@ def get_track_items(
):
log.info("___ valid trackitem: {}".format(track_item))
return_list.append(track_item)
except AttributeError:
pass
# collect all available active sequence track items
if not return_list:

View file

@ -1093,6 +1093,11 @@ class RenderProductsRenderman(ARenderProducts):
if not enabled:
continue
# Skip display types not producing any file output.
# Is there a better way to do it?
if not display_types.get(display["driverNode"]["type"]):
continue
aov_name = name
if aov_name == "rmanDefaultDisplay":
aov_name = "beauty"

View file

@ -83,7 +83,7 @@ class ImagePlaneLoader(load.LoaderPlugin):
families = ["image", "plate", "render"]
label = "Load imagePlane"
representations = ["mov", "exr", "preview", "png"]
representations = ["mov", "exr", "preview", "png", "jpg"]
icon = "image"
color = "orange"

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from maya import cmds # noqa
import pyblish.api
class CollectFbxCamera(pyblish.api.InstancePlugin):
"""Collect Camera for FBX export."""
order = pyblish.api.CollectorOrder + 0.2
label = "Collect Camera for FBX export"
families = ["camera"]
def process(self, instance):
if not instance.data.get("families"):
instance.data["families"] = []
if "fbx" not in instance.data["families"]:
instance.data["families"].append("fbx")
instance.data["cameras"] = True

View file

@ -22,10 +22,46 @@ RENDERER_NODE_TYPES = [
# redshift
"RedshiftMeshParameters"
]
SHAPE_ATTRS = set(SHAPE_ATTRS)
def get_pxr_multitexture_file_attrs(node):
attrs = []
for i in range(9):
if cmds.attributeQuery("filename{}".format(i), node=node, ex=True):
file = cmds.getAttr("{}.filename{}".format(node, i))
if file:
attrs.append("filename{}".format(i))
return attrs
FILE_NODES = {
"file": "fileTextureName",
"aiImage": "filename",
"RedshiftNormalMap": "text0",
"PxrBump": "filename",
"PxrNormalMap": "filename",
"PxrMultiTexture": get_pxr_multitexture_file_attrs,
"PxrPtexture": "filename",
"PxrTexture": "filename"
}
def get_attributes(dictionary, attr, node=None):
# type: (dict, str, str) -> list
if callable(dictionary[attr]):
val = dictionary[attr](node)
else:
val = dictionary.get(attr, [])
if not isinstance(val, list):
return [val]
return val
def get_look_attrs(node):
"""Returns attributes of a node that are important for the look.
@ -51,15 +87,14 @@ def get_look_attrs(node):
if cmds.objectType(node, isAType="shape"):
attrs = cmds.listAttr(node, changedSinceFileOpen=True) or []
for attr in attrs:
if attr in SHAPE_ATTRS:
if attr in SHAPE_ATTRS or \
attr not in SHAPE_ATTRS and attr.startswith('ai'):
result.append(attr)
elif attr.startswith('ai'):
result.append(attr)
return result
def node_uses_image_sequence(node):
def node_uses_image_sequence(node, node_path):
# type: (str) -> bool
"""Return whether file node uses an image sequence or single image.
Determine if a node uses an image sequence or just a single image,
@ -74,12 +109,15 @@ def node_uses_image_sequence(node):
"""
# useFrameExtension indicates an explicit image sequence
node_path = get_file_node_path(node).lower()
# The following tokens imply a sequence
patterns = ["<udim>", "<tile>", "<uvtile>", "u<u>_v<v>", "<frame0"]
patterns = ["<udim>", "<tile>", "<uvtile>",
"u<u>_v<v>", "<frame0", "<f4>"]
try:
use_frame_extension = cmds.getAttr('%s.useFrameExtension' % node)
except ValueError:
use_frame_extension = False
return (cmds.getAttr('%s.useFrameExtension' % node) or
return (use_frame_extension or
any(pattern in node_path for pattern in patterns))
@ -137,14 +175,15 @@ def seq_to_glob(path):
return path
def get_file_node_path(node):
def get_file_node_paths(node):
# type: (str) -> list
"""Get the file path used by a Maya file node.
Args:
node (str): Name of the Maya file node
Returns:
str: the file path in use
list: the file paths in use
"""
# if the path appears to be sequence, use computedFileTextureNamePattern,
@ -163,15 +202,20 @@ def get_file_node_path(node):
"<uvtile>"]
lower = texture_pattern.lower()
if any(pattern in lower for pattern in patterns):
return texture_pattern
return [texture_pattern]
if cmds.nodeType(node) == 'aiImage':
return cmds.getAttr('{0}.filename'.format(node))
if cmds.nodeType(node) == 'RedshiftNormalMap':
return cmds.getAttr('{}.tex0'.format(node))
try:
file_attributes = get_attributes(
FILE_NODES, cmds.nodeType(node), node)
except AttributeError:
file_attributes = "fileTextureName"
# otherwise use fileTextureName
return cmds.getAttr('{0}.fileTextureName'.format(node))
files = []
for file_attr in file_attributes:
if cmds.attributeQuery(file_attr, node=node, exists=True):
files.append(cmds.getAttr("{}.{}".format(node, file_attr)))
return files
def get_file_node_files(node):
@ -185,16 +229,21 @@ def get_file_node_files(node):
list: List of full file paths.
"""
paths = get_file_node_paths(node)
sequences = []
replaces = []
for index, path in enumerate(paths):
if node_uses_image_sequence(node, path):
glob_pattern = seq_to_glob(path)
sequences.extend(glob.glob(glob_pattern))
replaces.append(index)
path = get_file_node_path(node)
path = cmds.workspace(expandName=path)
if node_uses_image_sequence(node):
glob_pattern = seq_to_glob(path)
return glob.glob(glob_pattern)
elif os.path.exists(path):
return [path]
else:
return []
for index in replaces:
paths.pop(index)
paths.extend(sequences)
return [p for p in paths if os.path.exists(p)]
class CollectLook(pyblish.api.InstancePlugin):
@ -238,13 +287,13 @@ class CollectLook(pyblish.api.InstancePlugin):
"for %s" % instance.data['name'])
# Discover related object sets
self.log.info("Gathering sets..")
self.log.info("Gathering sets ...")
sets = self.collect_sets(instance)
# Lookup set (optimization)
instance_lookup = set(cmds.ls(instance, long=True))
self.log.info("Gathering set relations..")
self.log.info("Gathering set relations ...")
# Ensure iteration happen in a list so we can remove keys from the
# dict within the loop
@ -326,7 +375,10 @@ class CollectLook(pyblish.api.InstancePlugin):
"volumeShader",
"displacementShader",
"aiSurfaceShader",
"aiVolumeShader"]
"aiVolumeShader",
"rman__surface",
"rman__displacement"
]
if look_sets:
materials = []
@ -374,15 +426,17 @@ class CollectLook(pyblish.api.InstancePlugin):
or []
)
files = cmds.ls(history, type="file", long=True)
files.extend(cmds.ls(history, type="aiImage", long=True))
files.extend(cmds.ls(history, type="RedshiftNormalMap", long=True))
all_supported_nodes = FILE_NODES.keys()
files = []
for node_type in all_supported_nodes:
files.extend(cmds.ls(history, type=node_type, long=True))
self.log.info("Collected file nodes:\n{}".format(files))
# Collect textures if any file nodes are found
instance.data["resources"] = []
for n in files:
instance.data["resources"].append(self.collect_resource(n))
for res in self.collect_resources(n):
instance.data["resources"].append(res)
self.log.info("Collected resources: {}".format(instance.data["resources"]))
@ -502,7 +556,7 @@ class CollectLook(pyblish.api.InstancePlugin):
return attributes
def collect_resource(self, node):
def collect_resources(self, node):
"""Collect the link to the file(s) used (resource)
Args:
node (str): name of the node
@ -510,68 +564,69 @@ class CollectLook(pyblish.api.InstancePlugin):
Returns:
dict
"""
self.log.debug("processing: {}".format(node))
if cmds.nodeType(node) not in ["file", "aiImage", "RedshiftNormalMap"]:
all_supported_nodes = FILE_NODES.keys()
if cmds.nodeType(node) not in all_supported_nodes:
self.log.error(
"Unsupported file node: {}".format(cmds.nodeType(node)))
raise AssertionError("Unsupported file node")
if cmds.nodeType(node) == 'file':
self.log.debug(" - file node")
attribute = "{}.fileTextureName".format(node)
computed_attribute = "{}.computedFileTextureNamePattern".format(node)
elif cmds.nodeType(node) == 'aiImage':
self.log.debug("aiImage node")
attribute = "{}.filename".format(node)
computed_attribute = attribute
elif cmds.nodeType(node) == 'RedshiftNormalMap':
self.log.debug("RedshiftNormalMap node")
attribute = "{}.tex0".format(node)
computed_attribute = attribute
self.log.debug(" - got {}".format(cmds.nodeType(node)))
source = cmds.getAttr(attribute)
self.log.info(" - file source: {}".format(source))
color_space_attr = "{}.colorSpace".format(node)
try:
color_space = cmds.getAttr(color_space_attr)
except ValueError:
# node doesn't have colorspace attribute
color_space = "Raw"
# Compare with the computed file path, e.g. the one with the <UDIM>
# pattern in it, to generate some logging information about this
# difference
# computed_attribute = "{}.computedFileTextureNamePattern".format(node)
computed_source = cmds.getAttr(computed_attribute)
if source != computed_source:
self.log.debug("Detected computed file pattern difference "
"from original pattern: {0} "
"({1} -> {2})".format(node,
source,
computed_source))
attributes = get_attributes(FILE_NODES, cmds.nodeType(node), node)
for attribute in attributes:
source = cmds.getAttr("{}.{}".format(
node,
attribute
))
computed_attribute = "{}.{}".format(node, attribute)
if attribute == "fileTextureName":
computed_attribute = node + ".computedFileTextureNamePattern"
# We replace backslashes with forward slashes because V-Ray
# can't handle the UDIM files with the backslashes in the
# paths as the computed patterns
source = source.replace("\\", "/")
self.log.info(" - file source: {}".format(source))
color_space_attr = "{}.colorSpace".format(node)
try:
color_space = cmds.getAttr(color_space_attr)
except ValueError:
# node doesn't have colorspace attribute
color_space = "Raw"
# Compare with the computed file path, e.g. the one with
# the <UDIM> pattern in it, to generate some logging information
# about this difference
computed_source = cmds.getAttr(computed_attribute)
if source != computed_source:
self.log.debug("Detected computed file pattern difference "
"from original pattern: {0} "
"({1} -> {2})".format(node,
source,
computed_source))
files = get_file_node_files(node)
if len(files) == 0:
self.log.error("No valid files found from node `%s`" % node)
# We replace backslashes with forward slashes because V-Ray
# can't handle the UDIM files with the backslashes in the
# paths as the computed patterns
source = source.replace("\\", "/")
self.log.info("collection of resource done:")
self.log.info(" - node: {}".format(node))
self.log.info(" - attribute: {}".format(attribute))
self.log.info(" - source: {}".format(source))
self.log.info(" - file: {}".format(files))
self.log.info(" - color space: {}".format(color_space))
files = get_file_node_files(node)
if len(files) == 0:
self.log.error("No valid files found from node `%s`" % node)
# Define the resource
return {"node": node,
"attribute": attribute,
self.log.info("collection of resource done:")
self.log.info(" - node: {}".format(node))
self.log.info(" - attribute: {}".format(attribute))
self.log.info(" - source: {}".format(source))
self.log.info(" - file: {}".format(files))
self.log.info(" - color space: {}".format(color_space))
# Define the resource
yield {
"node": node,
# here we are passing not only attribute, but with node again
# this should be simplified and changed extractor.
"attribute": "{}.{}".format(node, attribute),
"source": source, # required for resources
"files": files,
"color_space": color_space} # required for resources
"color_space": color_space
} # required for resources
class CollectModelRenderSets(CollectLook):

View file

@ -339,9 +339,15 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
"source": filepath,
"expectedFiles": full_exp_files,
"publishRenderMetadataFolder": common_publish_meta_path,
"resolutionWidth": cmds.getAttr("defaultResolution.width"),
"resolutionHeight": cmds.getAttr("defaultResolution.height"),
"pixelAspect": cmds.getAttr("defaultResolution.pixelAspect"),
"resolutionWidth": lib.get_attr_in_layer(
"defaultResolution.height", layer=layer_name
),
"resolutionHeight": lib.get_attr_in_layer(
"defaultResolution.width", layer=layer_name
),
"pixelAspect": lib.get_attr_in_layer(
"defaultResolution.pixelAspect", layer=layer_name
),
"tileRendering": render_instance.data.get("tileRendering") or False, # noqa: E501
"tilesX": render_instance.data.get("tilesX") or 2,
"tilesY": render_instance.data.get("tilesY") or 2,

View file

@ -124,9 +124,15 @@ class CollectVrayScene(pyblish.api.InstancePlugin):
# Add source to allow tracing back to the scene from
# which was submitted originally
"source": context.data["currentFile"].replace("\\", "/"),
"resolutionWidth": cmds.getAttr("defaultResolution.width"),
"resolutionHeight": cmds.getAttr("defaultResolution.height"),
"pixelAspect": cmds.getAttr("defaultResolution.pixelAspect"),
"resolutionWidth": lib.get_attr_in_layer(
"defaultResolution.height", layer=layer_name
),
"resolutionHeight": lib.get_attr_in_layer(
"defaultResolution.width", layer=layer_name
),
"pixelAspect": lib.get_attr_in_layer(
"defaultResolution.pixelAspect", layer=layer_name
),
"priority": instance.data.get("priority"),
"useMultipleSceneFiles": instance.data.get(
"vraySceneMultipleFiles")

View file

@ -372,10 +372,12 @@ class ExtractLook(openpype.api.Extractor):
if mode == COPY:
transfers.append((source, destination))
self.log.info('copying')
self.log.info('file will be copied {} -> {}'.format(
source, destination))
elif mode == HARDLINK:
hardlinks.append((source, destination))
self.log.info('hardlinking')
self.log.info('file will be hardlinked {} -> {}'.format(
source, destination))
# Store the hashes from hash to destination to include in the
# database

View file

@ -373,7 +373,7 @@ def add_write_node_legacy(name, **kwarg):
Returns:
node (obj): nuke write node
"""
frame_range = kwarg.get("use_range_limit", None)
use_range_limit = kwarg.get("use_range_limit", None)
w = nuke.createNode(
"Write",
@ -391,10 +391,10 @@ def add_write_node_legacy(name, **kwarg):
log.debug(e)
continue
if frame_range:
if use_range_limit:
w["use_limit"].setValue(True)
w["first"].setValue(frame_range[0])
w["last"].setValue(frame_range[1])
w["first"].setValue(kwarg["frame_range"][0])
w["last"].setValue(kwarg["frame_range"][1])
return w
@ -409,7 +409,7 @@ def add_write_node(name, file_path, knobs, **kwarg):
Returns:
node (obj): nuke write node
"""
frame_range = kwarg.get("use_range_limit", None)
use_range_limit = kwarg.get("use_range_limit", None)
w = nuke.createNode(
"Write",
@ -420,10 +420,10 @@ def add_write_node(name, file_path, knobs, **kwarg):
# finally add knob overrides
set_node_knobs_from_settings(w, knobs, **kwarg)
if frame_range:
if use_range_limit:
w["use_limit"].setValue(True)
w["first"].setValue(frame_range[0])
w["last"].setValue(frame_range[1])
w["first"].setValue(kwarg["frame_range"][0])
w["last"].setValue(kwarg["frame_range"][1])
return w

View file

@ -18,7 +18,8 @@ from .lib import (
maintained_selection,
set_avalon_knob_data,
add_publish_knob,
get_nuke_imageio_settings
get_nuke_imageio_settings,
set_node_knobs_from_settings
)
@ -497,16 +498,7 @@ class ExporterReviewMov(ExporterReview):
add_tags.append("reformated")
rf_node = nuke.createNode("Reformat")
for kn_conf in reformat_node_config:
_type = kn_conf["type"]
k_name = str(kn_conf["name"])
k_value = kn_conf["value"]
# to remove unicode as nuke doesn't like it
if _type == "string":
k_value = str(kn_conf["value"])
rf_node[k_name].setValue(k_value)
set_node_knobs_from_settings(rf_node, reformat_node_config)
# connect
rf_node.setInput(0, self.previous_node)

View file

@ -27,6 +27,10 @@ class CreateWritePrerender(plugin.AbstractWriteRender):
# add fpath_template
write_data["fpath_template"] = self.fpath_template
write_data["use_range_limit"] = self.use_range_limit
write_data["frame_range"] = (
nuke.root()["first_frame"].value(),
nuke.root()["last_frame"].value()
)
if not self.is_legacy():
return create_write_node(

View file

@ -15,13 +15,13 @@ from openpype.hosts.nuke.api import (
class AlembicModelLoader(load.LoaderPlugin):
"""
This will load alembic model into script.
This will load alembic model or anim into script.
"""
families = ["model"]
families = ["model", "pointcache", "animation"]
representations = ["abc"]
label = "Load Alembic Model"
label = "Load Alembic"
icon = "cube"
color = "orange"
node_color = "0x4ecd91ff"

View file

@ -1,4 +1,5 @@
import nuke
import os
from openpype.api import Logger
from openpype.pipeline import install_host
@ -9,6 +10,7 @@ from openpype.hosts.nuke.api.lib import (
WorkfileSettings,
dirmap_file_name_filter
)
from openpype.settings import get_project_settings
log = Logger.get_logger(__name__)
@ -28,3 +30,32 @@ nuke.addOnScriptLoad(WorkfileSettings().set_context_settings)
nuke.addFilenameFilter(dirmap_file_name_filter)
log.info('Automatic syncing of write file knob to script version')
def add_scripts_menu():
try:
from scriptsmenu import launchfornuke
except ImportError:
log.warning(
"Skipping studio.menu install, because "
"'scriptsmenu' module seems unavailable."
)
return
# load configuration of custom menu
project_settings = get_project_settings(os.getenv("AVALON_PROJECT"))
config = project_settings["nuke"]["scriptsmenu"]["definition"]
_menu = project_settings["nuke"]["scriptsmenu"]["name"]
if not config:
log.warning("Skipping studio menu, no definition found.")
return
# run the launcher for Maya menu
studio_menu = launchfornuke.main(title=_menu.title())
# apply configuration
studio_menu.build_from_configuration(studio_menu, config)
add_scripts_menu()

View file

@ -84,7 +84,7 @@ class CollectColorCodedInstances(pyblish.api.ContextPlugin):
"variant": variant,
"family": resolved_family,
"task": task_name,
"layer": layer.name
"layer": layer.clean_name
}
subset = resolved_subset_template.format(

View file

@ -165,12 +165,12 @@ def parse_group_data(data):
if not group_raw:
continue
parts = group_raw.split(" ")
parts = group_raw.split("|")
# Check for length and concatenate 2 last items until length match
# - this happens if name contain spaces
while len(parts) > 6:
last_item = parts.pop(-1)
parts[-1] = " ".join([parts[-1], last_item])
parts[-1] = "|".join([parts[-1], last_item])
clip_id, group_id, red, green, blue, name = parts
group = {
@ -201,11 +201,16 @@ def get_groups_data(communicator=None):
george_script_lines = (
# Variable containing full path to output file
"output_path = \"{}\"".format(output_filepath),
"loop = 1",
"FOR idx = 1 TO 12",
"empty = 0",
# Loop over 100 groups
"FOR idx = 1 TO 100",
# Receive information about groups
"tv_layercolor \"getcolor\" 0 idx",
"tv_writetextfile \"strict\" \"append\" '\"'output_path'\"' result",
"END"
"PARSE result clip_id group_index c_red c_green c_blue group_name",
# Create and add line to output file
"line = clip_id'|'group_index'|'c_red'|'c_green'|'c_blue'|'group_name",
"tv_writetextfile \"strict\" \"append\" '\"'output_path'\"' line",
"END",
)
george_script = "\n".join(george_script_lines)
execute_george_through_file(george_script, communicator)

View file

@ -1,15 +1,19 @@
import os
import openpype.hosts
from openpype.lib.applications import Application
def add_implementation_envs(env, _app):
def add_implementation_envs(env: dict, _app: Application) -> None:
"""Modify environments to contain all required for implementation."""
# Set OPENPYPE_UNREAL_PLUGIN required for Unreal implementation
ue_plugin = "UE_5.0" if _app.name[:1] == "5" else "UE_4.7"
unreal_plugin_path = os.path.join(
os.path.dirname(os.path.abspath(openpype.hosts.__file__)),
"unreal", "integration"
"unreal", "integration", ue_plugin
)
env["OPENPYPE_UNREAL_PLUGIN"] = unreal_plugin_path
if not env.get("OPENPYPE_UNREAL_PLUGIN"):
env["OPENPYPE_UNREAL_PLUGIN"] = unreal_plugin_path
# Set default environments if are not set via settings
defaults = {

View file

@ -25,7 +25,7 @@ class UnrealPrelaunchHook(PreLaunchHook):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.signature = "( {} )".format(self.__class__.__name__)
self.signature = f"( {self.__class__.__name__} )"
def _get_work_filename(self):
# Use last workfile if was found
@ -71,7 +71,7 @@ class UnrealPrelaunchHook(PreLaunchHook):
if int(engine_version.split(".")[0]) < 4 and \
int(engine_version.split(".")[1]) < 26:
raise ApplicationLaunchFailed((
f"{self.signature} Old unsupported version of UE4 "
f"{self.signature} Old unsupported version of UE "
f"detected - {engine_version}"))
except ValueError:
# there can be string in minor version and in that case
@ -99,18 +99,19 @@ class UnrealPrelaunchHook(PreLaunchHook):
f"character ({unreal_project_name}). Appending 'P'"
))
unreal_project_name = f"P{unreal_project_name}"
unreal_project_filename = f'{unreal_project_name}.uproject'
project_path = Path(os.path.join(workdir, unreal_project_name))
self.log.info((
f"{self.signature} requested UE4 version: "
f"{self.signature} requested UE version: "
f"[ {engine_version} ]"
))
detected = unreal_lib.get_engine_versions(self.launch_context.env)
detected_str = ', '.join(detected.keys()) or 'none'
self.log.info((
f"{self.signature} detected UE4 versions: "
f"{self.signature} detected UE versions: "
f"[ {detected_str} ]"
))
if not detected:
@ -123,10 +124,10 @@ class UnrealPrelaunchHook(PreLaunchHook):
f"detected [ {engine_version} ]"
))
ue4_path = unreal_lib.get_editor_executable_path(
Path(detected[engine_version]))
ue_path = unreal_lib.get_editor_executable_path(
Path(detected[engine_version]), engine_version)
self.launch_context.launch_args = [ue4_path.as_posix()]
self.launch_context.launch_args = [ue_path.as_posix()]
project_path.mkdir(parents=True, exist_ok=True)
project_file = project_path / unreal_project_filename
@ -138,6 +139,11 @@ class UnrealPrelaunchHook(PreLaunchHook):
))
# Set "OPENPYPE_UNREAL_PLUGIN" to current process environment for
# execution of `create_unreal_project`
if self.launch_context.env.get("OPENPYPE_UNREAL_PLUGIN"):
self.log.info((
f"{self.signature} using OpenPype plugin from "
f"{self.launch_context.env.get('OPENPYPE_UNREAL_PLUGIN')}"
))
env_key = "OPENPYPE_UNREAL_PLUGIN"
if self.launch_context.env.get(env_key):
os.environ[env_key] = self.launch_context.env[env_key]

View file

@ -1,4 +1,4 @@
# OpenPype Unreal Integration plugin
# OpenPype Unreal Integration plugin - UE 4.x
This is plugin for Unreal Editor, creating menu for [OpenPype](https://github.com/getavalon) tools to run.

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Before After
Before After

View file

@ -0,0 +1,35 @@
# Prerequisites
*.d
# Compiled Object files
*.slo
*.lo
*.o
*.obj
# Precompiled Headers
*.gch
*.pch
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Fortran module files
*.mod
*.smod
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
*.exe
*.out
*.app
/Binaries
/Intermediate

View file

@ -0,0 +1,28 @@
import unreal
openpype_detected = True
try:
from openpype.pipeline import install_host
from openpype.hosts.unreal import api as openpype_host
except ImportError as exc:
openpype_host = None
openpype_detected = False
unreal.log_error("OpenPype: cannot load OpenPype [ {} ]".format(exc))
if openpype_detected:
install_host(openpype_host)
@unreal.uclass()
class OpenPypeIntegration(unreal.OpenPypePythonBridge):
@unreal.ufunction(override=True)
def RunInPython_Popup(self):
unreal.log_warning("OpenPype: showing tools popup")
if openpype_detected:
openpype_host.show_tools_popup()
@unreal.ufunction(override=True)
def RunInPython_Dialog(self):
unreal.log_warning("OpenPype: showing tools dialog")
if openpype_detected:
openpype_host.show_tools_dialog()

View file

@ -0,0 +1,24 @@
{
"FileVersion": 3,
"Version": 1,
"VersionName": "1.0",
"FriendlyName": "OpenPype",
"Description": "OpenPype Integration",
"Category": "OpenPype.Integration",
"CreatedBy": "Ondrej Samohel",
"CreatedByURL": "https://openpype.io",
"DocsURL": "https://openpype.io/docs/artist_hosts_unreal",
"MarketplaceURL": "",
"SupportURL": "https://pype.club/",
"CanContainContent": true,
"IsBetaVersion": true,
"IsExperimentalVersion": false,
"Installed": false,
"Modules": [
{
"Name": "OpenPype",
"Type": "Editor",
"LoadingPhase": "Default"
}
]
}

View file

@ -0,0 +1,11 @@
# OpenPype Unreal Integration plugin - UE 5.x
This is plugin for Unreal Editor, creating menu for [OpenPype](https://github.com/getavalon) tools to run.
## How does this work
Plugin is creating basic menu items in **Window/OpenPype** section of Unreal Editor main menu and a button
on the main toolbar with associated menu. Clicking on those menu items is calling callbacks that are
declared in C++ but needs to be implemented during Unreal Editor
startup in `Plugins/OpenPype/Content/Python/init_unreal.py` - this should be executed by Unreal Editor
automatically.

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View file

@ -0,0 +1,59 @@
// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved.
using UnrealBuildTool;
public class OpenPype : ModuleRules
{
public OpenPype(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
PublicIncludePaths.AddRange(
new string[] {
// ... add public include paths required here ...
}
);
PrivateIncludePaths.AddRange(
new string[] {
// ... add other private include paths required here ...
}
);
PublicDependencyModuleNames.AddRange(
new string[]
{
"Core",
// ... add other public dependencies that you statically link with here ...
}
);
PrivateDependencyModuleNames.AddRange(
new string[]
{
"Projects",
"InputCore",
"EditorFramework",
"UnrealEd",
"ToolMenus",
"LevelEditor",
"CoreUObject",
"Engine",
"Slate",
"SlateCore",
// ... add private dependencies that you statically link with here ...
}
);
DynamicallyLoadedModuleNames.AddRange(
new string[]
{
// ... add any modules that your module loads dynamically here ...
}
);
}
}

View file

@ -0,0 +1,115 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "AssetContainer.h"
#include "AssetRegistryModule.h"
#include "Misc/PackageName.h"
#include "Engine.h"
#include "Containers/UnrealString.h"
UAssetContainer::UAssetContainer(const FObjectInitializer& ObjectInitializer)
: UAssetUserData(ObjectInitializer)
{
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
FString path = UAssetContainer::GetPathName();
UE_LOG(LogTemp, Warning, TEXT("UAssetContainer %s"), *path);
FARFilter Filter;
Filter.PackagePaths.Add(FName(*path));
AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAssetContainer::OnAssetAdded);
AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAssetContainer::OnAssetRemoved);
AssetRegistryModule.Get().OnAssetRenamed().AddUObject(this, &UAssetContainer::OnAssetRenamed);
}
void UAssetContainer::OnAssetAdded(const FAssetData& AssetData)
{
TArray<FString> split;
// get directory of current container
FString selfFullPath = UAssetContainer::GetPathName();
FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath);
// get asset path and class
FString assetPath = AssetData.GetFullName();
FString assetFName = AssetData.AssetClass.ToString();
// split path
assetPath.ParseIntoArray(split, TEXT(" "), true);
FString assetDir = FPackageName::GetLongPackagePath(*split[1]);
// take interest only in paths starting with path of current container
if (assetDir.StartsWith(*selfDir))
{
// exclude self
if (assetFName != "AssetContainer")
{
assets.Add(assetPath);
assetsData.Add(AssetData);
UE_LOG(LogTemp, Log, TEXT("%s: asset added to %s"), *selfFullPath, *selfDir);
}
}
}
void UAssetContainer::OnAssetRemoved(const FAssetData& AssetData)
{
TArray<FString> split;
// get directory of current container
FString selfFullPath = UAssetContainer::GetPathName();
FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath);
// get asset path and class
FString assetPath = AssetData.GetFullName();
FString assetFName = AssetData.AssetClass.ToString();
// split path
assetPath.ParseIntoArray(split, TEXT(" "), true);
FString assetDir = FPackageName::GetLongPackagePath(*split[1]);
// take interest only in paths starting with path of current container
FString path = UAssetContainer::GetPathName();
FString lpp = FPackageName::GetLongPackagePath(*path);
if (assetDir.StartsWith(*selfDir))
{
// exclude self
if (assetFName != "AssetContainer")
{
// UE_LOG(LogTemp, Warning, TEXT("%s: asset removed"), *lpp);
assets.Remove(assetPath);
assetsData.Remove(AssetData);
}
}
}
void UAssetContainer::OnAssetRenamed(const FAssetData& AssetData, const FString& str)
{
TArray<FString> split;
// get directory of current container
FString selfFullPath = UAssetContainer::GetPathName();
FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath);
// get asset path and class
FString assetPath = AssetData.GetFullName();
FString assetFName = AssetData.AssetClass.ToString();
// split path
assetPath.ParseIntoArray(split, TEXT(" "), true);
FString assetDir = FPackageName::GetLongPackagePath(*split[1]);
if (assetDir.StartsWith(*selfDir))
{
// exclude self
if (assetFName != "AssetContainer")
{
assets.Remove(str);
assets.Add(assetPath);
assetsData.Remove(AssetData);
// UE_LOG(LogTemp, Warning, TEXT("%s: asset renamed %s"), *lpp, *str);
}
}
}

View file

@ -0,0 +1,20 @@
#include "AssetContainerFactory.h"
#include "AssetContainer.h"
UAssetContainerFactory::UAssetContainerFactory(const FObjectInitializer& ObjectInitializer)
: UFactory(ObjectInitializer)
{
SupportedClass = UAssetContainer::StaticClass();
bCreateNew = false;
bEditorImport = true;
}
UObject* UAssetContainerFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
{
UAssetContainer* AssetContainer = NewObject<UAssetContainer>(InParent, Class, Name, Flags);
return AssetContainer;
}
bool UAssetContainerFactory::ShouldShowInNewMenu() const {
return false;
}

View file

@ -0,0 +1,85 @@
#include "OpenPype.h"
#include "OpenPypeStyle.h"
#include "OpenPypeCommands.h"
#include "OpenPypePythonBridge.h"
#include "LevelEditor.h"
#include "Misc/MessageDialog.h"
#include "ToolMenus.h"
static const FName OpenPypeTabName("OpenPype");
#define LOCTEXT_NAMESPACE "FOpenPypeModule"
// This function is triggered when the plugin is staring up
void FOpenPypeModule::StartupModule()
{
FOpenPypeStyle::Initialize();
FOpenPypeStyle::ReloadTextures();
FOpenPypeCommands::Register();
PluginCommands = MakeShareable(new FUICommandList);
PluginCommands->MapAction(
FOpenPypeCommands::Get().OpenPypeTools,
FExecuteAction::CreateRaw(this, &FOpenPypeModule::MenuPopup),
FCanExecuteAction());
PluginCommands->MapAction(
FOpenPypeCommands::Get().OpenPypeToolsDialog,
FExecuteAction::CreateRaw(this, &FOpenPypeModule::MenuDialog),
FCanExecuteAction());
UToolMenus::RegisterStartupCallback(FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FOpenPypeModule::RegisterMenus));
}
void FOpenPypeModule::ShutdownModule()
{
UToolMenus::UnRegisterStartupCallback(this);
UToolMenus::UnregisterOwner(this);
FOpenPypeStyle::Shutdown();
FOpenPypeCommands::Unregister();
}
void FOpenPypeModule::RegisterMenus()
{
// Owner will be used for cleanup in call to UToolMenus::UnregisterOwner
FToolMenuOwnerScoped OwnerScoped(this);
{
UToolMenu* Menu = UToolMenus::Get()->ExtendMenu("LevelEditor.MainMenu.Tools");
{
// FToolMenuSection& Section = Menu->FindOrAddSection("OpenPype");
FToolMenuSection& Section = Menu->AddSection(
"OpenPype",
TAttribute<FText>(FText::FromString("OpenPype")),
FToolMenuInsert("Programming", EToolMenuInsertType::Before)
);
Section.AddMenuEntryWithCommandList(FOpenPypeCommands::Get().OpenPypeTools, PluginCommands);
Section.AddMenuEntryWithCommandList(FOpenPypeCommands::Get().OpenPypeToolsDialog, PluginCommands);
}
UToolMenu* ToolbarMenu = UToolMenus::Get()->ExtendMenu("LevelEditor.LevelEditorToolBar.PlayToolBar");
{
FToolMenuSection& Section = ToolbarMenu->FindOrAddSection("PluginTools");
{
FToolMenuEntry& Entry = Section.AddEntry(FToolMenuEntry::InitToolBarButton(FOpenPypeCommands::Get().OpenPypeTools));
Entry.SetCommandList(PluginCommands);
}
}
}
}
void FOpenPypeModule::MenuPopup() {
UOpenPypePythonBridge* bridge = UOpenPypePythonBridge::Get();
bridge->RunInPython_Popup();
}
void FOpenPypeModule::MenuDialog() {
UOpenPypePythonBridge* bridge = UOpenPypePythonBridge::Get();
bridge->RunInPython_Dialog();
}
IMPLEMENT_MODULE(FOpenPypeModule, OpenPype)

View file

@ -0,0 +1,13 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "OpenPypeCommands.h"
#define LOCTEXT_NAMESPACE "FOpenPypeModule"
void FOpenPypeCommands::RegisterCommands()
{
UI_COMMAND(OpenPypeTools, "OpenPype Tools", "Pipeline tools", EUserInterfaceActionType::Button, FInputChord());
UI_COMMAND(OpenPypeToolsDialog, "OpenPype Tools Dialog", "Pipeline tools dialog", EUserInterfaceActionType::Button, FInputChord());
}
#undef LOCTEXT_NAMESPACE

View file

@ -0,0 +1,48 @@
#include "OpenPypeLib.h"
#include "Misc/Paths.h"
#include "Misc/ConfigCacheIni.h"
#include "UObject/UnrealType.h"
/**
* Sets color on folder icon on given path
* @param InPath - path to folder
* @param InFolderColor - color of the folder
* @warning This color will appear only after Editor restart. Is there a better way?
*/
void UOpenPypeLib::CSetFolderColor(FString FolderPath, FLinearColor FolderColor, bool bForceAdd)
{
auto SaveColorInternal = [](FString InPath, FLinearColor InFolderColor)
{
// Saves the color of the folder to the config
if (FPaths::FileExists(GEditorPerProjectIni))
{
GConfig->SetString(TEXT("PathColor"), *InPath, *InFolderColor.ToString(), GEditorPerProjectIni);
}
};
SaveColorInternal(FolderPath, FolderColor);
}
/**
* Returns all poperties on given object
* @param cls - class
* @return TArray of properties
*/
TArray<FString> UOpenPypeLib::GetAllProperties(UClass* cls)
{
TArray<FString> Ret;
if (cls != nullptr)
{
for (TFieldIterator<FProperty> It(cls); It; ++It)
{
FProperty* Property = *It;
if (Property->HasAnyPropertyFlags(EPropertyFlags::CPF_Edit))
{
Ret.Add(Property->GetName());
}
}
}
return Ret;
}

View file

@ -0,0 +1,108 @@
#pragma once
#include "OpenPypePublishInstance.h"
#include "AssetRegistryModule.h"
UOpenPypePublishInstance::UOpenPypePublishInstance(const FObjectInitializer& ObjectInitializer)
: UObject(ObjectInitializer)
{
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
FString path = UOpenPypePublishInstance::GetPathName();
FARFilter Filter;
Filter.PackagePaths.Add(FName(*path));
AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UOpenPypePublishInstance::OnAssetAdded);
AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UOpenPypePublishInstance::OnAssetRemoved);
AssetRegistryModule.Get().OnAssetRenamed().AddUObject(this, &UOpenPypePublishInstance::OnAssetRenamed);
}
void UOpenPypePublishInstance::OnAssetAdded(const FAssetData& AssetData)
{
TArray<FString> split;
// get directory of current container
FString selfFullPath = UOpenPypePublishInstance::GetPathName();
FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath);
// get asset path and class
FString assetPath = AssetData.GetFullName();
FString assetFName = AssetData.AssetClass.ToString();
// split path
assetPath.ParseIntoArray(split, TEXT(" "), true);
FString assetDir = FPackageName::GetLongPackagePath(*split[1]);
// take interest only in paths starting with path of current container
if (assetDir.StartsWith(*selfDir))
{
// exclude self
if (assetFName != "OpenPypePublishInstance")
{
assets.Add(assetPath);
UE_LOG(LogTemp, Log, TEXT("%s: asset added to %s"), *selfFullPath, *selfDir);
}
}
}
void UOpenPypePublishInstance::OnAssetRemoved(const FAssetData& AssetData)
{
TArray<FString> split;
// get directory of current container
FString selfFullPath = UOpenPypePublishInstance::GetPathName();
FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath);
// get asset path and class
FString assetPath = AssetData.GetFullName();
FString assetFName = AssetData.AssetClass.ToString();
// split path
assetPath.ParseIntoArray(split, TEXT(" "), true);
FString assetDir = FPackageName::GetLongPackagePath(*split[1]);
// take interest only in paths starting with path of current container
FString path = UOpenPypePublishInstance::GetPathName();
FString lpp = FPackageName::GetLongPackagePath(*path);
if (assetDir.StartsWith(*selfDir))
{
// exclude self
if (assetFName != "OpenPypePublishInstance")
{
// UE_LOG(LogTemp, Warning, TEXT("%s: asset removed"), *lpp);
assets.Remove(assetPath);
}
}
}
void UOpenPypePublishInstance::OnAssetRenamed(const FAssetData& AssetData, const FString& str)
{
TArray<FString> split;
// get directory of current container
FString selfFullPath = UOpenPypePublishInstance::GetPathName();
FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath);
// get asset path and class
FString assetPath = AssetData.GetFullName();
FString assetFName = AssetData.AssetClass.ToString();
// split path
assetPath.ParseIntoArray(split, TEXT(" "), true);
FString assetDir = FPackageName::GetLongPackagePath(*split[1]);
if (assetDir.StartsWith(*selfDir))
{
// exclude self
if (assetFName != "AssetContainer")
{
assets.Remove(str);
assets.Add(assetPath);
// UE_LOG(LogTemp, Warning, TEXT("%s: asset renamed %s"), *lpp, *str);
}
}
}

View file

@ -0,0 +1,20 @@
#include "OpenPypePublishInstanceFactory.h"
#include "OpenPypePublishInstance.h"
UOpenPypePublishInstanceFactory::UOpenPypePublishInstanceFactory(const FObjectInitializer& ObjectInitializer)
: UFactory(ObjectInitializer)
{
SupportedClass = UOpenPypePublishInstance::StaticClass();
bCreateNew = false;
bEditorImport = true;
}
UObject* UOpenPypePublishInstanceFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
{
UOpenPypePublishInstance* OpenPypePublishInstance = NewObject<UOpenPypePublishInstance>(InParent, Class, Name, Flags);
return OpenPypePublishInstance;
}
bool UOpenPypePublishInstanceFactory::ShouldShowInNewMenu() const {
return false;
}

View file

@ -0,0 +1,13 @@
#include "OpenPypePythonBridge.h"
UOpenPypePythonBridge* UOpenPypePythonBridge::Get()
{
TArray<UClass*> OpenPypePythonBridgeClasses;
GetDerivedClasses(UOpenPypePythonBridge::StaticClass(), OpenPypePythonBridgeClasses);
int32 NumClasses = OpenPypePythonBridgeClasses.Num();
if (NumClasses > 0)
{
return Cast<UOpenPypePythonBridge>(OpenPypePythonBridgeClasses[NumClasses - 1]->GetDefaultObject());
}
return nullptr;
};

View file

@ -0,0 +1,61 @@
#include "OpenPype.h"
#include "OpenPypeStyle.h"
#include "Framework/Application/SlateApplication.h"
#include "Styling/SlateStyleRegistry.h"
#include "Slate/SlateGameResources.h"
#include "Interfaces/IPluginManager.h"
#include "Styling/SlateStyleMacros.h"
#define RootToContentDir Style->RootToContentDir
TSharedPtr<FSlateStyleSet> FOpenPypeStyle::OpenPypeStyleInstance = nullptr;
void FOpenPypeStyle::Initialize()
{
if (!OpenPypeStyleInstance.IsValid())
{
OpenPypeStyleInstance = Create();
FSlateStyleRegistry::RegisterSlateStyle(*OpenPypeStyleInstance);
}
}
void FOpenPypeStyle::Shutdown()
{
FSlateStyleRegistry::UnRegisterSlateStyle(*OpenPypeStyleInstance);
ensure(OpenPypeStyleInstance.IsUnique());
OpenPypeStyleInstance.Reset();
}
FName FOpenPypeStyle::GetStyleSetName()
{
static FName StyleSetName(TEXT("OpenPypeStyle"));
return StyleSetName;
}
const FVector2D Icon16x16(16.0f, 16.0f);
const FVector2D Icon20x20(20.0f, 20.0f);
const FVector2D Icon40x40(40.0f, 40.0f);
TSharedRef< FSlateStyleSet > FOpenPypeStyle::Create()
{
TSharedRef< FSlateStyleSet > Style = MakeShareable(new FSlateStyleSet("OpenPypeStyle"));
Style->SetContentRoot(IPluginManager::Get().FindPlugin("OpenPype")->GetBaseDir() / TEXT("Resources"));
Style->Set("OpenPype.OpenPypeTools", new IMAGE_BRUSH(TEXT("openpype40"), Icon40x40));
Style->Set("OpenPype.OpenPypeToolsDialog", new IMAGE_BRUSH(TEXT("openpype40"), Icon40x40));
return Style;
}
void FOpenPypeStyle::ReloadTextures()
{
if (FSlateApplication::IsInitialized())
{
FSlateApplication::Get().GetRenderer()->ReloadTextureResources();
}
}
const ISlateStyle& FOpenPypeStyle::Get()
{
return *OpenPypeStyleInstance;
}

View file

@ -0,0 +1,39 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "Engine/AssetUserData.h"
#include "AssetData.h"
#include "AssetContainer.generated.h"
/**
*
*/
UCLASS(Blueprintable)
class OPENPYPE_API UAssetContainer : public UAssetUserData
{
GENERATED_BODY()
public:
UAssetContainer(const FObjectInitializer& ObjectInitalizer);
// ~UAssetContainer();
UPROPERTY(EditAnywhere, BlueprintReadOnly)
TArray<FString> assets;
// There seems to be no reflection option to expose array of FAssetData
/*
UPROPERTY(Transient, BlueprintReadOnly, Category = "Python", meta=(DisplayName="Assets Data"))
TArray<FAssetData> assetsData;
*/
private:
TArray<FAssetData> assetsData;
void OnAssetAdded(const FAssetData& AssetData);
void OnAssetRemoved(const FAssetData& AssetData);
void OnAssetRenamed(const FAssetData& AssetData, const FString& str);
};

View file

@ -0,0 +1,21 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Factories/Factory.h"
#include "AssetContainerFactory.generated.h"
/**
*
*/
UCLASS()
class OPENPYPE_API UAssetContainerFactory : public UFactory
{
GENERATED_BODY()
public:
UAssetContainerFactory(const FObjectInitializer& ObjectInitializer);
virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
virtual bool ShouldShowInNewMenu() const override;
};

View file

@ -0,0 +1,23 @@
// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"
class FOpenPypeModule : public IModuleInterface
{
public:
virtual void StartupModule() override;
virtual void ShutdownModule() override;
private:
void RegisterMenus();
void MenuPopup();
void MenuDialog();
private:
TSharedPtr<class FUICommandList> PluginCommands;
};

View file

@ -0,0 +1,24 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Framework/Commands/Commands.h"
#include "OpenPypeStyle.h"
class FOpenPypeCommands : public TCommands<FOpenPypeCommands>
{
public:
FOpenPypeCommands()
: TCommands<FOpenPypeCommands>(TEXT("OpenPype"), NSLOCTEXT("Contexts", "OpenPype", "OpenPype Tools"), NAME_None, FOpenPypeStyle::GetStyleSetName())
{
}
// TCommands<> interface
virtual void RegisterCommands() override;
public:
TSharedPtr< FUICommandInfo > OpenPypeTools;
TSharedPtr< FUICommandInfo > OpenPypeToolsDialog;
};

View file

@ -0,0 +1,19 @@
#pragma once
#include "Engine.h"
#include "OpenPypeLib.generated.h"
UCLASS(Blueprintable)
class OPENPYPE_API UOpenPypeLib : public UObject
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = Python)
static void CSetFolderColor(FString FolderPath, FLinearColor FolderColor, bool bForceAdd);
UFUNCTION(BlueprintCallable, Category = Python)
static TArray<FString> GetAllProperties(UClass* cls);
};

View file

@ -0,0 +1,21 @@
#pragma once
#include "Engine.h"
#include "OpenPypePublishInstance.generated.h"
UCLASS(Blueprintable)
class OPENPYPE_API UOpenPypePublishInstance : public UObject
{
GENERATED_BODY()
public:
UOpenPypePublishInstance(const FObjectInitializer& ObjectInitalizer);
UPROPERTY(EditAnywhere, BlueprintReadOnly)
TArray<FString> assets;
private:
void OnAssetAdded(const FAssetData& AssetData);
void OnAssetRemoved(const FAssetData& AssetData);
void OnAssetRenamed(const FAssetData& AssetData, const FString& str);
};

View file

@ -0,0 +1,19 @@
#pragma once
#include "CoreMinimal.h"
#include "Factories/Factory.h"
#include "OpenPypePublishInstanceFactory.generated.h"
/**
*
*/
UCLASS()
class OPENPYPE_API UOpenPypePublishInstanceFactory : public UFactory
{
GENERATED_BODY()
public:
UOpenPypePublishInstanceFactory(const FObjectInitializer& ObjectInitializer);
virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
virtual bool ShouldShowInNewMenu() const override;
};

View file

@ -0,0 +1,20 @@
#pragma once
#include "Engine.h"
#include "OpenPypePythonBridge.generated.h"
UCLASS(Blueprintable)
class UOpenPypePythonBridge : public UObject
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = Python)
static UOpenPypePythonBridge* Get();
UFUNCTION(BlueprintImplementableEvent, Category = Python)
void RunInPython_Popup() const;
UFUNCTION(BlueprintImplementableEvent, Category = Python)
void RunInPython_Dialog() const;
};

View file

@ -0,0 +1,18 @@
#pragma once
#include "CoreMinimal.h"
#include "Styling/SlateStyle.h"
class FOpenPypeStyle
{
public:
static void Initialize();
static void Shutdown();
static void ReloadTextures();
static const ISlateStyle& Get();
static FName GetStyleSetName();
private:
static TSharedRef< class FSlateStyleSet > Create();
static TSharedPtr< class FSlateStyleSet > OpenPypeStyleInstance;
};

View file

@ -70,19 +70,22 @@ def get_engine_versions(env=None):
return OrderedDict()
def get_editor_executable_path(engine_path: Path) -> Path:
"""Get UE4 Editor executable path."""
ue4_path = engine_path / "Engine/Binaries"
def get_editor_executable_path(engine_path: Path, engine_version: str) -> Path:
"""Get UE Editor executable path."""
ue_path = engine_path / "Engine/Binaries"
if platform.system().lower() == "windows":
ue4_path /= "Win64/UE4Editor.exe"
if engine_version.split(".")[0] == "4":
ue_path /= "Win64/UE4Editor.exe"
elif engine_version.split(".")[0] == "5":
ue_path /= "Win64/UnrealEditor.exe"
elif platform.system().lower() == "linux":
ue4_path /= "Linux/UE4Editor"
ue_path /= "Linux/UE4Editor"
elif platform.system().lower() == "darwin":
ue4_path /= "Mac/UE4Editor"
ue_path /= "Mac/UE4Editor"
return ue4_path
return ue_path
def _win_get_engine_versions():
@ -208,22 +211,26 @@ def create_unreal_project(project_name: str,
# created in different UE4 version. When user convert such project
# to his UE4 version, Engine ID is replaced in uproject file. If some
# other user tries to open it, it will present him with similar error.
ue4_modules = Path()
ue_modules = Path()
if platform.system().lower() == "windows":
ue4_modules = Path(os.path.join(engine_path, "Engine", "Binaries",
"Win64", "UE4Editor.modules"))
ue_modules_path = engine_path / "Engine/Binaries/Win64"
if ue_version.split(".")[0] == "4":
ue_modules_path /= "UE4Editor.modules"
elif ue_version.split(".")[0] == "5":
ue_modules_path /= "UnrealEditor.modules"
ue_modules = Path(ue_modules_path)
if platform.system().lower() == "linux":
ue4_modules = Path(os.path.join(engine_path, "Engine", "Binaries",
ue_modules = Path(os.path.join(engine_path, "Engine", "Binaries",
"Linux", "UE4Editor.modules"))
if platform.system().lower() == "darwin":
ue4_modules = Path(os.path.join(engine_path, "Engine", "Binaries",
ue_modules = Path(os.path.join(engine_path, "Engine", "Binaries",
"Mac", "UE4Editor.modules"))
if ue4_modules.exists():
if ue_modules.exists():
print("--- Loading Engine ID from modules file ...")
with open(ue4_modules, "r") as mp:
with open(ue_modules, "r") as mp:
loaded_modules = json.load(mp)
if loaded_modules.get("BuildId"):
@ -280,7 +287,7 @@ def create_unreal_project(project_name: str,
python_path = None
if platform.system().lower() == "windows":
python_path = engine_path / ("Engine/Binaries/ThirdParty/"
"Python3/Win64/pythonw.exe")
"Python3/Win64/python.exe")
if platform.system().lower() == "linux":
python_path = engine_path / ("Engine/Binaries/ThirdParty/"
@ -294,14 +301,15 @@ def create_unreal_project(project_name: str,
raise NotImplementedError("Unsupported platform")
if not python_path.exists():
raise RuntimeError(f"Unreal Python not found at {python_path}")
subprocess.run(
subprocess.check_call(
[python_path.as_posix(), "-m", "pip", "install", "pyside2"])
if dev_mode or preset["dev_mode"]:
_prepare_cpp_project(project_file, engine_path)
_prepare_cpp_project(project_file, engine_path, ue_version)
def _prepare_cpp_project(project_file: Path, engine_path: Path) -> None:
def _prepare_cpp_project(
project_file: Path, engine_path: Path, ue_version: str) -> None:
"""Prepare CPP Unreal Project.
This function will add source files needed for project to be
@ -420,8 +428,12 @@ class {1}_API A{0}GameModeBase : public AGameModeBase
with open(sources_dir / f"{project_name}GameModeBase.h", mode="w") as f:
f.write(game_mode_h)
u_build_tool = Path(
engine_path / "Engine/Binaries/DotNET/UnrealBuildTool.exe")
u_build_tool_path = engine_path / "Engine/Binaries/DotNET"
if ue_version.split(".")[0] == "4":
u_build_tool_path /= "UnrealBuildTool.exe"
elif ue_version.split(".")[0] == "5":
u_build_tool_path /= "UnrealBuildTool/UnrealBuildTool.exe"
u_build_tool = Path(u_build_tool_path)
u_header_tool = None
arch = "Win64"

View file

@ -22,17 +22,24 @@ class CreateRender(Creator):
ar = unreal.AssetRegistryHelpers.get_asset_registry()
# The asset name is the the third element of the path which contains
# the map.
# The index of the split path is 3 because the first element is an
# empty string, as the path begins with "/Content".
a = unreal.EditorUtilityLibrary.get_selected_assets()[0]
asset_name = a.get_path_name().split("/")[3]
# Get the master sequence and the master level.
# There should be only one sequence and one level in the directory.
filter = unreal.ARFilter(
class_names=["LevelSequence"],
package_paths=[f"/Game/OpenPype/{self.data['asset']}"],
package_paths=[f"/Game/OpenPype/{asset_name}"],
recursive_paths=False)
sequences = ar.get_assets(filter)
ms = sequences[0].get_editor_property('object_path')
filter = unreal.ARFilter(
class_names=["World"],
package_paths=[f"/Game/OpenPype/{self.data['asset']}"],
package_paths=[f"/Game/OpenPype/{asset_name}"],
recursive_paths=False)
levels = ar.get_assets(filter)
ml = levels[0].get_editor_property('object_path')

View file

@ -14,6 +14,7 @@ from openpype.pipeline import (
)
from openpype.hosts.unreal.api import plugin
from openpype.hosts.unreal.api import pipeline as unreal_pipeline
from openpype.api import get_asset
class AnimationFBXLoader(plugin.Loader):
@ -77,13 +78,15 @@ class AnimationFBXLoader(plugin.Loader):
task.options.anim_sequence_import_data.set_editor_property(
'import_meshes_in_bone_hierarchy', False)
task.options.anim_sequence_import_data.set_editor_property(
'use_default_sample_rate', True)
'use_default_sample_rate', False)
task.options.anim_sequence_import_data.set_editor_property(
'custom_sample_rate', get_asset()["data"].get("fps"))
task.options.anim_sequence_import_data.set_editor_property(
'import_custom_attribute', True)
task.options.anim_sequence_import_data.set_editor_property(
'import_bone_tracks', True)
task.options.anim_sequence_import_data.set_editor_property(
'remove_redundant_keys', True)
'remove_redundant_keys', False)
task.options.anim_sequence_import_data.set_editor_property(
'convert_scene', True)
@ -139,22 +142,18 @@ class AnimationFBXLoader(plugin.Loader):
root = "/Game/OpenPype"
asset = context.get('asset').get('name')
suffix = "_CON"
if asset:
asset_name = "{}_{}".format(asset, name)
else:
asset_name = "{}".format(name)
asset_name = f"{asset}_{name}" if asset else f"{name}"
tools = unreal.AssetToolsHelpers().get_asset_tools()
asset_dir, container_name = tools.create_unique_asset_name(
f"{root}/Animations/{asset}/{name}", suffix="")
ar = unreal.AssetRegistryHelpers.get_asset_registry()
filter = unreal.ARFilter(
_filter = unreal.ARFilter(
class_names=["World"],
package_paths=[f"{root}/{hierarchy[0]}"],
recursive_paths=False)
levels = ar.get_assets(filter)
levels = ar.get_assets(_filter)
master_level = levels[0].get_editor_property('object_path')
hierarchy_dir = root
@ -162,11 +161,11 @@ class AnimationFBXLoader(plugin.Loader):
hierarchy_dir = f"{hierarchy_dir}/{h}"
hierarchy_dir = f"{hierarchy_dir}/{asset}"
filter = unreal.ARFilter(
_filter = unreal.ARFilter(
class_names=["World"],
package_paths=[f"{hierarchy_dir}/"],
recursive_paths=True)
levels = ar.get_assets(filter)
levels = ar.get_assets(_filter)
level = levels[0].get_editor_property('object_path')
unreal.EditorLevelLibrary.save_all_dirty_levels()
@ -233,8 +232,7 @@ class AnimationFBXLoader(plugin.Loader):
"parent": context["representation"]["parent"],
"family": context["representation"]["context"]["family"]
}
unreal_pipeline.imprint(
"{}/{}".format(asset_dir, container_name), data)
unreal_pipeline.imprint(f"{asset_dir}/{container_name}", data)
imported_content = EditorAssetLibrary.list_assets(
asset_dir, recursive=True, include_folder=False)
@ -279,13 +277,15 @@ class AnimationFBXLoader(plugin.Loader):
task.options.anim_sequence_import_data.set_editor_property(
'import_meshes_in_bone_hierarchy', False)
task.options.anim_sequence_import_data.set_editor_property(
'use_default_sample_rate', True)
'use_default_sample_rate', False)
task.options.anim_sequence_import_data.set_editor_property(
'custom_sample_rate', get_asset()["data"].get("fps"))
task.options.anim_sequence_import_data.set_editor_property(
'import_custom_attribute', True)
task.options.anim_sequence_import_data.set_editor_property(
'import_bone_tracks', True)
task.options.anim_sequence_import_data.set_editor_property(
'remove_redundant_keys', True)
'remove_redundant_keys', False)
task.options.anim_sequence_import_data.set_editor_property(
'convert_scene', True)
@ -296,8 +296,7 @@ class AnimationFBXLoader(plugin.Loader):
# do import fbx and replace existing data
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
container_path = "{}/{}".format(container["namespace"],
container["objectName"])
container_path = f'{container["namespace"]}/{container["objectName"]}'
# update metadata
unreal_pipeline.imprint(
container_path,

View file

@ -5,6 +5,7 @@ from pathlib import Path
import unreal
from unreal import EditorAssetLibrary
from unreal import EditorLevelLibrary
from unreal import EditorLevelUtils
from openpype.pipeline import (
AVALON_CONTAINER_ID,
@ -57,6 +58,33 @@ class CameraLoader(plugin.Loader):
min_frame_j,
max_frame_j + 1)
def _import_camera(
self, world, sequence, bindings, import_fbx_settings, import_filename
):
ue_version = unreal.SystemLibrary.get_engine_version().split('.')
ue_major = int(ue_version[0])
ue_minor = int(ue_version[1])
if ue_major == 4 and ue_minor <= 26:
unreal.SequencerTools.import_fbx(
world,
sequence,
bindings,
import_fbx_settings,
import_filename
)
elif (ue_major == 4 and ue_minor >= 27) or ue_major == 5:
unreal.SequencerTools.import_level_sequence_fbx(
world,
sequence,
bindings,
import_fbx_settings,
import_filename
)
else:
raise NotImplementedError(
f"Unreal version {ue_major} not supported")
def load(self, context, name, namespace, data):
"""
Load and containerise representation into Content Browser.
@ -84,10 +112,10 @@ class CameraLoader(plugin.Loader):
hierarchy = context.get('asset').get('data').get('parents')
root = "/Game/OpenPype"
hierarchy_dir = root
hierarchy_list = []
hierarchy_dir_list = []
for h in hierarchy:
hierarchy_dir = f"{hierarchy_dir}/{h}"
hierarchy_list.append(hierarchy_dir)
hierarchy_dir_list.append(hierarchy_dir)
asset = context.get('asset').get('name')
suffix = "_CON"
if asset:
@ -121,27 +149,40 @@ class CameraLoader(plugin.Loader):
asset_dir, container_name = tools.create_unique_asset_name(
f"{hierarchy_dir}/{asset}/{name}_{unique_number:02d}", suffix="")
asset_path = Path(asset_dir)
asset_path_parent = str(asset_path.parent.as_posix())
container_name += suffix
current_level = EditorLevelLibrary.get_editor_world().get_full_name()
EditorAssetLibrary.make_directory(asset_dir)
# Create map for the shot, and create hierarchy of map. If the maps
# already exist, we will use them.
h_dir = hierarchy_dir_list[0]
h_asset = hierarchy[0]
master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map"
if not EditorAssetLibrary.does_asset_exist(master_level):
EditorLevelLibrary.new_level(f"{h_dir}/{h_asset}_map")
level = f"{asset_path_parent}/{asset}_map.{asset}_map"
if not EditorAssetLibrary.does_asset_exist(level):
EditorLevelLibrary.new_level(f"{asset_path_parent}/{asset}_map")
EditorLevelLibrary.load_level(master_level)
EditorLevelUtils.add_level_to_world(
EditorLevelLibrary.get_editor_world(),
level,
unreal.LevelStreamingDynamic
)
EditorLevelLibrary.save_all_dirty_levels()
ar = unreal.AssetRegistryHelpers.get_asset_registry()
filter = unreal.ARFilter(
class_names=["World"],
package_paths=[f"{hierarchy_dir}/{asset}/"],
recursive_paths=True)
maps = ar.get_assets(filter)
# There should be only one map in the list
EditorLevelLibrary.load_level(maps[0].get_full_name())
EditorLevelLibrary.load_level(level)
# Get all the sequences in the hierarchy. It will create them, if
# they don't exist.
sequences = []
frame_ranges = []
i = 0
for h in hierarchy_list:
for h in hierarchy_dir_list:
root_content = EditorAssetLibrary.list_assets(
h, recursive=False, include_folder=False)
@ -228,7 +269,7 @@ class CameraLoader(plugin.Loader):
settings.set_editor_property('reduce_keys', False)
if cam_seq:
unreal.SequencerTools.import_fbx(
self._import_camera(
EditorLevelLibrary.get_editor_world(),
cam_seq,
cam_seq.get_bindings(),
@ -256,7 +297,7 @@ class CameraLoader(plugin.Loader):
"{}/{}".format(asset_dir, container_name), data)
EditorLevelLibrary.save_all_dirty_levels()
EditorLevelLibrary.load_level(current_level)
EditorLevelLibrary.load_level(master_level)
asset_content = EditorAssetLibrary.list_assets(
asset_dir, recursive=True, include_folder=True
@ -388,7 +429,7 @@ class CameraLoader(plugin.Loader):
sub_scene.set_sequence(new_sequence)
unreal.SequencerTools.import_fbx(
self._import_camera(
EditorLevelLibrary.get_editor_world(),
new_sequence,
new_sequence.get_bindings(),

View file

@ -20,6 +20,7 @@ from openpype.pipeline import (
AVALON_CONTAINER_ID,
legacy_io,
)
from openpype.api import get_asset
from openpype.hosts.unreal.api import plugin
from openpype.hosts.unreal.api import pipeline as unreal_pipeline
@ -87,7 +88,8 @@ class LayoutLoader(plugin.Loader):
return None
def _get_data(self, asset_name):
@staticmethod
def _get_data(asset_name):
asset_doc = legacy_io.find_one({
"type": "asset",
"name": asset_name
@ -95,8 +97,9 @@ class LayoutLoader(plugin.Loader):
return asset_doc.get("data")
@staticmethod
def _set_sequence_hierarchy(
self, seq_i, seq_j, max_frame_i, min_frame_j, max_frame_j, map_paths
seq_i, seq_j, max_frame_i, min_frame_j, max_frame_j, map_paths
):
# Get existing sequencer tracks or create them if they don't exist
tracks = seq_i.get_master_tracks()
@ -165,8 +168,9 @@ class LayoutLoader(plugin.Loader):
hid_section.set_row_index(index)
hid_section.set_level_names(maps)
@staticmethod
def _process_family(
self, assets, class_name, transform, sequence, inst_name=None
assets, class_name, transform, sequence, inst_name=None
):
ar = unreal.AssetRegistryHelpers.get_asset_registry()
@ -262,13 +266,15 @@ class LayoutLoader(plugin.Loader):
task.options.anim_sequence_import_data.set_editor_property(
'import_meshes_in_bone_hierarchy', False)
task.options.anim_sequence_import_data.set_editor_property(
'use_default_sample_rate', True)
'use_default_sample_rate', False)
task.options.anim_sequence_import_data.set_editor_property(
'custom_sample_rate', get_asset()["data"].get("fps"))
task.options.anim_sequence_import_data.set_editor_property(
'import_custom_attribute', True)
task.options.anim_sequence_import_data.set_editor_property(
'import_bone_tracks', True)
task.options.anim_sequence_import_data.set_editor_property(
'remove_redundant_keys', True)
'remove_redundant_keys', False)
task.options.anim_sequence_import_data.set_editor_property(
'convert_scene', True)
@ -311,11 +317,8 @@ class LayoutLoader(plugin.Loader):
for binding in bindings:
tracks = binding.get_tracks()
track = None
if not tracks:
track = binding.add_track(
unreal.MovieSceneSkeletalAnimationTrack)
else:
track = tracks[0]
track = tracks[0] if tracks else binding.add_track(
unreal.MovieSceneSkeletalAnimationTrack)
sections = track.get_sections()
section = None
@ -335,11 +338,11 @@ class LayoutLoader(plugin.Loader):
curr_anim.get_path_name()).parent
).replace('\\', '/')
filter = unreal.ARFilter(
_filter = unreal.ARFilter(
class_names=["AssetContainer"],
package_paths=[anim_path],
recursive_paths=False)
containers = ar.get_assets(filter)
containers = ar.get_assets(_filter)
if len(containers) > 0:
return
@ -350,6 +353,7 @@ class LayoutLoader(plugin.Loader):
sec_params = section.get_editor_property('params')
sec_params.set_editor_property('animation', animation)
@staticmethod
def _generate_sequence(self, h, h_dir):
tools = unreal.AssetToolsHelpers().get_asset_tools()
@ -583,10 +587,7 @@ class LayoutLoader(plugin.Loader):
hierarchy_dir_list.append(hierarchy_dir)
asset = context.get('asset').get('name')
suffix = "_CON"
if asset:
asset_name = "{}_{}".format(asset, name)
else:
asset_name = "{}".format(name)
asset_name = f"{asset}_{name}" if asset else name
tools = unreal.AssetToolsHelpers().get_asset_tools()
asset_dir, container_name = tools.create_unique_asset_name(
@ -800,7 +801,7 @@ class LayoutLoader(plugin.Loader):
lc for lc in layout_containers
if asset in lc.get('loaded_assets')]
if len(layouts) == 0:
if not layouts:
EditorAssetLibrary.delete_directory(str(Path(asset).parent))
# Remove the Level Sequence from the parent.
@ -810,17 +811,17 @@ class LayoutLoader(plugin.Loader):
namespace = container.get('namespace').replace(f"{root}/", "")
ms_asset = namespace.split('/')[0]
ar = unreal.AssetRegistryHelpers.get_asset_registry()
filter = unreal.ARFilter(
_filter = unreal.ARFilter(
class_names=["LevelSequence"],
package_paths=[f"{root}/{ms_asset}"],
recursive_paths=False)
sequences = ar.get_assets(filter)
sequences = ar.get_assets(_filter)
master_sequence = sequences[0].get_asset()
filter = unreal.ARFilter(
_filter = unreal.ARFilter(
class_names=["World"],
package_paths=[f"{root}/{ms_asset}"],
recursive_paths=False)
levels = ar.get_assets(filter)
levels = ar.get_assets(_filter)
master_level = levels[0].get_editor_property('object_path')
sequences = [master_sequence]

View file

@ -168,7 +168,7 @@ def make_sequence_collection(path, otio_range, metadata):
first, last = otio_range_to_frame_range(otio_range)
collection = clique.Collection(
head=head, tail=tail, padding=metadata["padding"])
collection.indexes.update(list(range(first, last)))
collection.indexes.update([i for i in range(first, last)])
return dir_path, collection

View file

@ -143,14 +143,17 @@ def create_chunks(iterable, chunk_size=None):
list<list>: Chunked items.
"""
chunks = []
if not iterable:
return chunks
tupled_iterable = tuple(iterable)
if not tupled_iterable:
return chunks
iterable_size = len(tupled_iterable)
if chunk_size is None:
chunk_size = 200
if chunk_size < 1:
chunk_size = 1
for idx in range(0, iterable_size, chunk_size):
chunks.append(tupled_iterable[idx:idx + chunk_size])
return chunks

View file

@ -92,14 +92,18 @@ def check_credentials(username, api_key, ftrack_server=None):
if not ftrack_server or not username or not api_key:
return False
user_exists = False
try:
session = ftrack_api.Session(
server_url=ftrack_server,
api_key=api_key,
api_user=username
)
# Validated that the username actually exists
user = session.query("User where username is \"{}\"".format(username))
user_exists = user is not None
session.close()
except Exception:
return False
return True
pass
return user_exists

View file

@ -165,6 +165,9 @@
"CollectMayaRender": {
"sync_workfile_version": false
},
"CollectFbxCamera": {
"enabled": false
},
"ValidateInstanceInContext": {
"enabled": true,
"optional": true,

View file

@ -15,6 +15,18 @@
"destination-path": []
}
},
"scriptsmenu": {
"name": "OpenPype Tools",
"definition": [
{
"type": "action",
"sourcetype": "python",
"title": "OpenPype Docs",
"command": "import webbrowser;webbrowser.open(url='https://openpype.io/docs/artist_hosts_nuke_tut')",
"tooltip": "Open the OpenPype Nuke user doc page"
}
]
},
"create": {
"CreateWriteRender": {
"fpath_template": "{work}/renders/nuke/{subset}/{subset}.{frame}.{ext}",

View file

@ -1243,9 +1243,19 @@
"host_name": "unreal",
"environment": {},
"variants": {
"4-26": {
"4-27": {
"use_python_2": false,
"environment": {}
},
"5-0": {
"use_python_2": false,
"environment": {
"UE_PYTHONPATH": "{PYTHONPATH}"
}
},
"__dynamic_keys_labels__": {
"4-27": "4.27",
"5-0": "5.0"
}
}
},

View file

@ -49,7 +49,7 @@
},
{
"type": "schema",
"name": "schema_maya_scriptsmenu"
"name": "schema_scriptsmenu"
},
{
"type": "schema",

View file

@ -79,6 +79,10 @@
}
]
},
{
"type": "schema",
"name": "schema_scriptsmenu"
},
{
"type": "dict",
"collapsible": true,

View file

@ -21,6 +21,20 @@
}
]
},
{
"type": "dict",
"collapsible": true,
"key": "CollectFbxCamera",
"label": "Collect Camera for FBX export",
"checkbox_key": "enabled",
"children": [
{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
}
]
},
{
"type": "splitter"
},

View file

@ -264,8 +264,6 @@ class HierarchyModel(QtCore.QAbstractItemModel):
if not project_doc:
return
self.blockSignals(True)
# Create project item
project_item = ProjectItem(project_doc)
self.add_item(project_item)
@ -379,8 +377,6 @@ class HierarchyModel(QtCore.QAbstractItemModel):
self.add_items(task_items, asset_item)
self.blockSignals(False)
# Emit that project was successfully changed
self.project_changed.emit()
@ -1476,12 +1472,7 @@ class HierarchyModel(QtCore.QAbstractItemModel):
mimedata.setData("application/copy_task", encoded_data)
return mimedata
def paste_mime_data(self, index, mime_data):
if not index.isValid():
return
item_id = index.data(IDENTIFIER_ROLE)
item = self._items_by_id[item_id]
def _paste_mime_data(self, item, mime_data):
if not isinstance(item, (AssetItem, TaskItem)):
return
@ -1515,6 +1506,25 @@ class HierarchyModel(QtCore.QAbstractItemModel):
task_item = TaskItem(task_data, True)
self.add_item(task_item, parent)
def paste(self, indexes, mime_data):
# Get the selected Assets uniquely
items = set()
for index in indexes:
if not index.isValid():
return
item_id = index.data(IDENTIFIER_ROLE)
item = self._items_by_id[item_id]
# Do not copy into the Task Item so get parent Asset instead
if isinstance(item, TaskItem):
item = item.parent()
items.add(item)
for item in items:
self._paste_mime_data(item, mime_data)
class BaseItem:
"""Base item for HierarchyModel.

View file

@ -139,6 +139,7 @@ class HierarchyView(QtWidgets.QTreeView):
self.setAlternatingRowColors(True)
self.setSelectionMode(HierarchyView.ExtendedSelection)
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.setEditTriggers(HierarchyView.AllEditTriggers)
column_delegates = {}
column_key_to_index = {}
@ -301,16 +302,6 @@ class HierarchyView(QtWidgets.QTreeView):
def rowsInserted(self, parent_index, start, end):
super(HierarchyView, self).rowsInserted(parent_index, start, end)
for row in range(start, end + 1):
for key, column in self._column_key_to_index.items():
if key not in self.persistent_columns:
continue
col_index = self._source_model.index(row, column, parent_index)
if bool(
self._source_model.flags(col_index)
& QtCore.Qt.ItemIsEditable
):
self.openPersistentEditor(col_index)
# Expand parent on insert
if not self.isExpanded(parent_index):
@ -365,20 +356,24 @@ class HierarchyView(QtWidgets.QTreeView):
event.accept()
def _copy_items(self, indexes=None):
clipboard = QtWidgets.QApplication.clipboard()
try:
if indexes is None:
indexes = self.selectedIndexes()
mime_data = self._source_model.copy_mime_data(indexes)
QtWidgets.QApplication.clipboard().setMimeData(mime_data)
clipboard.setMimeData(mime_data)
self._show_message("Tasks copied")
except ValueError as exc:
# Change clipboard to contain empty data
empty_mime_data = QtCore.QMimeData()
clipboard.setMimeData(empty_mime_data)
self._show_message(str(exc))
def _paste_items(self):
index = self.currentIndex()
mime_data = QtWidgets.QApplication.clipboard().mimeData()
self._source_model.paste_mime_data(index, mime_data)
rows = self.selectionModel().selectedRows()
self._source_model.paste(rows, mime_data)
def _delete_items(self, indexes=None):
if indexes is None:

View file

@ -184,14 +184,14 @@ class ProjectManagerWindow(QtWidgets.QWidget):
self.resize(1200, 600)
self.setStyleSheet(load_stylesheet())
def _set_project(self, project_name=None):
def _set_project(self, project_name=None, force=False):
self._create_folders_btn.setEnabled(project_name is not None)
self._remove_projects_btn.setEnabled(project_name is not None)
self._add_asset_btn.setEnabled(project_name is not None)
self._add_task_btn.setEnabled(project_name is not None)
self._save_btn.setEnabled(project_name is not None)
self._project_proxy_model.set_filter_default(project_name is not None)
self.hierarchy_view.set_project(project_name, True)
self.hierarchy_view.set_project(project_name, force)
def _current_project(self):
row = self._project_combobox.currentIndex()
@ -229,11 +229,11 @@ class ProjectManagerWindow(QtWidgets.QWidget):
self._project_combobox.setCurrentIndex(row)
selected_project = self._current_project()
self._set_project(selected_project)
self._set_project(selected_project, True)
def _on_project_change(self):
selected_project = self._current_project()
self._set_project(selected_project)
self._set_project(selected_project, False)
def _on_project_refresh(self):
self.refresh_projects()

View file

@ -494,8 +494,6 @@ class AssetModel(QtGui.QStandardItemModel):
# Remove cache of removed items
for asset_id in removed_asset_ids:
self._items_by_asset_id.pop(asset_id)
if asset_id in self._items_with_color_by_id:
self._items_with_color_by_id.pop(asset_id)
# Refresh data
# - all items refresh all data except id

View file

@ -119,7 +119,8 @@ module.{module_name}()"""
"""
# get the current application and its linked keyboard modifiers
modifiers = QtWidgets.QApplication.keyboardModifiers()
app = QtWidgets.QApplication.instance()
modifiers = app.keyboardModifiers()
# If the menu has a callback registered for the current modifier
# we run the callback instead of the action itself.

View file

@ -8,7 +8,7 @@ def _nuke_main_window():
if (obj.inherits('QMainWindow') and
obj.metaObject().className() == 'Foundry::UI::DockMainWindow'):
return obj
raise RuntimeError('Could not find Nuke MainWindow instance')
raise RuntimeError('Could not find Nuke MainWindow instance')
def _nuke_main_menubar():
@ -22,9 +22,6 @@ def _nuke_main_menubar():
def main(title="Scripts"):
# Register control + shift callback to add to shelf (Nuke behavior)
# modifiers = QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier
# menu.register_callback(modifiers, to_shelf)
nuke_main_bar = _nuke_main_menubar()
for nuke_bar in nuke_main_bar.children():
if isinstance(nuke_bar, scriptsmenu.ScriptsMenu):
@ -33,4 +30,4 @@ def main(title="Scripts"):
return menu
menu = scriptsmenu.ScriptsMenu(title=title, parent=nuke_main_bar)
return menu
return menu

View file

@ -264,8 +264,7 @@ class ScriptsMenu(QtWidgets.QMenu):
action.setVisible(True)
else:
for action in self._script_actions:
if not action.has_tag(search.lower()):
action.setVisible(False)
action.setVisible(action.has_tag(search.lower()))
# Set visibility for all submenus
for action in self.actions():

View file

@ -1,6 +1,6 @@
VERSION_MAJOR = 1
VERSION_MINOR = 5
VERSION_PATCH = 1
VERSION_PATCH = 2
version = '{}.{}.{}'.format(VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH)

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring Pype version."""
__version__ = "3.10.0-nightly.3"
__version__ = "3.10.1-nightly.1"

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "OpenPype"
version = "3.10.0-nightly.3" # OpenPype
version = "3.10.1-nightly.1" # OpenPype
description = "Open VFX and Animation pipeline with support."
authors = ["OpenPype Team <info@openpype.io>"]
license = "MIT License"

View file

@ -0,0 +1,14 @@
---
id: admin_hosts_nuke
title: Nuke
sidebar_label: Nuke
---
## Custom Menu
You can add your custom tools menu into Nuke by extending definitions in **Nuke -> Scripts Menu Definition**.
![Custom menu definition](assets/nuke-admin_scriptsmenu.png)
:::note Work in progress
This is still work in progress. Menu definition will be handled more friendly with widgets and not
raw json.
:::

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

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