Skip to content

Commit 8b7a9e3

Browse files
authored
Merge pull request #7 from GPLgithub/master
7669 sub sequences (#13)
2 parents 7b7b1e3 + 87ce7b4 commit 8b7a9e3

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)