Merge branch 'develop' into OP-3172_More-control-over-thumbnail-processing

This commit is contained in:
Allan Ihsan 2022-05-30 19:22:20 +03:00
commit eef1e09cd3
80 changed files with 1554 additions and 378 deletions

View file

@ -1,8 +1,24 @@
# Changelog
## [3.10.1-nightly.1](https://github.com/pypeclub/OpenPype/tree/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/3.9.8...3.10.0)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.10.0-nightly.6...3.10.0)
**🆕 New features**
@ -31,7 +47,6 @@
- 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)
- Unreal: Layout and Camera update and remove functions reimplemented and improvements [\#3116](https://github.com/pypeclub/OpenPype/pull/3116)
**🐛 Bug fixes**
@ -131,10 +146,6 @@
- TVPaint: Composite layers in reversed order [\#3134](https://github.com/pypeclub/OpenPype/pull/3134)
**Merged pull requests:**
- Ftrack: AssetVersion status on publish [\#3114](https://github.com/pypeclub/OpenPype/pull/3114)
## [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)

View file

@ -3,6 +3,7 @@ import os
import re
import json
import pickle
import clique
import tempfile
import itertools
import contextlib
@ -560,7 +561,7 @@ def get_segment_attributes(segment):
if not hasattr(segment, attr_name):
continue
attr = getattr(segment, attr_name)
segment_attrs_data[attr] = str(attr).replace("+", ":")
segment_attrs_data[attr_name] = str(attr).replace("+", ":")
if attr_name in ["record_in", "record_out"]:
clip_data[attr_name] = attr.relative_frame
@ -762,6 +763,7 @@ class MediaInfoFile(object):
_start_frame = None
_fps = None
_drop_mode = None
_file_pattern = None
def __init__(self, path, **kwargs):
@ -773,17 +775,28 @@ class MediaInfoFile(object):
self._validate_media_script_path()
# derivate other feed variables
self.feed_basename = os.path.basename(path)
self.feed_dir = os.path.dirname(path)
self.feed_ext = os.path.splitext(self.feed_basename)[1][1:].lower()
feed_basename = os.path.basename(path)
feed_dir = os.path.dirname(path)
feed_ext = os.path.splitext(feed_basename)[1][1:].lower()
with maintained_temp_file_path(".clip") as tmp_path:
self.log.info("Temp File: {}".format(tmp_path))
self._generate_media_info_file(tmp_path)
self._generate_media_info_file(tmp_path, feed_ext, feed_dir)
# get collection containing feed_basename from path
self.file_pattern = self._get_collection(
feed_basename, feed_dir, feed_ext)
if (
not self.file_pattern
and os.path.exists(os.path.join(feed_dir, feed_basename))
):
self.file_pattern = feed_basename
# get clip data and make them single if there is multiple
# clips data
xml_data = self._make_single_clip_media_info(tmp_path)
xml_data = self._make_single_clip_media_info(
tmp_path, feed_basename, self.file_pattern)
self.log.debug("xml_data: {}".format(xml_data))
self.log.debug("type: {}".format(type(xml_data)))
@ -794,6 +807,123 @@ class MediaInfoFile(object):
self.log.debug("drop frame: {}".format(self.drop_mode))
self.clip_data = xml_data
def _get_collection(self, feed_basename, feed_dir, feed_ext):
""" Get collection string
Args:
feed_basename (str): file base name
feed_dir (str): file's directory
feed_ext (str): file extension
Raises:
AttributeError: feed_ext is not matching feed_basename
Returns:
str: collection basename with range of sequence
"""
partialname = self._separate_file_head(feed_basename, feed_ext)
self.log.debug("__ partialname: {}".format(partialname))
# make sure partial input basename is having correct extensoon
if not partialname:
raise AttributeError(
"Wrong input attributes. Basename - {}, Ext - {}".format(
feed_basename, feed_ext
)
)
# get all related files
files = [
f for f in os.listdir(feed_dir)
if partialname == self._separate_file_head(f, feed_ext)
]
# ignore reminders as we dont need them
collections = clique.assemble(files)[0]
# in case no collection found return None
# it is probably just single file
if not collections:
return
# we expect only one collection
collection = collections[0]
self.log.debug("__ collection: {}".format(collection))
if collection.is_contiguous():
return self._format_collection(collection)
# add `[` in front to make sure it want capture
# shot name with the same number
number_from_path = self._separate_number(feed_basename, feed_ext)
search_number_pattern = "[" + number_from_path
# convert to multiple collections
_continues_colls = collection.separate()
for _coll in _continues_colls:
coll_to_text = self._format_collection(
_coll, len(number_from_path))
self.log.debug("__ coll_to_text: {}".format(coll_to_text))
if search_number_pattern in coll_to_text:
return coll_to_text
@staticmethod
def _format_collection(collection, padding=None):
padding = padding or collection.padding
# if no holes then return collection
head = collection.format("{head}")
tail = collection.format("{tail}")
range_template = "[{{:0{0}d}}-{{:0{0}d}}]".format(
padding)
ranges = range_template.format(
min(collection.indexes),
max(collection.indexes)
)
# if no holes then return collection
return "{}{}{}".format(head, ranges, tail)
def _separate_file_head(self, basename, extension):
""" Get only head with out sequence and extension
Args:
basename (str): file base name
extension (str): file extension
Returns:
str: file head
"""
# in case sequence file
found = re.findall(
r"(.*)[._][\d]*(?=.{})".format(extension),
basename,
)
if found:
return found.pop()
# in case single file
name, ext = os.path.splitext(basename)
if extension == ext[1:]:
return name
def _separate_number(self, basename, extension):
""" Get only sequence number as string
Args:
basename (str): file base name
extension (str): file extension
Returns:
str: number with padding
"""
# in case sequence file
found = re.findall(
r"[._]([\d]*)(?=.{})".format(extension),
basename,
)
if found:
return found.pop()
@property
def clip_data(self):
"""Clip's xml clip data
@ -846,18 +976,41 @@ class MediaInfoFile(object):
def drop_mode(self, text):
self._drop_mode = str(text)
@property
def file_pattern(self):
"""Clips file patter
Returns:
str: file pattern. ex. file.[1-2].exr
"""
return self._file_pattern
@file_pattern.setter
def file_pattern(self, fpattern):
self._file_pattern = fpattern
def _validate_media_script_path(self):
if not os.path.isfile(self.MEDIA_SCRIPT_PATH):
raise IOError("Media Scirpt does not exist: `{}`".format(
self.MEDIA_SCRIPT_PATH))
def _generate_media_info_file(self, fpath):
def _generate_media_info_file(self, fpath, feed_ext, feed_dir):
""" Generate media info xml .clip file
Args:
fpath (str): .clip file path
feed_ext (str): file extension to be filtered
feed_dir (str): look up directory
Raises:
TypeError: Type error if it fails
"""
# Create cmd arguments for gettig xml file info file
cmd_args = [
self.MEDIA_SCRIPT_PATH,
"-e", self.feed_ext,
"-e", feed_ext,
"-o", fpath,
self.feed_dir
feed_dir
]
try:
@ -867,7 +1020,20 @@ class MediaInfoFile(object):
raise TypeError(
"Error creating `{}` due: {}".format(fpath, error))
def _make_single_clip_media_info(self, fpath):
def _make_single_clip_media_info(self, fpath, feed_basename, path_pattern):
""" Separate only relative clip object form .clip file
Args:
fpath (str): clip file path
feed_basename (str): search basename
path_pattern (str): search file pattern (file.[1-2].exr)
Raises:
ET.ParseError: if nothing found
Returns:
ET.Element: xml element data of matching clip
"""
with open(fpath) as f:
lines = f.readlines()
_added_root = itertools.chain(
@ -878,14 +1044,30 @@ class MediaInfoFile(object):
xml_clips = new_root.findall("clip")
matching_clip = None
for xml_clip in xml_clips:
if xml_clip.find("name").text in self.feed_basename:
matching_clip = xml_clip
clip_name = xml_clip.find("name").text
self.log.debug("__ clip_name: `{}`".format(clip_name))
if clip_name not in feed_basename:
continue
# test path pattern
for out_track in xml_clip.iter("track"):
for out_feed in out_track.iter("feed"):
for span in out_feed.iter("span"):
# start frame
span_path = span.find("path")
self.log.debug(
"__ span_path.text: {}, path_pattern: {}".format(
span_path.text, path_pattern
)
)
if path_pattern in span_path.text:
matching_clip = xml_clip
if matching_clip is None:
# return warning there is missing clip
raise ET.ParseError(
"Missing clip in `{}`. Available clips {}".format(
self.feed_basename, [
feed_basename, [
xml_clip.find("name").text
for xml_clip in xml_clips
]
@ -894,6 +1076,11 @@ class MediaInfoFile(object):
return matching_clip
def _get_time_info_from_origin(self, xml_data):
"""Set time info to class attributes
Args:
xml_data (ET.Element): clip data
"""
try:
for out_track in xml_data.iter('track'):
for out_feed in out_track.iter('feed'):
@ -912,8 +1099,6 @@ class MediaInfoFile(object):
'startTimecode/dropMode')
self.drop_mode = out_feed_drop_mode_obj.text
break
else:
continue
except Exception as msg:
self.log.warning(msg)

View file

@ -360,6 +360,7 @@ class PublishableClip:
driving_layer_default = ""
index_from_segment_default = False
use_shot_name_default = False
include_handles_default = False
def __init__(self, segment, **kwargs):
self.rename_index = kwargs["rename_index"]
@ -493,6 +494,8 @@ class PublishableClip:
"reviewTrack", {}).get("value") or self.review_track_default
self.audio = self.ui_inputs.get(
"audio", {}).get("value") or False
self.include_handles = self.ui_inputs.get(
"includeHandles", {}).get("value") or self.include_handles_default
# build subset name from layer name
if self.subset_name == "[ track name ]":

View file

@ -1,5 +1,8 @@
import os
from xml.etree import ElementTree as ET
from openpype.api import Logger
log = Logger.get_logger(__name__)
def export_clip(export_path, clip, preset_path, **kwargs):
@ -143,10 +146,40 @@ def modify_preset_file(xml_path, staging_dir, data):
# change xml following data keys
with open(xml_path, "r") as datafile:
tree = ET.parse(datafile)
_root = ET.parse(datafile)
for key, value in data.items():
for element in tree.findall(".//{}".format(key)):
element.text = str(value)
tree.write(temp_path)
try:
if "/" in key:
if not key.startswith("./"):
key = ".//" + key
split_key_path = key.split("/")
element_key = split_key_path[-1]
parent_obj_path = "/".join(split_key_path[:-1])
parent_obj = _root.find(parent_obj_path)
element_obj = parent_obj.find(element_key)
if not element_obj:
append_element(parent_obj, element_key, value)
else:
finds = _root.findall(".//{}".format(key))
if not finds:
raise AttributeError
for element in finds:
element.text = str(value)
except AttributeError:
log.warning(
"Cannot create attribute: {}: {}. Skipping".format(
key, value
))
_root.write(temp_path)
return temp_path
def append_element(root_element_obj, key, value):
new_element_obj = ET.Element(key)
log.debug("__ new_element_obj: {}".format(new_element_obj))
new_element_obj.text = str(value)
root_element_obj.insert(0, new_element_obj)

View file

@ -94,83 +94,30 @@ def create_otio_time_range(start_frame, frame_duration, fps):
def _get_metadata(item):
if hasattr(item, 'metadata'):
if not item.metadata:
return {}
return {key: value for key, value in dict(item.metadata)}
return dict(item.metadata) if item.metadata else {}
return {}
def create_time_effects(otio_clip, item):
# todo #2426: add retiming effects to export
# get all subtrack items
# subTrackItems = flatten(track_item.parent().subTrackItems())
# speed = track_item.playbackSpeed()
def create_time_effects(otio_clip, speed):
otio_effect = None
# otio_effect = None
# # retime on track item
# if speed != 1.:
# # make effect
# otio_effect = otio.schema.LinearTimeWarp()
# otio_effect.name = "Speed"
# otio_effect.time_scalar = speed
# otio_effect.metadata = {}
# retime on track item
if speed != 1.:
# make effect
otio_effect = otio.schema.LinearTimeWarp()
otio_effect.name = "Speed"
otio_effect.time_scalar = speed
otio_effect.metadata = {}
# # freeze frame effect
# if speed == 0.:
# otio_effect = otio.schema.FreezeFrame()
# otio_effect.name = "FreezeFrame"
# otio_effect.metadata = {}
# freeze frame effect
if speed == 0.:
otio_effect = otio.schema.FreezeFrame()
otio_effect.name = "FreezeFrame"
otio_effect.metadata = {}
# if otio_effect:
# # add otio effect to clip effects
# otio_clip.effects.append(otio_effect)
# # loop through and get all Timewarps
# for effect in subTrackItems:
# if ((track_item not in effect.linkedItems())
# and (len(effect.linkedItems()) > 0)):
# continue
# # avoid all effect which are not TimeWarp and disabled
# if "TimeWarp" not in effect.name():
# continue
# if not effect.isEnabled():
# continue
# node = effect.node()
# name = node["name"].value()
# # solve effect class as effect name
# _name = effect.name()
# if "_" in _name:
# effect_name = re.sub(r"(?:_)[_0-9]+", "", _name) # more numbers
# else:
# effect_name = re.sub(r"\d+", "", _name) # one number
# metadata = {}
# # add knob to metadata
# for knob in ["lookup", "length"]:
# value = node[knob].value()
# animated = node[knob].isAnimated()
# if animated:
# value = [
# ((node[knob].getValueAt(i)) - i)
# for i in range(
# track_item.timelineIn(),
# track_item.timelineOut() + 1)
# ]
# metadata[knob] = value
# # make effect
# otio_effect = otio.schema.TimeEffect()
# otio_effect.name = name
# otio_effect.effect_name = effect_name
# otio_effect.metadata = metadata
# # add otio effect to clip effects
# otio_clip.effects.append(otio_effect)
pass
if otio_effect:
# add otio effect to clip effects
otio_clip.effects.append(otio_effect)
def _get_marker_color(flame_colour):
@ -260,6 +207,7 @@ def create_otio_markers(otio_item, item):
def create_otio_reference(clip_data, fps=None):
metadata = _get_metadata(clip_data)
duration = int(clip_data["source_duration"])
# get file info for path and start frame
frame_start = 0
@ -273,7 +221,6 @@ def create_otio_reference(clip_data, fps=None):
# get padding and other file infos
log.debug("_ path: {}".format(path))
frame_duration = clip_data["source_duration"]
otio_ex_ref_item = None
is_sequence = frame_number = utils.get_frame_from_filename(file_name)
@ -300,7 +247,7 @@ def create_otio_reference(clip_data, fps=None):
rate=fps,
available_range=create_otio_time_range(
frame_start,
frame_duration,
duration,
fps
)
)
@ -316,7 +263,7 @@ def create_otio_reference(clip_data, fps=None):
target_url=reformated_path,
available_range=create_otio_time_range(
frame_start,
frame_duration,
duration,
fps
)
)
@ -333,23 +280,50 @@ def create_otio_clip(clip_data):
segment = clip_data["PySegment"]
# calculate source in
media_info = MediaInfoFile(clip_data["fpath"])
media_info = MediaInfoFile(clip_data["fpath"], logger=log)
media_timecode_start = media_info.start_frame
media_fps = media_info.fps
# create media reference
media_reference = create_otio_reference(clip_data, media_fps)
# define first frame
first_frame = media_timecode_start or utils.get_frame_from_filename(
clip_data["fpath"]) or 0
source_in = int(clip_data["source_in"]) - int(first_frame)
_clip_source_in = int(clip_data["source_in"])
_clip_source_out = int(clip_data["source_out"])
_clip_record_duration = int(clip_data["record_duration"])
# first solve if the reverse timing
speed = 1
if clip_data["source_in"] > clip_data["source_out"]:
source_in = _clip_source_out - int(first_frame)
source_out = _clip_source_in - int(first_frame)
speed = -1
else:
source_in = _clip_source_in - int(first_frame)
source_out = _clip_source_out - int(first_frame)
source_duration = (source_out - source_in + 1)
# secondly check if any change of speed
if source_duration != _clip_record_duration:
retime_speed = float(source_duration) / float(_clip_record_duration)
log.debug("_ retime_speed: {}".format(retime_speed))
speed *= retime_speed
log.debug("_ source_in: {}".format(source_in))
log.debug("_ source_out: {}".format(source_out))
log.debug("_ speed: {}".format(speed))
log.debug("_ source_duration: {}".format(source_duration))
log.debug("_ _clip_record_duration: {}".format(_clip_record_duration))
# create media reference
media_reference = create_otio_reference(
clip_data, media_fps)
# creatae source range
source_range = create_otio_time_range(
source_in,
clip_data["record_duration"],
_clip_record_duration,
CTX.get_fps()
)
@ -363,6 +337,9 @@ def create_otio_clip(clip_data):
if MARKERS_INCLUDE:
create_otio_markers(otio_clip, segment)
if speed != 1:
create_time_effects(otio_clip, speed)
return otio_clip

View file

@ -268,6 +268,14 @@ class CreateShotClip(opfapi.Creator):
"target": "tag",
"toolTip": "Handle at end of clip", # noqa
"order": 2
},
"includeHandles": {
"value": False,
"type": "QCheckBox",
"label": "Include handles",
"target": "tag",
"toolTip": "By default handles are excluded", # noqa
"order": 3
}
}
}

View file

@ -1,8 +1,8 @@
import re
import pyblish
import openpype
import openpype.hosts.flame.api as opfapi
from openpype.hosts.flame.otio import flame_export
import openpype.lib as oplib
# # developer reload modules
from pprint import pformat
@ -36,6 +36,7 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin):
for segment in selected_segments:
# get openpype tag data
marker_data = opfapi.get_segment_data_marker(segment)
self.log.debug("__ marker_data: {}".format(
pformat(marker_data)))
@ -58,24 +59,44 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin):
clip_name = clip_data["segment_name"]
self.log.debug("clip_name: {}".format(clip_name))
# get otio clip data
otio_data = self._get_otio_clip_instance_data(clip_data) or {}
self.log.debug("__ otio_data: {}".format(pformat(otio_data)))
# get file path
file_path = clip_data["fpath"]
first_frame = opfapi.get_frame_from_filename(file_path) or 0
head, tail = self._get_head_tail(clip_data, first_frame)
head, tail = self._get_head_tail(
clip_data,
otio_data["otioClip"],
marker_data["handleStart"],
marker_data["handleEnd"]
)
# make sure value is absolute
if head != 0:
head = abs(head)
if tail != 0:
tail = abs(tail)
# solve handles length
marker_data["handleStart"] = min(
marker_data["handleStart"], abs(head))
marker_data["handleStart"], head)
marker_data["handleEnd"] = min(
marker_data["handleEnd"], abs(tail))
marker_data["handleEnd"], tail)
workfile_start = self._set_workfile_start(marker_data)
with_audio = bool(marker_data.pop("audio"))
# add marker data to instance data
inst_data = dict(marker_data.items())
# add ocio_data to instance data
inst_data.update(otio_data)
asset = marker_data["asset"]
subset = marker_data["subset"]
@ -98,6 +119,7 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin):
"families": families,
"publish": marker_data["publish"],
"fps": self.fps,
"workfileFrameStart": workfile_start,
"sourceFirstFrame": int(first_frame),
"path": file_path,
"flameAddTasks": self.add_tasks,
@ -105,13 +127,6 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin):
task["name"]: {"type": task["type"]}
for task in self.add_tasks}
})
# get otio clip data
otio_data = self._get_otio_clip_instance_data(clip_data) or {}
self.log.debug("__ otio_data: {}".format(pformat(otio_data)))
# add to instance data
inst_data.update(otio_data)
self.log.debug("__ inst_data: {}".format(pformat(inst_data)))
# add resolution
@ -145,6 +160,17 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin):
if marker_data.get("reviewTrack") is not None:
instance.data["reviewAudio"] = True
@staticmethod
def _set_workfile_start(data):
include_handles = data.get("includeHandles")
workfile_start = data["workfileFrameStart"]
handle_start = data["handleStart"]
if include_handles:
workfile_start += handle_start
return workfile_start
def _get_comment_attributes(self, segment):
comment = segment.comment.get_value()
@ -236,20 +262,24 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin):
return split_comments
def _get_head_tail(self, clip_data, first_frame):
def _get_head_tail(self, clip_data, otio_clip, handle_start, handle_end):
# calculate head and tail with forward compatibility
head = clip_data.get("segment_head")
tail = clip_data.get("segment_tail")
self.log.debug("__ head: `{}`".format(head))
self.log.debug("__ tail: `{}`".format(tail))
# HACK: it is here to serve for versions bellow 2021.1
if not head:
head = int(clip_data["source_in"]) - int(first_frame)
if not tail:
tail = int(
clip_data["source_duration"] - (
head + clip_data["record_duration"]
)
)
if not any([head, tail]):
retimed_attributes = oplib.get_media_range_with_retimes(
otio_clip, handle_start, handle_end)
self.log.debug(
">> retimed_attributes: {}".format(retimed_attributes))
# retimed head and tail
head = int(retimed_attributes["handleStart"])
tail = int(retimed_attributes["handleEnd"])
return head, tail
def _get_resolution_to_data(self, data, context):
@ -340,7 +370,7 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin):
continue
if otio_clip.name not in segment.name.get_value():
continue
if openpype.lib.is_overlapping_otio_ranges(
if oplib.is_overlapping_otio_ranges(
parent_range, timeline_range, strict=True):
# add pypedata marker to otio_clip metadata

View file

@ -39,7 +39,8 @@ class CollecTimelineOTIO(pyblish.api.ContextPlugin):
"name": subset_name,
"asset": asset_doc["name"],
"subset": subset_name,
"family": "workfile"
"family": "workfile",
"families": []
}
# create instance with workfile

View file

@ -6,6 +6,7 @@ from copy import deepcopy
import pyblish.api
import openpype.api
from openpype.hosts.flame import api as opfapi
from openpype.hosts.flame.api import MediaInfoFile
import flame
@ -33,24 +34,8 @@ class ExtractSubsetResources(openpype.api.Extractor):
"representation_add_range": False,
"representation_tags": ["thumbnail"],
"path_regex": ".*"
},
"ftrackpreview": {
"active": True,
"ext": "mov",
"xml_preset_file": "Apple iPad (1920x1080).xml",
"xml_preset_dir": "",
"export_type": "Movie",
"parsed_comment_attrs": False,
"colorspace_out": "Output - Rec.709",
"representation_add_range": True,
"representation_tags": [
"review",
"delete"
],
"path_regex": ".*"
}
}
keep_original_representation = False
# hide publisher during exporting
hide_ui_on_process = True
@ -59,11 +44,7 @@ class ExtractSubsetResources(openpype.api.Extractor):
export_presets_mapping = {}
def process(self, instance):
if (
self.keep_original_representation
and "representations" not in instance.data
or not self.keep_original_representation
):
if "representations" not in instance.data:
instance.data["representations"] = []
# flame objects
@ -91,7 +72,6 @@ class ExtractSubsetResources(openpype.api.Extractor):
handles = max(handle_start, handle_end)
# get media source range with handles
source_end_handles = instance.data["sourceEndH"]
source_start_handles = instance.data["sourceStartH"]
source_end_handles = instance.data["sourceEndH"]
@ -108,27 +88,7 @@ class ExtractSubsetResources(openpype.api.Extractor):
for unique_name, preset_config in export_presets.items():
modify_xml_data = {}
# get activating attributes
activated_preset = preset_config["active"]
filter_path_regex = preset_config.get("filter_path_regex")
self.log.info(
"Preset `{}` is active `{}` with filter `{}`".format(
unique_name, activated_preset, filter_path_regex
)
)
self.log.debug(
"__ clip_path: `{}`".format(clip_path))
# skip if not activated presete
if not activated_preset:
continue
# exclude by regex filter if any
if (
filter_path_regex
and not re.search(filter_path_regex, clip_path)
):
if self._should_skip(preset_config, clip_path, unique_name):
continue
# get all presets attributes
@ -146,20 +106,12 @@ class ExtractSubsetResources(openpype.api.Extractor):
)
)
# get attribures related loading in integrate_batch_group
load_to_batch_group = preset_config.get(
"load_to_batch_group")
batch_group_loader_name = preset_config.get(
"batch_group_loader_name")
# convert to None if empty string
if batch_group_loader_name == "":
batch_group_loader_name = None
# get frame range with handles for representation range
frame_start_handle = frame_start - handle_start
# calculate duration with handles
source_duration_handles = (
source_end_handles - source_start_handles) + 1
source_end_handles - source_start_handles)
# define in/out marks
in_mark = (source_start_handles - source_first_frame) + 1
@ -180,15 +132,15 @@ class ExtractSubsetResources(openpype.api.Extractor):
name_patern_xml = (
"<segment name>_<shot name>_{}.").format(
unique_name)
# change in/out marks to timeline in/out
in_mark = clip_in
out_mark = clip_out
else:
exporting_clip = self.import_clip(clip_path)
exporting_clip.name.set_value("{}_{}".format(
asset_name, segment_name))
# change in/out marks to timeline in/out
in_mark = clip_in
out_mark = clip_out
# add xml tags modifications
modify_xml_data.update({
"exportHandles": True,
@ -201,10 +153,6 @@ class ExtractSubsetResources(openpype.api.Extractor):
# add any xml overrides collected form segment.comment
modify_xml_data.update(instance.data["xml_overrides"])
self.log.debug("__ modify_xml_data: {}".format(pformat(
modify_xml_data
)))
export_kwargs = {}
# validate xml preset file is filled
if preset_file == "":
@ -231,19 +179,34 @@ class ExtractSubsetResources(openpype.api.Extractor):
preset_dir, preset_file
))
preset_path = opfapi.modify_preset_file(
preset_orig_xml_path, staging_dir, modify_xml_data)
# define kwargs based on preset type
if "thumbnail" in unique_name:
export_kwargs["thumb_frame_number"] = int(in_mark + (
modify_xml_data.update({
"video/posterFrame": True,
"video/useFrameAsPoster": 1,
"namePattern": "__thumbnail"
})
thumb_frame_number = int(in_mark + (
source_duration_handles / 2))
self.log.debug("__ in_mark: {}".format(in_mark))
self.log.debug("__ thumb_frame_number: {}".format(
thumb_frame_number
))
export_kwargs["thumb_frame_number"] = thumb_frame_number
else:
export_kwargs.update({
"in_mark": in_mark,
"out_mark": out_mark
})
self.log.debug("__ modify_xml_data: {}".format(
pformat(modify_xml_data)
))
preset_path = opfapi.modify_preset_file(
preset_orig_xml_path, staging_dir, modify_xml_data)
# get and make export dir paths
export_dir_path = str(os.path.join(
staging_dir, unique_name
@ -254,18 +217,24 @@ class ExtractSubsetResources(openpype.api.Extractor):
opfapi.export_clip(
export_dir_path, exporting_clip, preset_path, **export_kwargs)
# make sure only first segment is used if underscore in name
# HACK: `ftrackreview_withLUT` will result only in `ftrackreview`
repr_name = unique_name.split("_")[0]
# create representation data
representation_data = {
"name": unique_name,
"outputName": unique_name,
"name": repr_name,
"outputName": repr_name,
"ext": extension,
"stagingDir": export_dir_path,
"tags": repre_tags,
"data": {
"colorspace": color_out
},
"load_to_batch_group": load_to_batch_group,
"batch_group_loader_name": batch_group_loader_name
"load_to_batch_group": preset_config.get(
"load_to_batch_group"),
"batch_group_loader_name": preset_config.get(
"batch_group_loader_name") or None
}
# collect all available content of export dir
@ -320,6 +289,30 @@ class ExtractSubsetResources(openpype.api.Extractor):
self.log.debug("All representations: {}".format(
pformat(instance.data["representations"])))
def _should_skip(self, preset_config, clip_path, unique_name):
# get activating attributes
activated_preset = preset_config["active"]
filter_path_regex = preset_config.get("filter_path_regex")
self.log.info(
"Preset `{}` is active `{}` with filter `{}`".format(
unique_name, activated_preset, filter_path_regex
)
)
self.log.debug(
"__ clip_path: `{}`".format(clip_path))
# skip if not activated presete
if not activated_preset:
return True
# exclude by regex filter if any
if (
filter_path_regex
and not re.search(filter_path_regex, clip_path)
):
return True
def _unfolds_nested_folders(self, stage_dir, files_list, ext):
"""Unfolds nested folders
@ -408,8 +401,17 @@ class ExtractSubsetResources(openpype.api.Extractor):
"""
Import clip from path
"""
clips = flame.import_clips(path)
dir_path = os.path.dirname(path)
media_info = MediaInfoFile(path, logger=self.log)
file_pattern = media_info.file_pattern
self.log.debug("__ file_pattern: {}".format(file_pattern))
# rejoin the pattern to dir path
new_path = os.path.join(dir_path, file_pattern)
clips = flame.import_clips(new_path)
self.log.info("Clips [{}] imported from `{}`".format(clips, path))
if not clips:
self.log.warning("Path `{}` is not having any clips".format(path))
return None

View file

@ -942,6 +942,10 @@ def is_overlapping(ti_test, ti_original, strict=False):
(ti_test.timelineIn() <= ti_original.timelineIn())
and (ti_test.timelineOut() >= ti_original.timelineOut())
)
if strict:
return covering_exp
inside_exp = (
(ti_test.timelineIn() >= ti_original.timelineIn())
and (ti_test.timelineOut() <= ti_original.timelineOut())
@ -955,15 +959,12 @@ def is_overlapping(ti_test, ti_original, strict=False):
and (ti_test.timelineIn() <= ti_original.timelineIn())
)
if strict:
return covering_exp
else:
return any((
covering_exp,
inside_exp,
overlaying_right_exp,
overlaying_left_exp
))
return any((
covering_exp,
inside_exp,
overlaying_right_exp,
overlaying_left_exp
))
def get_sequence_pattern_and_padding(file):

View file

@ -28,7 +28,7 @@ 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):
if cmds.attributeQuery("filename{}".format(i), node=node, ex=True):
file = cmds.getAttr("{}.filename{}".format(node, i))
if file:
attrs.append("filename{}".format(i))
@ -50,10 +50,10 @@ FILE_NODES = {
}
def get_attributes(dictionary, attr):
# type: (dict, str) -> list
def get_attributes(dictionary, attr, node=None):
# type: (dict, str, str) -> list
if callable(dictionary[attr]):
val = dictionary[attr]()
val = dictionary[attr](node)
else:
val = dictionary.get(attr, [])
@ -205,7 +205,8 @@ def get_file_node_paths(node):
return [texture_pattern]
try:
file_attributes = get_attributes(FILE_NODES, cmds.nodeType(node))
file_attributes = get_attributes(
FILE_NODES, cmds.nodeType(node), node)
except AttributeError:
file_attributes = "fileTextureName"
@ -434,7 +435,8 @@ class CollectLook(pyblish.api.InstancePlugin):
# 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"]))
@ -554,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
@ -571,60 +573,60 @@ class CollectLook(pyblish.api.InstancePlugin):
self.log.debug(" - got {}".format(cmds.nodeType(node)))
attribute = FILE_NODES.get(cmds.nodeType(node))
source = cmds.getAttr("{}.{}".format(
node,
attribute
))
computed_attribute = "{}.{}".format(node, attribute)
if attribute == "fileTextureName":
computed_attribute = node + ".computedFileTextureNamePattern"
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"
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))
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))
# 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("\\", "/")
# 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("\\", "/")
files = get_file_node_files(node)
if len(files) == 0:
self.log.error("No valid files found from node `%s`" % node)
files = get_file_node_files(node)
if len(files) == 0:
self.log.error("No valid files found from node `%s`" % node)
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))
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
return {
"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
# 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
class CollectModelRenderSets(CollectLook):

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

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

@ -203,6 +203,7 @@ from .editorial import (
is_overlapping_otio_ranges,
otio_range_to_frame_range,
otio_range_with_handles,
get_media_range_with_retimes,
convert_to_padded_path,
trim_media_range,
range_from_frames,
@ -382,6 +383,7 @@ __all__ = [
"otio_range_with_handles",
"convert_to_padded_path",
"otio_range_to_frame_range",
"get_media_range_with_retimes",
"trim_media_range",
"range_from_frames",
"frames_to_secons",

View file

@ -270,16 +270,16 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end):
"retime": True,
"speed": time_scalar,
"timewarps": time_warp_nodes,
"handleStart": handle_start,
"handleEnd": handle_end
"handleStart": round(handle_start),
"handleEnd": round(handle_end)
}
}
returning_dict = {
"mediaIn": media_in_trimmed,
"mediaOut": media_out_trimmed,
"handleStart": handle_start,
"handleEnd": handle_end
"handleStart": round(handle_start),
"handleEnd": round(handle_end)
}
# add version data only if retime

