Skip to content

Commit

Permalink
Merge pull request #4477 from ynput/enhancement/unreal-render_creator…
Browse files Browse the repository at this point in the history
…_improvements
  • Loading branch information
antirotor committed Apr 24, 2023
2 parents 5c4297d + 7d72069 commit 37cb21a
Show file tree
Hide file tree
Showing 3 changed files with 228 additions and 38 deletions.
10 changes: 10 additions & 0 deletions openpype/hosts/unreal/api/rendering.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from openpype.pipeline import Anatomy
from openpype.hosts.unreal.api import pipeline
from openpype.widgets.message_window import Window


queue = None
Expand Down Expand Up @@ -37,6 +38,15 @@ def start_rendering():
# Get selected sequences
assets = unreal.EditorUtilityLibrary.get_selected_assets()

if not assets:
Window(
parent=None,
title="No assets selected",
message="No assets selected. Select a render instance.",
level="warning")
raise RuntimeError(
"No assets selected. You need to select a render instance.")

# instances = pipeline.ls_inst()
instances = [
a for a in assets
Expand Down
214 changes: 176 additions & 38 deletions openpype/hosts/unreal/plugins/create/create_render.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
# -*- coding: utf-8 -*-
from pathlib import Path

import unreal

from openpype.pipeline import CreatorError
from openpype.hosts.unreal.api.pipeline import (
get_subsequences
UNREAL_VERSION,
create_folder,
get_subsequences,
)
from openpype.hosts.unreal.api.plugin import (
UnrealAssetCreator
)
from openpype.lib import UILabelDef
from openpype.lib import (
UILabelDef,
UISeparatorDef,
BoolDef,
NumberDef
)


class CreateRender(UnrealAssetCreator):
Expand All @@ -19,16 +27,101 @@ class CreateRender(UnrealAssetCreator):
family = "render"
icon = "eye"

def create(self, subset_name, instance_data, pre_create_data):
def create_instance(
self, instance_data, subset_name, pre_create_data,
selected_asset_path, master_seq, master_lvl, seq_data
):
instance_data["members"] = [selected_asset_path]
instance_data["sequence"] = selected_asset_path
instance_data["master_sequence"] = master_seq
instance_data["master_level"] = master_lvl
instance_data["output"] = seq_data.get('output')
instance_data["frameStart"] = seq_data.get('frame_range')[0]
instance_data["frameEnd"] = seq_data.get('frame_range')[1]

super(CreateRender, self).create(
subset_name,
instance_data,
pre_create_data)

def create_with_new_sequence(
self, subset_name, instance_data, pre_create_data
):
# If the option to create a new level sequence is selected,
# create a new level sequence and a master level.

root = f"/Game/OpenPype/Sequences"

# Create a new folder for the sequence in root
sequence_dir_name = create_folder(root, subset_name)
sequence_dir = f"{root}/{sequence_dir_name}"

unreal.log_warning(f"sequence_dir: {sequence_dir}")

# Create the level sequence
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
seq = asset_tools.create_asset(
asset_name=subset_name,
package_path=sequence_dir,
asset_class=unreal.LevelSequence,
factory=unreal.LevelSequenceFactoryNew())

seq.set_playback_start(pre_create_data.get("start_frame"))
seq.set_playback_end(pre_create_data.get("end_frame"))

pre_create_data["members"] = [seq.get_path_name()]

unreal.EditorAssetLibrary.save_asset(seq.get_path_name())

# Create the master level
if UNREAL_VERSION.major >= 5:
curr_level = unreal.LevelEditorSubsystem().get_current_level()
else:
world = unreal.EditorLevelLibrary.get_editor_world()
levels = unreal.EditorLevelUtils.get_levels(world)
curr_level = levels[0] if len(levels) else None
if not curr_level:
raise RuntimeError("No level loaded.")
curr_level_path = curr_level.get_outer().get_path_name()

# If the level path does not start with "/Game/", the current
# level is a temporary, unsaved level.
if curr_level_path.startswith("/Game/"):
if UNREAL_VERSION.major >= 5:
unreal.LevelEditorSubsystem().save_current_level()
else:
unreal.EditorLevelLibrary.save_current_level()

ml_path = f"{sequence_dir}/{subset_name}_MasterLevel"

if UNREAL_VERSION.major >= 5:
unreal.LevelEditorSubsystem().new_level(ml_path)
else:
unreal.EditorLevelLibrary.new_level(ml_path)

seq_data = {
"sequence": seq,
"output": f"{seq.get_name()}",
"frame_range": (
seq.get_playback_start(),
seq.get_playback_end())}

self.create_instance(
instance_data, subset_name, pre_create_data,
seq.get_path_name(), seq.get_path_name(), ml_path, seq_data)

def create_from_existing_sequence(
self, subset_name, instance_data, pre_create_data
):
ar = unreal.AssetRegistryHelpers.get_asset_registry()

sel_objects = unreal.EditorUtilityLibrary.get_selected_assets()
selection = [
a.get_path_name() for a in sel_objects
if a.get_class().get_name() == "LevelSequence"]

if not selection:
raise CreatorError("Please select at least one Level Sequence.")
if len(selection) == 0:
raise RuntimeError("Please select at least one Level Sequence.")

seq_data = None

Expand All @@ -42,28 +135,38 @@ def create(self, subset_name, instance_data, pre_create_data):
f"Skipping {selected_asset.get_name()}. It isn't a Level "
"Sequence.")

