Merge branch 'develop' into feature/unreal-render_publishing

This commit is contained in:
Simone Barbieri 2022-04-28 17:53:36 +01:00
commit 99631d80c5
17 changed files with 738 additions and 187 deletions

0
.gitmodules vendored
View file

View file

@ -7,6 +7,7 @@ from openpype import (
)
from openpype.tools.utils import host_tools
from openpype.tools.utils.lib import qt_app_context
from openpype.hosts.unreal.api import rendering
class ToolsBtnsWidget(QtWidgets.QWidget):
@ -20,6 +21,7 @@ class ToolsBtnsWidget(QtWidgets.QWidget):
load_btn = QtWidgets.QPushButton("Load...", self)
publish_btn = QtWidgets.QPushButton("Publish...", self)
manage_btn = QtWidgets.QPushButton("Manage...", self)
render_btn = QtWidgets.QPushButton("Render...", self)
experimental_tools_btn = QtWidgets.QPushButton(
"Experimental tools...", self
)
@ -30,6 +32,7 @@ class ToolsBtnsWidget(QtWidgets.QWidget):
layout.addWidget(load_btn, 0)
layout.addWidget(publish_btn, 0)
layout.addWidget(manage_btn, 0)
layout.addWidget(render_btn, 0)
layout.addWidget(experimental_tools_btn, 0)
layout.addStretch(1)
@ -37,6 +40,7 @@ class ToolsBtnsWidget(QtWidgets.QWidget):
load_btn.clicked.connect(self._on_load)
publish_btn.clicked.connect(self._on_publish)
manage_btn.clicked.connect(self._on_manage)
render_btn.clicked.connect(self._on_render)
experimental_tools_btn.clicked.connect(self._on_experimental)
def _on_create(self):
@ -51,6 +55,9 @@ class ToolsBtnsWidget(QtWidgets.QWidget):
def _on_manage(self):
self.tool_required.emit("sceneinventory")
def _on_render(self):
rendering.start_rendering()
def _on_experimental(self):
self.tool_required.emit("experimental_tools")

View file

@ -254,6 +254,7 @@ def create_unreal_project(project_name: str,
{"Name": "PythonScriptPlugin", "Enabled": True},
{"Name": "EditorScriptingUtilities", "Enabled": True},
{"Name": "SequencerScripting", "Enabled": True},
{"Name": "MovieRenderPipeline", "Enabled": True},
{"Name": "OpenPype", "Enabled": True}
]
}

View file

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
from unreal import EditorLevelLibrary as ell
from unreal import EditorLevelLibrary
from openpype.hosts.unreal.api import plugin
from openpype.hosts.unreal.api.pipeline import instantiate
@ -28,13 +29,13 @@ class CreateLayout(plugin.Creator):
# sel_objects = unreal.EditorUtilityLibrary.get_selected_assets()
# selection = [a.get_path_name() for a in sel_objects]
data["level"] = ell.get_editor_world().get_path_name()
data["level"] = EditorLevelLibrary.get_editor_world().get_path_name()
data["members"] = []
if (self.options or {}).get("useSelection"):
# Set as members the selected actors
for actor in ell.get_selected_level_actors():
for actor in EditorLevelLibrary.get_selected_level_actors():
data["members"].append("{}.{}".format(
actor.get_outer().get_name(), actor.get_name()))

View file

@ -1,5 +1,6 @@
import unreal
from openpype.pipeline import legacy_io
from openpype.hosts.unreal.api import pipeline
from openpype.hosts.unreal.api.plugin import Creator

View file

