[Automated] Merged develop into main

This commit is contained in:
ynbot 2023-04-24 17:41:41 +02:00 committed by GitHub
commit 9b4ad20dfd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 469 additions and 140 deletions

View file

@ -190,7 +190,7 @@ class CollectNukeWrites(pyblish.api.InstancePlugin,
# make sure rendered sequence on farm will
# be used for extract review
if not instance.data["review"]:
if not instance.data.get("review"):
instance.data["useSequenceForReview"] = False
self.log.debug("instance.data: {}".format(pformat(instance.data)))

View file

@ -2,8 +2,10 @@ import os
import unreal
from openpype.settings import get_project_settings
from openpype.pipeline import Anatomy
from openpype.hosts.unreal.api import pipeline
from openpype.widgets.message_window import Window
queue = None
@ -32,11 +34,20 @@ def start_rendering():
"""
Start the rendering process.
"""
print("Starting rendering...")
unreal.log("Starting 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
@ -66,6 +77,13 @@ def start_rendering():
ar = unreal.AssetRegistryHelpers.get_asset_registry()
data = get_project_settings(project)
config = None
config_path = str(data.get("unreal").get("render_config_path"))
if config_path and unreal.EditorAssetLibrary.does_asset_exist(config_path):
unreal.log("Found saved render configuration")
config = ar.get_asset_by_object_path(config_path).get_asset()
for i in inst_data:
sequence = ar.get_asset_by_object_path(i["sequence"]).get_asset()
@ -81,55 +99,80 @@ def start_rendering():
# Get all the sequences to render. If there are subsequences,
# add them and their frame ranges to the render list. We also
# use the names for the output paths.
for s in sequences:
subscenes = pipeline.get_subsequences(s.get('sequence'))
for seq in sequences:
subscenes = pipeline.get_subsequences(seq.get('sequence'))
if subscenes:
for ss in subscenes:
for sub_seq in subscenes:
sequences.append({
"sequence": ss.get_sequence(),
"output": (f"{s.get('output')}/"
f"{ss.get_sequence().get_name()}"),
"sequence": sub_seq.get_sequence(),
"output": (f"{seq.get('output')}/"
f"{sub_seq.get_sequence().get_name()}"),
"frame_range": (
ss.get_start_frame(), ss.get_end_frame())
sub_seq.get_start_frame(), sub_seq.get_end_frame())
})
else:
# Avoid rendering camera sequences
if "_camera" not in s.get('sequence').get_name():
render_list.append(s)
if "_camera" not in seq.get('sequence').get_name():
render_list.append(seq)
# Create the rendering jobs and add them to the queue.
for r in render_list:
for render_setting in render_list:
job = queue.allocate_new_job(unreal.MoviePipelineExecutorJob)
job.sequence = unreal.SoftObjectPath(i["master_sequence"])
job.map = unreal.SoftObjectPath(i["master_level"])
job.author = "OpenPype"
# If we have a saved configuration, copy it to the job.
if config:
job.get_configuration().copy_from(config)
# User data could be used to pass data to the job, that can be
# read in the job's OnJobFinished callback. We could,
# for instance, pass the AvalonPublishInstance's path to the job.
# job.user_data = ""
output_dir = render_setting.get('output')
shot_name = render_setting.get('sequence').get_name()
settings = job.get_configuration().find_or_add_setting_by_class(
unreal.MoviePipelineOutputSetting)
settings.output_resolution = unreal.IntPoint(1920, 1080)
settings.custom_start_frame = r.get("frame_range")[0]
settings.custom_end_frame = r.get("frame_range")[1]
settings.custom_start_frame = render_setting.get("frame_range")[0]
settings.custom_end_frame = render_setting.get("frame_range")[1]
settings.use_custom_playback_range = True
settings.file_name_format = "{sequence_name}.{frame_number}"
settings.output_directory.path = f"{render_dir}/{r.get('output')}"
renderPass = job.get_configuration().find_or_add_setting_by_class(
unreal.MoviePipelineDeferredPassBase)
renderPass.disable_multisample_effects = True
settings.file_name_format = f"{shot_name}" + ".{frame_number}"
settings.output_directory.path = f"{render_dir}/{output_dir}"
job.get_configuration().find_or_add_setting_by_class(
unreal.MoviePipelineImageSequenceOutput_PNG)
unreal.MoviePipelineDeferredPassBase)
render_format = data.get("unreal").get("render_format", "png")
if render_format == "png":
job.get_configuration().find_or_add_setting_by_class(
unreal.MoviePipelineImageSequenceOutput_PNG)
elif render_format == "exr":
job.get_configuration().find_or_add_setting_by_class(
unreal.MoviePipelineImageSequenceOutput_EXR)
elif render_format == "jpg":
job.get_configuration().find_or_add_setting_by_class(
unreal.MoviePipelineImageSequenceOutput_JPG)
elif render_format == "bmp":
job.get_configuration().find_or_add_setting_by_class(
unreal.MoviePipelineImageSequenceOutput_BMP)
# If there are jobs in the queue, start the rendering process.
if queue.get_jobs():
global executor
executor = unreal.MoviePipelinePIEExecutor()
preroll_frames = data.get("unreal").get("preroll_frames", 0)
settings = unreal.MoviePipelinePIEExecutorSettings()
settings.set_editor_property(
"initial_delay_frame_count", preroll_frames)
executor.on_executor_finished_delegate.add_callable_unique(
_queue_finish_callback)
executor.on_individual_job_finished_delegate.add_callable_unique(

View file

@ -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):
@ -19,7 +27,92 @@ 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()
@ -27,8 +120,8 @@ class CreateRender(UnrealAssetCreator):
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
@ -42,28 +135,38 @@ class CreateRender(UnrealAssetCreator):
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.
@ -79,7 +182,8 @@ class CreateRender(UnrealAssetCreator):
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]
@ -119,20 +223,54 @@ class CreateRender(UnrealAssetCreator):
"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
),
]