# The asset name is the third element of the path which
# contains the map.
# To take the asset name, we remove from the path the prefix
# "/Game/OpenPype/" and then we split the path by "/".
sel_path = selected_asset_path
asset_name = sel_path.replace("/Game/OpenPype/", "").split("/")[0]
if pre_create_data.get("use_hierarchy"):
# The asset name is the the third element of the path which
# contains the map.
# To take the asset name, we remove from the path the prefix
# "/Game/OpenPype/" and then we split the path by "/".
sel_path = selected_asset_path
asset_name = sel_path.replace(
"/Game/OpenPype/", "").split("/")[0]

search_path = f"/Game/OpenPype/{asset_name}"
else:
search_path = Path(selected_asset_path).parent.as_posix()

# Get the master sequence and the master level.
# There should be only one sequence and one level in the directory.
ar_filter = unreal.ARFilter(
class_names=["LevelSequence"],
package_paths=[f"/Game/OpenPype/{asset_name}"],
recursive_paths=False)
sequences = ar.get_assets(ar_filter)
master_seq = sequences[0].get_asset().get_path_name()
master_seq_obj = sequences[0].get_asset()
ar_filter = unreal.ARFilter(
class_names=["World"],
package_paths=[f"/Game/OpenPype/{asset_name}"],
recursive_paths=False)
levels = ar.get_assets(ar_filter)
master_lvl = levels[0].get_asset().get_path_name()
try:
ar_filter = unreal.ARFilter(
class_names=["LevelSequence"],
package_paths=[search_path],
recursive_paths=False)
sequences = ar.get_assets(ar_filter)
master_seq = sequences[0].get_asset().get_path_name()
master_seq_obj = sequences[0].get_asset()
ar_filter = unreal.ARFilter(
class_names=["World"],
package_paths=[search_path],
recursive_paths=False)
levels = ar.get_assets(ar_filter)
master_lvl = levels[0].get_asset().get_path_name()
except IndexError:
raise RuntimeError(
f"Could not find the hierarchy for the selected sequence.")

# If the selected asset is the master sequence, we get its data
# and then we create the instance for the master sequence.
Expand All @@ -79,7 +182,8 @@ def create(self, subset_name, instance_data, pre_create_data):
master_seq_obj.get_playback_start(),
master_seq_obj.get_playback_end())}

if selected_asset_path == master_seq:
if (selected_asset_path == master_seq or
pre_create_data.get("use_hierarchy")):
seq_data = master_seq_data
else:
seq_data_list = [master_seq_data]
Expand Down Expand Up @@ -119,20 +223,54 @@ def create(self, subset_name, instance_data, pre_create_data):
"sub-sequence of the master sequence.")
continue

instance_data["members"] = [selected_asset_path]
instance_data["sequence"] = selected_asset_path
instance_data["master_sequence"] = master_seq
instance_data["master_level"] = master_lvl
instance_data["output"] = seq_data.get('output')
instance_data["frameStart"] = seq_data.get('frame_range')[0]
instance_data["frameEnd"] = seq_data.get('frame_range')[1]
self.create_instance(
instance_data, subset_name, pre_create_data,
selected_asset_path, master_seq, master_lvl, seq_data)

super(CreateRender, self).create(
subset_name,
instance_data,
pre_create_data)
def create(self, subset_name, instance_data, pre_create_data):
if pre_create_data.get("create_seq"):
self.create_with_new_sequence(
subset_name, instance_data, pre_create_data)
else:
self.create_from_existing_sequence(
subset_name, instance_data, pre_create_data)

def get_pre_create_attr_defs(self):
return [
UILabelDef("Select the sequence to render.")
UILabelDef(
"Select a Level Sequence to render or create a new one."
),
BoolDef(
"create_seq",
label="Create a new Level Sequence",
default=False
),
UILabelDef(
"WARNING: If you create a new Level Sequence, the current\n"
"level will be saved and a new Master Level will be created."
),
NumberDef(
"start_frame",
label="Start Frame",
default=0,
minimum=-999999,
maximum=999999
),
NumberDef(
"end_frame",
label="Start Frame",
default=150,
minimum=-999999,
maximum=999999
),
UISeparatorDef(),
UILabelDef(
"The following settings are valid only if you are not\n"
"creating a new sequence."
),
BoolDef(
"use_hierarchy",
label="Use Hierarchy",
default=False
),
]
42 changes: 42 additions & 0 deletions openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import clique

import pyblish.api


class ValidateSequenceFrames(pyblish.api.InstancePlugin):
"""Ensure the sequence of frames is complete
The files found in the folder are checked against the frameStart and
frameEnd of the instance. If the first or last file is not
corresponding with the first or last frame it is flagged as invalid.
"""

order = pyblish.api.ValidatorOrder
label = "Validate Sequence Frames"
families = ["render"]
hosts = ["unreal"]
optional = True

def process(self, instance):
representations = instance.data.get("representations")
for repr in representations:
data = instance.data.get("assetEntity", {}).get("data", {})
patterns = [clique.PATTERNS["frames"]]
collections, remainder = clique.assemble(
repr["files"], minimum_items=1, patterns=patterns)

assert not remainder, "Must not have remainder"
assert len(collections) == 1, "Must detect single collection"
collection = collections[0]
frames = list(collection.indexes)

current_range = (frames[0], frames[-1])
required_range = (data["frameStart"],
data["frameEnd"])

if current_range != required_range:
raise ValueError(f"Invalid frame range: {current_range} - "
f"expected: {required_range}")

missing = collection.holes().indexes
assert not missing, "Missing frames: %s" % (missing,)

0 comments on commit 37cb21a

Please sign in to comment.