@ -3,13 +3,17 @@
import os
import json
import unreal
from unreal import EditorAssetLibrary
from unreal import MovieSceneSkeletalAnimationTrack
from unreal import MovieSceneSkeletalAnimationSection
from openpype.pipeline import (
get_representation_path,
AVALON_CONTAINER_ID
)
from openpype.hosts.unreal.api import plugin
from openpype.hosts.unreal.api import pipeline as unreal_pipeline
import unreal # noqa
class AnimationFBXLoader(plugin.Loader):
@ -21,59 +25,13 @@ class AnimationFBXLoader(plugin.Loader):
icon = "cube"
color = "orange"
def load(self, context, name, namespace, options=None):
"""
Load and containerise representation into Content Browser.
This is two step process. First, import FBX to temporary path and
then call `containerise()` on it - this moves all content to new
directory and then it will create AssetContainer there and imprint it
with metadata. This will mark this path as container.
Args:
context (dict): application context
name (str): subset name
namespace (str): in Unreal this is basically path to container.
This is not passed here, so namespace is set
by `containerise()` because only then we know
real path.
data (dict): Those would be data to be imprinted. This is not used
now, data are imprinted by `containerise()`.
Returns:
list(str): list of container content
"""
# Create directory for asset and OpenPype container
root = "/Game/OpenPype/Assets"
asset = context.get('asset').get('name')
suffix = "_CON"
if asset:
asset_name = "{}_{}".format(asset, name)
else:
asset_name = "{}".format(name)
tools = unreal.AssetToolsHelpers().get_asset_tools()
asset_dir, container_name = tools.create_unique_asset_name(
"{}/{}/{}".format(root, asset, name), suffix="")
container_name += suffix
unreal.EditorAssetLibrary.make_directory(asset_dir)
def _process(self, asset_dir, asset_name, instance_name):
automated = False
actor = None
task = unreal.AssetImportTask()
task.options = unreal.FbxImportUI()
lib_path = self.fname.replace("fbx", "json")
with open(lib_path, "r") as fp:
data = json.load(fp)
instance_name = data.get("instance_name")
if instance_name:
automated = True
# Old method to get the actor
@ -131,6 +89,116 @@ class AnimationFBXLoader(plugin.Loader):
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
asset_content = EditorAssetLibrary.list_assets(
asset_dir, recursive=True, include_folder=True
)
animation = None
for a in asset_content:
imported_asset_data = EditorAssetLibrary.find_asset_data(a)
imported_asset = unreal.AssetRegistryHelpers.get_asset(
imported_asset_data)
if imported_asset.__class__ == unreal.AnimSequence:
animation = imported_asset
break
if animation:
animation.set_editor_property('enable_root_motion', True)
actor.skeletal_mesh_component.set_editor_property(
'animation_mode', unreal.AnimationMode.ANIMATION_SINGLE_NODE)
actor.skeletal_mesh_component.animation_data.set_editor_property(
'anim_to_play', animation)
return animation
def load(self, context, name, namespace, options=None):
"""
Load and containerise representation into Content Browser.
This is two step process. First, import FBX to temporary path and
then call `containerise()` on it - this moves all content to new
directory and then it will create AssetContainer there and imprint it
with metadata. This will mark this path as container.
Args:
context (dict): application context
name (str): subset name
namespace (str): in Unreal this is basically path to container.
This is not passed here, so namespace is set
by `containerise()` because only then we know
real path.
data (dict): Those would be data to be imprinted. This is not used
now, data are imprinted by `containerise()`.
Returns:
list(str): list of container content
"""
# Create directory for asset and avalon container
hierarchy = context.get('asset').get('data').get('parents')
root = "/Game/OpenPype"
asset = context.get('asset').get('name')
suffix = "_CON"
if asset:
asset_name = "{}_{}".format(asset, name)
else:
asset_name = "{}".format(name)
tools = unreal.AssetToolsHelpers().get_asset_tools()
asset_dir, container_name = tools.create_unique_asset_name(
f"{root}/Animations/{asset}/{name}", suffix="")
hierarchy_dir = root
for h in hierarchy:
hierarchy_dir = f"{hierarchy_dir}/{h}"
hierarchy_dir = f"{hierarchy_dir}/{asset}"
container_name += suffix
EditorAssetLibrary.make_directory(asset_dir)
libpath = self.fname.replace("fbx", "json")
with open(libpath, "r") as fp:
data = json.load(fp)
instance_name = data.get("instance_name")
animation = self._process(asset_dir, container_name, instance_name)
asset_content = EditorAssetLibrary.list_assets(
hierarchy_dir, recursive=True, include_folder=False)
# Get the sequence for the layout, excluding the camera one.
sequences = [a for a in asset_content
if (EditorAssetLibrary.find_asset_data(a).get_class() ==
unreal.LevelSequence.static_class() and
"_camera" not in a.split("/")[-1])]
ar = unreal.AssetRegistryHelpers.get_asset_registry()
for s in sequences:
sequence = ar.get_asset_by_object_path(s).get_asset()
possessables = [
p for p in sequence.get_possessables()
if p.get_display_name() == instance_name]
for p in possessables:
tracks = [
t for t in p.get_tracks()
if (t.get_class() ==
MovieSceneSkeletalAnimationTrack.static_class())]
for t in tracks:
sections = [
s for s in t.get_sections()
if (s.get_class() ==
MovieSceneSkeletalAnimationSection.static_class())]
for s in sections:
s.params.set_editor_property('animation', animation)
# Create Asset Container
unreal_pipeline.create_container(
container=container_name, path=asset_dir)
@ -150,29 +218,11 @@ class AnimationFBXLoader(plugin.Loader):
unreal_pipeline.imprint(
"{}/{}".format(asset_dir, container_name), data)
asset_content = unreal.EditorAssetLibrary.list_assets(
asset_dir, recursive=True, include_folder=True
)
imported_content = EditorAssetLibrary.list_assets(
asset_dir, recursive=True, include_folder=False)
animation = None
for a in asset_content:
unreal.EditorAssetLibrary.save_asset(a)
imported_asset_data = unreal.EditorAssetLibrary.find_asset_data(a)
imported_asset = unreal.AssetRegistryHelpers.get_asset(
imported_asset_data)
if imported_asset.__class__ == unreal.AnimSequence:
animation = imported_asset
break
if animation:
animation.set_editor_property('enable_root_motion', True)
actor.skeletal_mesh_component.set_editor_property(
'animation_mode', unreal.AnimationMode.ANIMATION_SINGLE_NODE)
actor.skeletal_mesh_component.animation_data.set_editor_property(
'anim_to_play', animation)
return asset_content
for a in imported_content:
EditorAssetLibrary.save_asset(a)
def update(self, container, representation):
name = container["asset_name"]
@ -218,7 +268,7 @@ class AnimationFBXLoader(plugin.Loader):
task.options.anim_sequence_import_data.set_editor_property(
'convert_scene', True)
skeletal_mesh = unreal.EditorAssetLibrary.load_asset(
skeletal_mesh = EditorAssetLibrary.load_asset(
container.get('namespace') + "/" + container.get('asset_name'))
skeleton = skeletal_mesh.get_editor_property('skeleton')
task.options.set_editor_property('skeleton', skeleton)
@ -235,22 +285,22 @@ class AnimationFBXLoader(plugin.Loader):
"parent": str(representation["parent"])
})
asset_content = unreal.EditorAssetLibrary.list_assets(
asset_content = EditorAssetLibrary.list_assets(
destination_path, recursive=True, include_folder=True
)
for a in asset_content:
unreal.EditorAssetLibrary.save_asset(a)
EditorAssetLibrary.save_asset(a)
def remove(self, container):
path = container["namespace"]
parent_path = os.path.dirname(path)
unreal.EditorAssetLibrary.delete_directory(path)
EditorAssetLibrary.delete_directory(path)
asset_content = unreal.EditorAssetLibrary.list_assets(
asset_content = EditorAssetLibrary.list_assets(
parent_path, recursive=False, include_folder=True
)
if len(asset_content) == 0:
unreal.EditorAssetLibrary.delete_directory(parent_path)
EditorAssetLibrary.delete_directory(parent_path)

View file

@ -2,13 +2,16 @@
"""Load camera from FBX."""
import os
import unreal
from unreal import EditorAssetLibrary
from unreal import EditorLevelLibrary
from openpype.pipeline import (
AVALON_CONTAINER_ID,
legacy_io,
)
from openpype.hosts.unreal.api import plugin
from openpype.hosts.unreal.api import pipeline as unreal_pipeline
import unreal # noqa
class CameraLoader(plugin.Loader):
@ -20,6 +23,40 @@ class CameraLoader(plugin.Loader):
icon = "cube"
color = "orange"
def _get_data(self, asset_name):
asset_doc = legacy_io.find_one({
"type": "asset",
"name": asset_name
})
return asset_doc.get("data")
def _set_sequence_hierarchy(
self, seq_i, seq_j, min_frame_j, max_frame_j
):
tracks = seq_i.get_master_tracks()
track = None
for t in tracks:
if t.get_class() == unreal.MovieSceneSubTrack.static_class():
track = t
break
if not track:
track = seq_i.add_master_track(unreal.MovieSceneSubTrack)
subscenes = track.get_sections()
subscene = None
for s in subscenes:
if s.get_editor_property('sub_sequence') == seq_j:
subscene = s
break
if not subscene:
subscene = track.add_section()
subscene.set_row_index(len(track.get_sections()))
subscene.set_editor_property('sub_sequence', seq_j)
subscene.set_range(
min_frame_j,
max_frame_j + 1)
def load(self, context, name, namespace, data):
"""
Load and containerise representation into Content Browser.
@ -43,8 +80,14 @@ class CameraLoader(plugin.Loader):
list(str): list of container content
"""
# Create directory for asset and OpenPype container
root = "/Game/OpenPype/Assets"
# Create directory for asset and avalon container
hierarchy = context.get('asset').get('data').get('parents')
root = "/Game/OpenPype"
hierarchy_dir = root
hierarchy_list = []
for h in hierarchy:
hierarchy_dir = f"{hierarchy_dir}/{h}"
hierarchy_list.append(hierarchy_dir)
asset = context.get('asset').get('name')
suffix = "_CON"
if asset:
@ -54,10 +97,10 @@ class CameraLoader(plugin.Loader):
tools = unreal.AssetToolsHelpers().get_asset_tools()
# Create a unique name for the camera directory
unique_number = 1
if unreal.EditorAssetLibrary.does_directory_exist(f"{root}/{asset}"):
asset_content = unreal.EditorAssetLibrary.list_assets(
if EditorAssetLibrary.does_directory_exist(f"{hierarchy_dir}/{asset}"):
asset_content = EditorAssetLibrary.list_assets(
f"{root}/{asset}", recursive=False, include_folder=True
)
@ -76,42 +119,122 @@ class CameraLoader(plugin.Loader):
unique_number = f_numbers[-1] + 1
asset_dir, container_name = tools.create_unique_asset_name(
f"{root}/{asset}/{name}_{unique_number:02d}", suffix="")
f"{hierarchy_dir}/{asset}/{name}_{unique_number:02d}", suffix="")
container_name += suffix
unreal.EditorAssetLibrary.make_directory(asset_dir)
current_level = EditorLevelLibrary.get_editor_world().get_full_name()
EditorLevelLibrary.save_all_dirty_levels()
sequence = tools.create_asset(
asset_name=asset_name,
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())
# 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:
root_content = EditorAssetLibrary.list_assets(
h, recursive=False, include_folder=False)
existing_sequences = [
EditorAssetLibrary.find_asset_data(asset)
for asset in root_content
if EditorAssetLibrary.find_asset_data(
asset).get_class().get_name() == 'LevelSequence'
]
if not existing_sequences:
scene = tools.create_asset(
asset_name=hierarchy[i],
package_path=h,
asset_class=unreal.LevelSequence,
factory=unreal.LevelSequenceFactoryNew()
)
asset_data = legacy_io.find_one({
"type": "asset",
"name": h.split('/')[-1]
})
id = asset_data.get('_id')
start_frames = []
end_frames = []
elements = list(
legacy_io.find({"type": "asset", "data.visualParent": id}))
for e in elements:
start_frames.append(e.get('data').get('clipIn'))
end_frames.append(e.get('data').get('clipOut'))
elements.extend(legacy_io.find({
"type": "asset",
"data.visualParent": e.get('_id')
}))
min_frame = min(start_frames)
max_frame = max(end_frames)
scene.set_display_rate(
unreal.FrameRate(asset_data.get('data').get("fps"), 1.0))
scene.set_playback_start(min_frame)
scene.set_playback_end(max_frame)
sequences.append(scene)
frame_ranges.append((min_frame, max_frame))
else:
for e in existing_sequences:
sequences.append(e.get_asset())
frame_ranges.append((
e.get_asset().get_playback_start(),
e.get_asset().get_playback_end()))
i += 1
EditorAssetLibrary.make_directory(asset_dir)
cam_seq = tools.create_asset(
asset_name=f"{asset}_camera",
package_path=asset_dir,
asset_class=unreal.LevelSequence,
factory=unreal.LevelSequenceFactoryNew()
)
io_asset = legacy_io.Session["AVALON_ASSET"]
asset_doc = legacy_io.find_one({
"type": "asset",
"name": io_asset
})
# Add sequences data to hierarchy
for i in range(0, len(sequences) - 1):
self._set_sequence_hierarchy(
sequences[i], sequences[i + 1],
frame_ranges[i + 1][0], frame_ranges[i + 1][1])
data = asset_doc.get("data")
if data:
sequence.set_display_rate(unreal.FrameRate(data.get("fps"), 1.0))
sequence.set_playback_start(data.get("frameStart"))
sequence.set_playback_end(data.get("frameEnd"))
data = self._get_data(asset)
cam_seq.set_display_rate(
unreal.FrameRate(data.get("fps"), 1.0))
cam_seq.set_playback_start(0)
cam_seq.set_playback_end(data.get('clipOut') - data.get('clipIn') + 1)
self._set_sequence_hierarchy(
sequences[-1], cam_seq,
data.get('clipIn'), data.get('clipOut'))
settings = unreal.MovieSceneUserImportFBXSettings()
settings.set_editor_property('reduce_keys', False)
unreal.SequencerTools.import_fbx(
unreal.EditorLevelLibrary.get_editor_world(),
sequence,
sequence.get_bindings(),
settings,
self.fname
)
if cam_seq:
unreal.SequencerTools.import_fbx(
EditorLevelLibrary.get_editor_world(),
cam_seq,
cam_seq.get_bindings(),
settings,
self.fname
)
# Create Asset Container
unreal_pipeline.create_container(
@ -132,12 +255,15 @@ class CameraLoader(plugin.Loader):
unreal_pipeline.imprint(
"{}/{}".format(asset_dir, container_name), data)
asset_content = unreal.EditorAssetLibrary.list_assets(
EditorLevelLibrary.save_all_dirty_levels()
EditorLevelLibrary.load_level(current_level)
asset_content = EditorAssetLibrary.list_assets(
asset_dir, recursive=True, include_folder=True
)
for a in asset_content:
unreal.EditorAssetLibrary.save_asset(a)
EditorAssetLibrary.save_asset(a)
return asset_content
@ -147,25 +273,25 @@ class CameraLoader(plugin.Loader):
ar = unreal.AssetRegistryHelpers.get_asset_registry()
tools = unreal.AssetToolsHelpers().get_asset_tools()
asset_content = unreal.EditorAssetLibrary.list_assets(
asset_content = EditorAssetLibrary.list_assets(
path, recursive=False, include_folder=False
)
asset_name = ""
for a in asset_content:
asset = ar.get_asset_by_object_path(a)
if a.endswith("_CON"):
loaded_asset = unreal.EditorAssetLibrary.load_asset(a)
unreal.EditorAssetLibrary.set_metadata_tag(
loaded_asset = EditorAssetLibrary.load_asset(a)
EditorAssetLibrary.set_metadata_tag(
loaded_asset, "representation", str(representation["_id"])
)
unreal.EditorAssetLibrary.set_metadata_tag(
EditorAssetLibrary.set_metadata_tag(
loaded_asset, "parent", str(representation["parent"])
)
asset_name = unreal.EditorAssetLibrary.get_metadata_tag(
asset_name = EditorAssetLibrary.get_metadata_tag(
loaded_asset, "asset_name"
)
elif asset.asset_class == "LevelSequence":
unreal.EditorAssetLibrary.delete_asset(a)
EditorAssetLibrary.delete_asset(a)
sequence = tools.create_asset(
asset_name=asset_name,
@ -191,7 +317,7 @@ class CameraLoader(plugin.Loader):
settings.set_editor_property('reduce_keys', False)
unreal.SequencerTools.import_fbx(
unreal.EditorLevelLibrary.get_editor_world(),
EditorLevelLibrary.get_editor_world(),
sequence,
sequence.get_bindings(),
settings,
@ -202,11 +328,11 @@ class CameraLoader(plugin.Loader):
path = container["namespace"]
parent_path = os.path.dirname(path)
unreal.EditorAssetLibrary.delete_directory(path)
EditorAssetLibrary.delete_directory(path)
asset_content = unreal.EditorAssetLibrary.list_assets(
asset_content = EditorAssetLibrary.list_assets(
parent_path, recursive=False, include_folder=True
)
if len(asset_content) == 0:
unreal.EditorAssetLibrary.delete_directory(parent_path)
EditorAssetLibrary.delete_directory(parent_path)

View file

@ -7,6 +7,7 @@ from pathlib import Path
import unreal
from unreal import EditorAssetLibrary
from unreal import EditorLevelLibrary
from unreal import EditorLevelUtils
from unreal import AssetToolsHelpers
from unreal import FBXImportType
from unreal import MathLibrary as umath
@ -17,6 +18,7 @@ from openpype.pipeline import (
load_container,
get_representation_path,
AVALON_CONTAINER_ID,
legacy_io,
)
from openpype.hosts.unreal.api import plugin
from openpype.hosts.unreal.api import pipeline as unreal_pipeline
@ -31,7 +33,7 @@ class LayoutLoader(plugin.Loader):
label = "Load Layout"
icon = "code-fork"
color = "orange"
ASSET_ROOT = "/Game/OpenPype/Assets"
ASSET_ROOT = "/Game/OpenPype"
def _get_asset_containers(self, path):
ar = unreal.AssetRegistryHelpers.get_asset_registry()
@ -85,11 +87,91 @@ class LayoutLoader(plugin.Loader):
return None
@staticmethod
def _process_family(assets, class_name, transform, inst_name=None):
def _get_data(self, asset_name):
asset_doc = legacy_io.find_one({
"type": "asset",
"name": asset_name
})
return asset_doc.get("data")
def _set_sequence_hierarchy(
self, 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()
subscene_track = None
visibility_track = None
for t in tracks:
if t.get_class() == unreal.MovieSceneSubTrack.static_class():
subscene_track = t
if (t.get_class() ==
unreal.MovieSceneLevelVisibilityTrack.static_class()):
visibility_track = t
if not subscene_track:
subscene_track = seq_i.add_master_track(unreal.MovieSceneSubTrack)
if not visibility_track:
visibility_track = seq_i.add_master_track(
unreal.MovieSceneLevelVisibilityTrack)
# Create the sub-scene section
subscenes = subscene_track.get_sections()
subscene = None
for s in subscenes:
if s.get_editor_property('sub_sequence') == seq_j:
subscene = s
break
if not subscene:
subscene = subscene_track.add_section()
subscene.set_row_index(len(subscene_track.get_sections()))
subscene.set_editor_property('sub_sequence', seq_j)
subscene.set_range(
min_frame_j,
max_frame_j + 1)
# Create the visibility section
ar = unreal.AssetRegistryHelpers.get_asset_registry()
maps = []
for m in map_paths:
# Unreal requires to load the level to get the map name
EditorLevelLibrary.save_all_dirty_levels()
EditorLevelLibrary.load_level(m)
maps.append(str(ar.get_asset_by_object_path(m).asset_name))
vis_section = visibility_track.add_section()
index = len(visibility_track.get_sections())
vis_section.set_range(
min_frame_j,
max_frame_j + 1)
vis_section.set_visibility(unreal.LevelVisibility.VISIBLE)
vis_section.set_row_index(index)
vis_section.set_level_names(maps)
if min_frame_j > 1:
hid_section = visibility_track.add_section()
hid_section.set_range(
1,
min_frame_j)
hid_section.set_visibility(unreal.LevelVisibility.HIDDEN)
hid_section.set_row_index(index)
hid_section.set_level_names(maps)
if max_frame_j < max_frame_i:
hid_section = visibility_track.add_section()
hid_section.set_range(
max_frame_j + 1,
max_frame_i + 1)
hid_section.set_visibility(unreal.LevelVisibility.HIDDEN)
hid_section.set_row_index(index)
hid_section.set_level_names(maps)
def _process_family(
self, assets, class_name, transform, sequence, inst_name=None
):
ar = unreal.AssetRegistryHelpers.get_asset_registry()
actors = []
bindings = []
for asset in assets:
obj = ar.get_asset_by_object_path(asset).get_asset()
@ -119,14 +201,23 @@ class LayoutLoader(plugin.Loader):
), False)
actor.set_actor_scale3d(transform.get('scale'))
if class_name == 'SkeletalMesh':
skm_comp = actor.get_editor_property(
'skeletal_mesh_component')
skm_comp.set_bounds_scale(10.0)
actors.append(actor)
return actors
binding = sequence.add_possessable(actor)
bindings.append(binding)
return actors, bindings
@staticmethod
def _import_animation(
asset_dir, path, instance_name, skeleton, actors_dict,
animation_file):
self, asset_dir, path, instance_name, skeleton, actors_dict,
animation_file, bindings_dict, sequence
):
anim_file = Path(animation_file)
anim_file_name = anim_file.with_suffix('')
@ -205,7 +296,20 @@ class LayoutLoader(plugin.Loader):
actor.skeletal_mesh_component.animation_data.set_editor_property(
'anim_to_play', animation)
def _process(self, lib_path, asset_dir, loaded=None):
# Add animation to the sequencer
bindings = bindings_dict.get(instance_name)
for binding in bindings:
binding.add_track(unreal.MovieSceneSkeletalAnimationTrack)
for track in binding.get_tracks():
section = track.add_section()
section.set_range(
sequence.get_playback_start(),
sequence.get_playback_end())
sec_params = section.get_editor_property('params')
sec_params.set_editor_property('animation', animation)
def _process(self, lib_path, asset_dir, sequence, loaded=None):
ar = unreal.AssetRegistryHelpers.get_asset_registry()
with open(lib_path, "r") as fp:
@ -220,6 +324,7 @@ class LayoutLoader(plugin.Loader):
skeleton_dict = {}
actors_dict = {}
bindings_dict = {}
for element in data:
reference = None
@ -277,12 +382,13 @@ class LayoutLoader(plugin.Loader):
actors = []
if family == 'model':
actors = self._process_family(
assets, 'StaticMesh', transform, inst)
actors, _ = self._process_family(
assets, 'StaticMesh', transform, sequence, inst)
elif family == 'rig':
actors = self._process_family(
assets, 'SkeletalMesh', transform, inst)
actors, bindings = self._process_family(
assets, 'SkeletalMesh', transform, sequence, inst)
actors_dict[inst] = actors
bindings_dict[inst] = bindings
if family == 'rig':
# Finds skeleton among the imported assets
@ -302,8 +408,8 @@ class LayoutLoader(plugin.Loader):
if animation_file and skeleton:
self._import_animation(
asset_dir, path, instance_name, skeleton,
actors_dict, animation_file)
asset_dir, path, instance_name, skeleton, actors_dict,
animation_file, bindings_dict, sequence)
@staticmethod
def _remove_family(assets, components, class_name, prop_name):
@ -369,7 +475,13 @@ class LayoutLoader(plugin.Loader):
list(str): list of container content
"""
# Create directory for asset and avalon container
hierarchy = context.get('asset').get('data').get('parents')
root = self.ASSET_ROOT
hierarchy_dir = root
hierarchy_list = []
for h in hierarchy:
hierarchy_dir = f"{hierarchy_dir}/{h}"
hierarchy_list.append(hierarchy_dir)
asset = context.get('asset').get('name')
suffix = "_CON"
if asset:
@ -379,13 +491,156 @@ class LayoutLoader(plugin.Loader):
tools = unreal.AssetToolsHelpers().get_asset_tools()
asset_dir, container_name = tools.create_unique_asset_name(
"{}/{}/{}".format(root, asset, name), suffix="")
"{}/{}/{}".format(hierarchy_dir, asset, name), suffix="")
container_name += suffix
EditorAssetLibrary.make_directory(asset_dir)
self._process(self.fname, asset_dir)
# Create map for the shot, and create hierarchy of map. If the maps
# already exist, we will use them.
maps = []
for h in hierarchy_list:
a = h.split('/')[-1]
map = f"{h}/{a}_map.{a}_map"
new = False
if not EditorAssetLibrary.does_asset_exist(map):
EditorLevelLibrary.new_level(f"{h}/{a}_map")
new = True
maps.append({"map": map, "new": new})
EditorLevelLibrary.new_level(f"{asset_dir}/{asset}_map")
maps.append(
{"map": f"{asset_dir}/{asset}_map.{asset}_map", "new": True})
for i in range(0, len(maps) - 1):
for j in range(i + 1, len(maps)):
if maps[j].get('new'):
EditorLevelLibrary.load_level(maps[i].get('map'))
EditorLevelUtils.add_level_to_world(
EditorLevelLibrary.get_editor_world(),
maps[j].get('map'),
unreal.LevelStreamingDynamic
)
EditorLevelLibrary.save_all_dirty_levels()
EditorLevelLibrary.load_level(maps[-1].get('map'))
# 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:
root_content = EditorAssetLibrary.list_assets(
h, recursive=False, include_folder=False)
existing_sequences = [
EditorAssetLibrary.find_asset_data(asset)
for asset in root_content
if EditorAssetLibrary.find_asset_data(
asset).get_class().get_name() == 'LevelSequence'
]
if not existing_sequences:
sequence = tools.create_asset(
asset_name=hierarchy[i],
package_path=h,
asset_class=unreal.LevelSequence,
factory=unreal.LevelSequenceFactoryNew()
)
asset_data = legacy_io.find_one({
"type": "asset",
"name": h.split('/')[-1]
})
id = asset_data.get('_id')
start_frames = []
end_frames = []
elements = list(
legacy_io.find({"type": "asset", "data.visualParent": id}))
for e in elements:
start_frames.append(e.get('data').get('clipIn'))
end_frames.append(e.get('data').get('clipOut'))
elements.extend(legacy_io.find({
"type": "asset",
"data.visualParent": e.get('_id')
}))
min_frame = min(start_frames)
max_frame = max(end_frames)
sequence.set_display_rate(
unreal.FrameRate(asset_data.get('data').get("fps"), 1.0))
sequence.set_playback_start(min_frame)
sequence.set_playback_end(max_frame)
sequences.append(sequence)
frame_ranges.append((min_frame, max_frame))
tracks = sequence.get_master_tracks()
track = None
for t in tracks:
if (t.get_class() ==
unreal.MovieSceneCameraCutTrack.static_class()):
track = t
break
if not track:
track = sequence.add_master_track(
unreal.MovieSceneCameraCutTrack)
else:
for e in existing_sequences:
sequences.append(e.get_asset())
frame_ranges.append((
e.get_asset().get_playback_start(),
e.get_asset().get_playback_end()))
i += 1
shot = tools.create_asset(
asset_name=asset,
package_path=asset_dir,
asset_class=unreal.LevelSequence,
factory=unreal.LevelSequenceFactoryNew()
)
# sequences and frame_ranges have the same length
for i in range(0, len(sequences) - 1):
maps_to_add = []
for j in range(i + 1, len(maps)):
maps_to_add.append(maps[j].get('map'))
self._set_sequence_hierarchy(
sequences[i], sequences[i + 1],
frame_ranges[i][1],
frame_ranges[i + 1][0], frame_ranges[i + 1][1],
maps_to_add)
data = self._get_data(asset)
shot.set_display_rate(
unreal.FrameRate(data.get("fps"), 1.0))
shot.set_playback_start(0)
shot.set_playback_end(data.get('clipOut') - data.get('clipIn') + 1)
self._set_sequence_hierarchy(
sequences[-1], shot,
frame_ranges[-1][1],
data.get('clipIn'), data.get('clipOut'),
[maps[-1].get('map')])
EditorLevelLibrary.load_level(maps[-1].get('map'))
self._process(self.fname, asset_dir, shot)
for s in sequences:
EditorAssetLibrary.save_asset(s.get_full_name())
EditorLevelLibrary.save_current_level()
# Create Asset Container
unreal_pipeline.create_container(
@ -412,6 +667,8 @@ class LayoutLoader(plugin.Loader):
for a in asset_content:
EditorAssetLibrary.save_asset(a)
EditorLevelLibrary.load_level(maps[0].get('map'))
return asset_content
def update(self, container, representation):

View file

@ -0,0 +1,48 @@
from pathlib import Path
import unreal
import openpype.api
class ExtractRender(openpype.api.Extractor):
"""Extract render."""
label = "Extract Render"
hosts = ["unreal"]
families = ["render"]
optional = True
def process(self, instance):
# Define extract output file path
stagingdir = self.staging_dir(instance)
# Perform extraction
self.log.info("Performing extraction..")
# Get the render output directory
project_dir = unreal.Paths.project_dir()
render_dir = (f"{project_dir}/Saved/MovieRenders/"
f"{instance.data['subset']}")
assert unreal.Paths.directory_exists(render_dir), \
"Render directory does not exist"
render_path = Path(render_dir)
frames = []
for x in render_path.iterdir():
if x.is_file() and x.suffix == '.png':
frames.append(str(x))
if "representations" not in instance.data:
instance.data["representations"] = []
render_representation = {
'name': 'png',
'ext': 'png',
'files': frames,
"stagingDir": stagingdir,
}
instance.data["representations"].append(render_representation)

View file

@ -1532,13 +1532,13 @@ class BuildWorkfile:
subsets = list(legacy_io.find({
"type": "subset",
"parent": {"$in": asset_entity_by_ids.keys()}
"parent": {"$in": list(asset_entity_by_ids.keys())}
}))
subset_entity_by_ids = {subset["_id"]: subset for subset in subsets}
sorted_versions = list(legacy_io.find({
"type": "version",
"parent": {"$in": subset_entity_by_ids.keys()}
"parent": {"$in": list(subset_entity_by_ids.keys())}
}).sort("name", -1))
subset_id_with_latest_version = []
@ -1552,7 +1552,7 @@ class BuildWorkfile:
repres = legacy_io.find({
"type": "representation",
"parent": {"$in": last_versions_by_id.keys()}
"parent": {"$in": list(last_versions_by_id.keys())}
})
output = {}

@ -0,0 +1 @@
Subproject commit b746fedf7286c3755a46f07ab72f4c414cd41fc0

@ -0,0 +1 @@
Subproject commit d277f474ab016e7b53479c36af87cb861d0cc53e

View file

@ -24,48 +24,6 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin):
label = "Integrate Ftrack Api"
families = ["ftrack"]
def query(self, entitytype, data):
""" Generate a query expression from data supplied.
If a value is not a string, we'll add the id of the entity to the
query.
Args:
entitytype (str): The type of entity to query.
data (dict): The data to identify the entity.
exclusions (list): All keys to exclude from the query.
Returns:
str: String query to use with "session.query"
"""
queries = []
if sys.version_info[0] < 3:
for key, value in data.iteritems():
if not isinstance(value, (basestring, int)):
self.log.info("value: {}".format(value))
if "id" in value.keys():
queries.append(
"{0}.id is \"{1}\"".format(key, value["id"])
)
else:
queries.append("{0} is \"{1}\"".format(key, value))
else:
for key, value in data.items():
if not isinstance(value, (str, int)):
self.log.info("value: {}".format(value))
if "id" in value.keys():
queries.append(
"{0}.id is \"{1}\"".format(key, value["id"])
)
else:
queries.append("{0} is \"{1}\"".format(key, value))
query = (
"select id from " + entitytype + " where " + " and ".join(queries)
)
self.log.debug(query)
return query
def process(self, instance):
session = instance.context.data["ftrackSession"]
context = instance.context
@ -108,7 +66,19 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin):
default_asset_name = parent_entity["name"]
# Change status on task
self._set_task_status(instance, task_entity, session)
asset_version_status_ids_by_name = {}
project_entity = instance.context.data.get("ftrackProject")
if project_entity:
project_schema = project_entity["project_schema"]
asset_version_statuses = (
project_schema.get_statuses("AssetVersion")
)
asset_version_status_ids_by_name = {
status["name"].lower(): status["id"]
for status in asset_version_statuses
}
self._set_task_status(instance, project_entity, task_entity, session)
# Prepare AssetTypes
asset_types_by_short = self._ensure_asset_types_exists(
@ -139,7 +109,11 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin):
# Asset Version
asset_version_data = data.get("assetversion_data") or {}
asset_version_entity = self._ensure_asset_version_exists(
session, asset_version_data, asset_entity["id"], task_entity
session,
asset_version_data,
asset_entity["id"],
task_entity,
asset_version_status_ids_by_name
)
# Component
@ -174,8 +148,7 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin):
if asset_version not in instance.data[asset_versions_key]:
instance.data[asset_versions_key].append(asset_version)
def _set_task_status(self, instance, task_entity, session):
project_entity = instance.context.data.get("ftrackProject")
def _set_task_status(self, instance, project_entity, task_entity, session):
if not project_entity:
self.log.info("Task status won't be set, project is not known.")
return
@ -319,12 +292,19 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin):
).first()
def _ensure_asset_version_exists(
self, session, asset_version_data, asset_id, task_entity
self,
session,
asset_version_data,
asset_id,
task_entity,
status_ids_by_name
):
task_id = None
if task_entity:
task_id = task_entity["id"]
status_name = asset_version_data.pop("status_name", None)
# Try query asset version by criteria (asset id and version)
version = asset_version_data.get("version") or 0
asset_version_entity = self._query_asset_version(
@ -366,6 +346,18 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin):
session, version, asset_id
)
if status_name:
status_id = status_ids_by_name.get(status_name.lower())
if not status_id:
self.log.info((
"Ftrack status with name \"{}\""
" for AssetVersion was not found."
).format(status_name))
elif asset_version_entity["status_id"] != status_id:
asset_version_entity["status_id"] = status_id
session.commit()
# Set custom attributes if there were any set
custom_attrs = asset_version_data.get("custom_attributes") or {}
for attr_key, attr_value in custom_attrs.items():

View file

@ -3,6 +3,8 @@ import json
import copy
import pyblish.api
from openpype.lib.profiles_filtering import filter_profiles
class IntegrateFtrackInstance(pyblish.api.InstancePlugin):
"""Collect ftrack component data (not integrate yet).
@ -36,6 +38,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin):
"reference": "reference"
}
keep_first_subset_name_for_review = True
asset_versions_status_profiles = {}
def process(self, instance):
self.log.debug("instance {}".format(instance))
@ -80,6 +83,8 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin):
if instance_fps is None:
instance_fps = instance.context.data["fps"]
status_name = self._get_asset_version_status_name(instance)
# Base of component item data
# - create a copy of this object when want to use it
base_component_item = {
@ -91,7 +96,8 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin):
},
"assetversion_data": {
"version": version_number,
"comment": instance.context.data.get("comment") or ""
"comment": instance.context.data.get("comment") or "",
"status_name": status_name
},
"component_overwrite": False,
# This can be change optionally
@ -317,3 +323,24 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin):
)
))
instance.data["ftrackComponentsList"] = component_list
def _get_asset_version_status_name(self, instance):
if not self.asset_versions_status_profiles:
return None
# Prepare filtering data for new asset version status
anatomy_data = instance.data["anatomyData"]
task_type = anatomy_data.get("task", {}).get("type")
filtering_criteria = {
"families": instance.data["family"],
"hosts": instance.context.data["hostName"],
"task_types": task_type
}
matching_profile = filter_profiles(
self.asset_versions_status_profiles,
filtering_criteria
)
if not matching_profile:
return None
return matching_profile["status"] or None

View file

@ -418,7 +418,8 @@
"redshiftproxy": "cache",
"usd": "usd"
},
"keep_first_subset_name_for_review": true
"keep_first_subset_name_for_review": true,
"asset_versions_status_profiles": []
}
}
}

View file

@ -858,6 +858,43 @@
"key": "keep_first_subset_name_for_review",
"label": "Make subset name as first asset name",
"default": true
},
{
"type": "list",
"collapsible": true,
"key": "asset_versions_status_profiles",
"label": "AssetVersion status on publish",
"use_label_wrap": true,
"object_type": {
"type": "dict",
"children": [
{
"key": "hosts",
"label": "Host names",
"type": "hosts-enum",
"multiselection": true
},
{
"key": "task_types",
"label": "Task types",
"type": "task-types-enum"
},
{
"key": "family",
"label": "Family",
"type": "list",
"object_type": "text"
},
{
"type": "separator"
},
{
"key": "status",
"label": "Status name",
"type": "text"
}
]
}
}
]
}

@ -0,0 +1 @@
Subproject commit 43f6ea943980b29c02a170942b566ae11f2b7080