Skip to content

Commit 87ce7b4

Browse files
authored
7669 sub sequences (#13)
Render shots which are a sub-sequence of another sequence in the context of the parent sequence to ensure frame ranges and other values which are part of the edit are accurate. Propose to render shots from all the sequences they are part of.
1 parent 9cd4d3a commit 87ce7b4

File tree

2 files changed

+215
-18
lines changed

2 files changed

+215
-18
lines changed

hooks/tk-multi-publish2/basic/collector.py

Lines changed: 166 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,18 @@
22
# This file has been modified by Epic Games, Inc. and is subject to the license
33
# file included in this repository.
44

5-
import sgtk
5+
from collections import namedtuple, defaultdict
6+
import copy
67
import os
78

9+
import unreal
10+
11+
import sgtk
12+
13+
# A named tuple to store LevelSequence edits: the sequence/track/section
14+
# the edit is in.
15+
SequenceEdit = namedtuple("SequenceEdit", ["sequence", "track", "section"])
16+
817

918
HookBaseClass = sgtk.get_hook_baseclass()
1019

@@ -129,26 +138,170 @@ def collect_current_session(self, settings, parent_item):
129138

130139
return session_item
131140

141+
def create_asset_item(self, parent_item, asset_path, asset_type, asset_name, display_name=None):
142+
"""
143+
Create an unreal item under the given parent item.
144+
145+
:param asset_path: The unreal asset path, as a string.
146+
:param asset_type: The unreal asset type, as a string.
147+
:param asset_name: The unreal asset name, as a string.
148+
:param display_name: Optional display name for the item.
149+
:returns: The created item.
150+
"""
151+
item_type = "unreal.asset.%s" % asset_type
152+
asset_item = parent_item.create_item(
153+
item_type, # Include the asset type for the publish plugin to use
154+
asset_type, # Display type
155+
display_name or asset_name, # Display name of item instance
156+
)
157+
158+
# Set asset properties which can be used by publish plugins
159+
asset_item.properties["asset_path"] = asset_path
160+
asset_item.properties["asset_name"] = asset_name
161+
asset_item.properties["asset_type"] = asset_type
162+
return asset_item
163+
132164
def collect_selected_assets(self, parent_item):
133165
"""
134166
Creates items for assets selected in Unreal.
135167
136168
:param parent_item: Parent Item instance
137169
"""
138170
unreal_sg = sgtk.platform.current_engine().unreal_sg_engine
171+
sequence_edits = None
139172
# Iterate through the selected assets and get their info and add them as items to be published
140173
for asset in unreal_sg.selected_assets:
141-
asset_name = str(asset.asset_name)
142-
asset_type = str(asset.asset_class)
143-
144-
item_type = "unreal.asset." + asset_type
145-
asset_item = parent_item.create_item(
146-
item_type, # Include the asset type for the publish plugin to use
147-
asset_type, # display type
148-
asset_name # display name of item instance
174+
if asset.asset_class == "LevelSequence":
175+
if sequence_edits is None:
176+
sequence_edits = self.retrieve_sequence_edits()
177+
self.collect_level_sequence(parent_item, asset, sequence_edits)
178+
else:
179+
self.create_asset_item(
180+
parent_item,
181+
# :class:`Name` instances, we cast them to strings otherwise
182+
# string operations fail down the line..
183+
"%s" % asset.object_path,
184+
"%s" % asset.asset_class,
185+
"%s" % asset.asset_name,
186+
)
187+
188+
def get_all_paths_from_sequence(self, level_sequence, sequence_edits, visited=None):
189+
"""
190+
Retrieve all edit paths from the given Level Sequence to top Level Sequences.
191+
192+
Recursively explore the sequence edits, stop the recursion when a Level
193+
Sequence which is not a sub-sequence of another is reached.
194+
195+
Lists of Level Sequences are returned, where each list contains all the
196+
the Level Sequences to traverse to reach the top Level Sequence from the
197+
starting Level Sequence.
198+
199+
For example if a master Level Sequence contains some `Seq_<seq number>`
200+
sequences and each of them contains shots like `Shot_<seq number>_<shot number>`,
201+
a path for Shot_001_010 would be `[Shot_001_010, Seq_001, Master sequence]`.
202+
203+
If an alternate Cut is maintained with another master level Sequence, both
204+
paths would be detected and returned by this method, e.g.
205+
`[[Shot_001_010, Seq_001, Master sequence], [Shot_001_010, Seq_001, Master sequence 2]]`
206+
207+
Maintain a list of visited Level Sequences to detect cycles.
208+
209+
:param level_sequence: A :class:`unreal.LevelSequence` instance.
210+
:param sequence_edits: A dictionary with :class:`unreal.LevelSequence as keys and
211+
lists of :class:`SequenceEdit` as values.
212+
:param visited: A list of :class:`unreal.LevelSequence` instances, populated
213+
as nodes are visited.
214+
:returns: A list of lists of Level Sequences.
215+
"""
216+
if not visited:
217+
visited = []
218+
visited.append(level_sequence)
219+
self.logger.info("Treating %s" % level_sequence.get_name())
220+
if not sequence_edits[level_sequence]:
221+
# No parent, return a list with a single entry with the current
222+
# sequence
223+
return [[level_sequence]]
224+
225+
all_paths = []
226+
# Loop over parents get all paths starting from them
227+
for edit in sequence_edits[level_sequence]:
228+
if edit.sequence in visited:
229+
self.logger.warning(
230+
"Detected a cycle in edits path %s to %s" % (
231+
"->".join(visited), edit.sequence
232+
)
233+
)
234+
else:
235+
# Get paths from the parent and prepend the current sequence
236+
# to them.
237+
for edit_path in self.get_all_paths_from_sequence(
238+
edit.sequence,
239+
sequence_edits,
240+
copy.copy(visited), # Each visit needs its own stack
241+
):
242+
self.logger.info("Got %s from %s" % (edit_path, edit.sequence.get_name()))
243+
all_paths.append([level_sequence] + edit_path)
244+
return all_paths
245+
246+
def collect_level_sequence(self, parent_item, asset, sequence_edits):
247+
"""
248+
Collect the items for the given Level Sequence asset.
249+
250+
Multiple items can be collected for a given Level Sequence if it appears
251+
in multiple edits.
252+
253+
:param parent_item: Parent Item instance.
254+
:param asset: An Unreal LevelSequence asset.
255+
:param sequence_edits: A dictionary with :class:`unreal.LevelSequence as keys and
256+
lists of :class:`SequenceEdit` as values.
257+
"""
258+
level_sequence = unreal.load_asset(asset.object_path)
259+
for edits_path in self.get_all_paths_from_sequence(level_sequence, sequence_edits):
260+
# Reverse the path to have it from top master sequence to the shot.
261+
edits_path.reverse()
262+
self.logger.info("Collected %s" % [x.get_name() for x in edits_path])
263+
if len(edits_path) > 1:
264+
display_name = "%s (%s)" % (edits_path[0].get_name(), edits_path[-1].get_name())
265+
else:
266+
display_name = edits_path[0].get_name()
267+
item = self.create_asset_item(
268+
parent_item,
269+
edits_path[0].get_path_name(),
270+
"LevelSequence",
271+
edits_path[0].get_name(),
272+
display_name,
149273
)
274+
# Store the edits on the item so we can leverage them later when
275+
# publishing.
276+
item.properties["edits_path"] = edits_path
150277

151-
# Asset properties that can be used by publish plugins
152-
asset_item.properties["asset_path"] = asset.object_path
153-
asset_item.properties["asset_name"] = asset_name
154-
asset_item.properties["asset_type"] = asset_type
278+
def retrieve_sequence_edits(self):
279+
"""
280+
Build a dictionary for all Level Sequences where keys are Level Sequences
281+
and values the list of edits they are in.
282+
283+
:returns: A dictionary of :class:`unreal.LevelSequence` where values are
284+
lists of :class:`SequenceEdit`.
285+
"""
286+
sequence_edits = defaultdict(list)
287+
288+
asset_helper = unreal.AssetRegistryHelpers.get_asset_registry()
289+
# Retrieve all Level Sequence assets
290+
all_level_sequences = asset_helper.get_assets_by_class("LevelSequence")
291+
for lvseq_asset in all_level_sequences:
292+
lvseq = unreal.load_asset(lvseq_asset.object_path, unreal.LevelSequence)
293+
# Check shots
294+
for track in lvseq.find_master_tracks_by_type(unreal.MovieSceneCinematicShotTrack):
295+
for section in track.get_sections():
296+
# Not sure if you can have anything else than a MovieSceneSubSection
297+
# in a MovieSceneCinematicShotTrack, but let's be cautious here.
298+
try:
299+
# Get the Sequence attached to the section and check if
300+
# it is the one we're looking for.
301+
section_seq = section.get_sequence()
302+
sequence_edits[section_seq].append(
303+
SequenceEdit(lvseq, track, section)
304+
)
305+
except AttributeError:
306+
pass
307+
return sequence_edits

hooks/tk-multi-publish2/basic/publish_movie.py

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,20 @@ def validate(self, settings, item):
383383
if not asset_path or not asset_name:
384384
self.logger.debug("Sequence path or name not configured.")
385385
return False
386+
# Retrieve the Level Sequences sections tree for this Level Sequence.
387+
# This is needed to get frame ranges in the "edit" context.
388+
edits_path = item.properties.get("edits_path")
389+
if not edits_path:
390+
self.logger.debug("Edits path not configured.")
391+
return False
386392

393+
self.logger.info("Edits path %s" % edits_path)
394+
item.properties["unreal_master_sequence"] = edits_path[0]
395+
item.properties["unreal_shot"] = ".".join([lseq.get_name() for lseq in edits_path[1:]])
396+
self.logger.info("Master sequence %s, shot %s" % (
397+
item.properties["unreal_master_sequence"].get_name(),
398+
item.properties["unreal_shot"] or "all shots",
399+
))
387400
# Get the configured publish template
388401
publish_template = item.properties["publish_template"]
389402

@@ -415,10 +428,14 @@ def validate(self, settings, item):
415428
self.logger.debug("Current map must be saved first.")
416429
return False
417430

418-
# Add the map name and level sequence to fields
431+
# Add the map name to fields
419432
world_name = unreal_map.get_name()
433+
# Add the Level Sequence to fields, with the shot if any
420434
fields["ue_world"] = world_name
421-
fields["ue_level_sequence"] = asset_name
435+
if len(edits_path) > 1:
436+
fields["ue_level_sequence"] = "%s_%s" % (edits_path[0].get_name(), edits_path[-1].get_name())
437+
else:
438+
fields["ue_level_sequence"] = edits_path[0].get_name()
422439

423440
# Stash the level sequence and map paths in properties for the render
424441
item.properties["unreal_asset_path"] = asset_path
@@ -453,8 +470,12 @@ def validate(self, settings, item):
453470
"Apple ProRes Media plugin must be loaded to be able to render with the Movie Render Queue, "
454471
"Level Sequencer will be used for rendering."
455472
)
456-
else:
473+
474+
if not use_movie_render_queue:
475+
if item.properties["unreal_shot"]:
476+
raise ValueError("Rendering invidual shots for a sequence is only supported with the Movie Render Queue.")
457477
self.logger.info("Movie Render Queue not available, Level Sequencer will be used for rendering.")
478+
458479
item.properties["use_movie_render_queue"] = use_movie_render_queue
459480
item.properties["movie_render_queue_presets"] = render_presets
460481
# Set the UE movie extension based on the current platform and rendering engine
@@ -554,7 +575,8 @@ def publish(self, settings, item):
554575
publish_path,
555576
unreal_map_path,
556577
unreal_asset_path,
557-
presets
578+
presets,
579+
item.properties.get("unreal_shot") or None,
558580
)
559581
else:
560582
self.logger.info("Rendering %s with the Level Sequencer." % publish_path)
@@ -763,16 +785,19 @@ def _unreal_render_sequence_with_sequencer(self, output_path, unreal_map_path, s
763785

764786
return os.path.isfile(output_path), output_path
765787

766-
def _unreal_render_sequence_with_movie_queue(self, output_path, unreal_map_path, sequence_path, presets=None):
788+
def _unreal_render_sequence_with_movie_queue(self, output_path, unreal_map_path, sequence_path, presets=None, shot_name=None):
767789
"""
768790
Renders a given sequence in a given level with the Movie Render queue.
769791
770792
:param str output_path: Full path to the movie to render.
771793
:param str unreal_map_path: Path of the Unreal map in which to run the sequence.
772794
:param str sequence_path: Content Browser path of sequence to render.
773795
:param presets: Optional :class:`unreal.MoviePipelineMasterConfig` instance to use for renderig.
796+
:param str shot_name: Optional shot name to render a single shot from this sequence.
774797
:returns: True if a movie file was generated, False otherwise
775798
string representing the path of the generated movie file
799+
:raises ValueError: If a shot name is specified but can't be found in
800+
the sequence.
776801
"""
777802
output_folder, output_file = os.path.split(output_path)
778803
movie_name = os.path.splitext(output_file)[0]
@@ -782,6 +807,19 @@ def _unreal_render_sequence_with_movie_queue(self, output_path, unreal_map_path,
782807
job = queue.allocate_new_job(unreal.MoviePipelineExecutorJob)
783808
job.sequence = unreal.SoftObjectPath(sequence_path)
784809
job.map = unreal.SoftObjectPath(unreal_map_path)
810+
# If a specific shot was given, disable all the others.
811+
if shot_name:
812+
shot_found = False
813+
for shot in job.shot_info:
814+
if shot.outer_name != shot_name:
815+
self.logger.info("Disabling shot %s" % shot.outer_name)
816+
shot.enabled = False
817+
else:
818+
shot_found = True
819+
if not shot_found:
820+
raise ValueError(
821+
"Unable to find shot %s in sequence %s, aborting..." % (shot_name, sequence_path)
822+
)
785823
# Set settings from presets, if any
786824
if presets:
787825
job.set_preset_origin(presets)
@@ -823,6 +861,7 @@ def _unreal_render_sequence_with_movie_queue(self, output_path, unreal_map_path,
823861

824862
# We can't control the name of the manifest file, so we save and then rename the file.
825863
_, manifest_path = unreal.MoviePipelineEditorLibrary.save_queue_to_manifest_file(queue)
864+
826865
manifest_path = os.path.abspath(manifest_path)
827866
manifest_dir, manifest_file = os.path.split(manifest_path)
828867
f, new_path = tempfile.mkstemp(
@@ -893,6 +932,11 @@ def _unreal_render_sequence_with_movie_queue(self, output_path, unreal_map_path,
893932
# This need to be a path relative the to the Unreal project "Saved" folder.
894933
"-MoviePipelineConfig=\"%s\"" % manifest_path,
895934
]
935+
unreal.log(
936+
"Movie Queue command-line arguments: {}".format(
937+
" ".join(cmd_args)
938+
)
939+
)
896940
# Make a shallow copy of the current environment and clear some variables
897941
run_env = copy.copy(os.environ)
898942
# Prevent SG TK to try to bootstrap in the new process

0 commit comments

Comments
 (0)