View file

@ -10,8 +10,7 @@ import os
import clique
import opentimelineio as otio
import pyblish.api
import openpype
from openpype.lib import editorial
import openpype.lib as oplib
class CollectOtioSubsetResources(pyblish.api.InstancePlugin):
@ -43,7 +42,7 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin):
available_duration = otio_avalable_range.duration.value
# get available range trimmed with processed retimes
retimed_attributes = editorial.get_media_range_with_retimes(
retimed_attributes = oplib.get_media_range_with_retimes(
otio_clip, handle_start, handle_end)
self.log.debug(
">> retimed_attributes: {}".format(retimed_attributes))
@ -65,7 +64,7 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin):
a_frame_end_h = media_out + handle_end
# create trimmed otio time range
trimmed_media_range_h = editorial.range_from_frames(
trimmed_media_range_h = oplib.range_from_frames(
a_frame_start_h, (a_frame_end_h - a_frame_start_h) + 1,
media_fps
)
@ -145,7 +144,7 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin):
# in case it is file sequence but not new OTIO schema
# `ImageSequenceReference`
path = media_ref.target_url
collection_data = openpype.lib.make_sequence_collection(
collection_data = oplib.make_sequence_collection(
path, trimmed_media_range_h, metadata)
self.staging_dir, collection = collection_data

View file

@ -16,7 +16,8 @@
"vSyncOn": false,
"workfileFrameStart": 1001,
"handleStart": 5,
"handleEnd": 5
"handleEnd": 5,
"includeHandles": false
}
},
"publish": {

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

@ -123,6 +123,11 @@
"type": "number",
"key": "handleEnd",
"label": "Handle end (tail)"
},
{
"type": "boolean",
"key": "includeHandles",
"label": "Enable handles including"
}
]
}

View file

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

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "OpenPype"
version = "3.10.0" # 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"