View file

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

View file

@ -49,7 +49,12 @@ class ValidateSequenceFrames(pyblish.api.InstancePlugin):
collection = collections[0]
frames = list(collection.indexes)
if instance.data.get("slate"):
# Slate is not part of the frame range
frames = frames[1:]
current_range = (frames[0], frames[-1])
required_range = (instance.data["frameStart"],
instance.data["frameEnd"])

View file

@ -11,6 +11,9 @@
},
"level_sequences_for_layouts": false,
"delete_unmatched_assets": false,
"render_config_path": "",
"preroll_frames": 0,
"render_format": "png",
"project_setup": {
"dev_mode": true
}

View file

@ -32,6 +32,28 @@
"key": "delete_unmatched_assets",
"label": "Delete assets that are not matched"
},
{
"type": "text",
"key": "render_config_path",
"label": "Render Config Path"
},
{
"type": "number",
"key": "preroll_frames",
"label": "Pre-roll frames"
},
{
"key": "render_format",
"label": "Render format",
"type": "enum",
"multiselection": false,
"enum_items": [
{"png": "PNG"},
{"exr": "EXR"},
{"jpg": "JPG"},
{"bmp": "BMP"}
]
},
{
"type": "dict",
"collapsible": true,

View file

@ -282,6 +282,9 @@ class CreateWidget(QtWidgets.QWidget):
thumbnail_widget.thumbnail_created.connect(self._on_thumbnail_create)
thumbnail_widget.thumbnail_cleared.connect(self._on_thumbnail_clear)
controller.event_system.add_callback(
"main.window.closed", self._on_main_window_close
)
controller.event_system.add_callback(
"plugins.refresh.finished", self._on_plugins_refresh
)
@ -316,6 +319,10 @@ class CreateWidget(QtWidgets.QWidget):
self._first_show = True
self._last_thumbnail_path = None
self._last_current_context_asset = None
self._last_current_context_task = None
self._use_current_context = True
@property
def current_asset_name(self):
return self._controller.current_asset_name
@ -356,12 +363,39 @@ class CreateWidget(QtWidgets.QWidget):
if check_prereq:
self._invalidate_prereq()
def _on_main_window_close(self):
"""Publisher window was closed."""
# Use current context on next refresh
self._use_current_context = True
def refresh(self):
current_asset_name = self._controller.current_asset_name
current_task_name = self._controller.current_task_name
# Get context before refresh to keep selection of asset and
# task widgets
asset_name = self._get_asset_name()
task_name = self._get_task_name()
# Replace by current context if last loaded context was
# 'current context' before reset
if (
self._use_current_context
or (
self._last_current_context_asset
and asset_name == self._last_current_context_asset
and task_name == self._last_current_context_task
)
):
asset_name = current_asset_name
task_name = current_task_name
# Store values for future refresh
self._last_current_context_asset = current_asset_name
self._last_current_context_task = current_task_name
self._use_current_context = False
self._prereq_available = False
# Disable context widget so refresh of asset will use context asset
@ -398,7 +432,10 @@ class CreateWidget(QtWidgets.QWidget):
prereq_available = False
creator_btn_tooltips.append("Creator is not selected")
if self._context_change_is_enabled() and self._asset_name is None:
if (
self._context_change_is_enabled()
and self._get_asset_name() is None
):
# QUESTION how to handle invalid asset?
prereq_available = False
creator_btn_tooltips.append("Context is not selected")

View file

@ -406,6 +406,9 @@ class PublisherWindow(QtWidgets.QDialog):
self._comment_input.setText("") # clear comment
self._reset_on_show = True
self._controller.clear_thumbnail_temp_dir_path()
# Trigger custom event that should be captured only in UI
# - backend (controller) must not be dependent on this event topic!!!
self._controller.event_system.emit("main.window.closed", {}, "window")
super(PublisherWindow, self).closeEvent(event)
def leaveEvent(self, event):

View file

@ -199,90 +199,103 @@ class InventoryModel(TreeModel):
"""Refresh the model"""
host = registered_host()
if not items: # for debugging or testing, injecting items from outside
# for debugging or testing, injecting items from outside
if items is None:
if isinstance(host, ILoadHost):
items = host.get_containers()
else:
elif hasattr(host, "ls"):
items = host.ls()
else:
items = []
self.clear()
if self._hierarchy_view and selected:
if not hasattr(host.pipeline, "update_hierarchy"):
# If host doesn't support hierarchical containers, then
# cherry-pick only.
self.add_items((item for item in items
if item["objectName"] in selected))
return
# Update hierarchy info for all containers
items_by_name = {item["objectName"]: item
for item in host.pipeline.update_hierarchy(items)}
selected_items = set()
def walk_children(names):
"""Select containers and extend to chlid containers"""
for name in [n for n in names if n not in selected_items]:
selected_items.add(name)
item = items_by_name[name]
yield item
for child in walk_children(item["children"]):
yield child
items = list(walk_children(selected)) # Cherry-picked and extended
# Cut unselected upstream containers
for item in items:
if not item.get("parent") in selected_items:
# Parent not in selection, this is root item.
item["parent"] = None
parents = [self._root_item]
# The length of `items` array is the maximum depth that a
# hierarchy could be.
# Take this as an easiest way to prevent looping forever.
maximum_loop = len(items)
count = 0
while items:
if count > maximum_loop:
self.log.warning("Maximum loop count reached, possible "
"missing parent node.")
break
_parents = list()
for parent in parents:
_unparented = list()
def _children():
"""Child item provider"""
for item in items:
if item.get("parent") == parent.get("objectName"):
# (NOTE)
# Since `self._root_node` has no "objectName"
# entry, it will be paired with root item if
# the value of key "parent" is None, or not
# having the key.
yield item
else:
# Not current parent's child, try next
_unparented.append(item)
self.add_items(_children(), parent)
items[:] = _unparented
# Parents of next level
for group_node in parent.children():
_parents += group_node.children()
parents[:] = _parents
count += 1
else:
if not selected or not self._hierarchy_view:
self.add_items(items)
return
if (
not hasattr(host, "pipeline")
or not hasattr(host.pipeline, "update_hierarchy")
):
# If host doesn't support hierarchical containers, then
# cherry-pick only.
self.add_items((
item
for item in items
if item["objectName"] in selected
))
return
# TODO find out what this part does. Function 'update_hierarchy' is
# available only in 'blender' at this moment.
# Update hierarchy info for all containers
items_by_name = {
item["objectName"]: item
for item in host.pipeline.update_hierarchy(items)
}
selected_items = set()
def walk_children(names):
"""Select containers and extend to chlid containers"""
for name in [n for n in names if n not in selected_items]:
selected_items.add(name)
item = items_by_name[name]
yield item
for child in walk_children(item["children"]):
yield child
items = list(walk_children(selected)) # Cherry-picked and extended
# Cut unselected upstream containers
for item in items:
if not item.get("parent") in selected_items:
# Parent not in selection, this is root item.
item["parent"] = None
parents = [self._root_item]
# The length of `items` array is the maximum depth that a
# hierarchy could be.
# Take this as an easiest way to prevent looping forever.
maximum_loop = len(items)
count = 0
while items:
if count > maximum_loop:
self.log.warning("Maximum loop count reached, possible "
"missing parent node.")
break
_parents = list()
for parent in parents:
_unparented = list()
def _children():
"""Child item provider"""
for item in items:
if item.get("parent") == parent.get("objectName"):
# (NOTE)
# Since `self._root_node` has no "objectName"
# entry, it will be paired with root item if
# the value of key "parent" is None, or not
# having the key.
yield item
else:
# Not current parent's child, try next
_unparented.append(item)
self.add_items(_children(), parent)
items[:] = _unparented
# Parents of next level
for group_node in parent.children():
_parents += group_node.children()
parents[:] = _parents
count += 1
def add_items(self, items, parent=None):
"""Add the items to the model.

View file

@ -107,8 +107,8 @@ class SceneInventoryWindow(QtWidgets.QDialog):
view.hierarchy_view_changed.connect(
self._on_hierarchy_view_change
)
view.data_changed.connect(self.refresh)
refresh_button.clicked.connect(self.refresh)
view.data_changed.connect(self._on_refresh_request)
refresh_button.clicked.connect(self._on_refresh_request)
update_all_button.clicked.connect(self._on_update_all)
self._update_all_button = update_all_button
@ -139,6 +139,11 @@ class SceneInventoryWindow(QtWidgets.QDialog):
"""
def _on_refresh_request(self):
"""Signal callback to trigger 'refresh' without any arguments."""
self.refresh()
def refresh(self, items=None):
with preserve_expanded_rows(
tree_view=self._view,

View file

@ -180,5 +180,23 @@ class TestValidateSequenceFrames(BaseTest):
plugin.process(instance)
assert ("Missing frames: [1002]" in str(excinfo.value))
def test_validate_sequence_frames_slate(self, instance, plugin):
representations = [
{
"ext": "exr",
"files": [
"Main_beauty.1000.exr",
"Main_beauty.1001.exr",
"Main_beauty.1002.exr",
"Main_beauty.1003.exr"
]
}
]
instance.data["slate"] = True
instance.data["representations"] = representations
instance.data["frameEnd"] = 1003
plugin.process(instance)
test_case = TestValidateSequenceFrames()