Skip to content

Commit 378d16e

Browse files
authored
6649 movie render queue (#2)
Cleaned up a couple of things and added the ability to render with the Movie Render Queue, if available.
1 parent 91653a1 commit 378d16e

File tree

2 files changed

+173
-18
lines changed

2 files changed

+173
-18
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,6 @@ pip-log.txt
3131

3232
# Max OS X Desktop Services Store files
3333
.DS_Store
34+
35+
# Visual Studio
36+
.vs

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

Lines changed: 170 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import subprocess
1010
import sys
1111
import datetime
12+
import tempfile
1213

1314
HookBaseClass = sgtk.get_hook_baseclass()
1415

@@ -227,7 +228,29 @@ def validate(self, settings, item):
227228
fields["MM"] = date.month
228229
fields["DD"] = date.day
229230

230-
# ensure the fields work for the publish template
231+
# Check if we can use the Movie Render queue available from 4.26
232+
use_movie_render_queue = False
233+
if "MoviePipelineQueueEngineSubsystem" in dir(unreal):
234+
if "MoviePipelineAppleProResOutput" in dir(unreal):
235+
use_movie_render_queue = True
236+
self.logger.info("Movie Render Queue will be used for rendering.")
237+
else:
238+
self.logger.info(
239+
"Apple ProRes Media plugin must be loaded to be able to render with the Movie Render Queue, "
240+
"Level Sequencer will be used for rendering."
241+
)
242+
else:
243+
self.logger.info("Movie Render Queue not available, Level Sequencer will be used for rendering.")
244+
item.properties["use_movie_render_queue"] = use_movie_render_queue
245+
# Set the UE movie extension based on the current platform and rendering engine
246+
if use_movie_render_queue:
247+
fields["ue_mov_ext"] = "mov" # mov on all platforms
248+
else:
249+
if sys.platform == "win32":
250+
fields["ue_mov_ext"] = "avi"
251+
else:
252+
fields["ue_mov_ext"] = "mov"
253+
# Ensure the fields work for the publish template
231254
missing_keys = publish_template.missing_keys(fields)
232255
if missing_keys:
233256
error_msg = "Missing keys required for the publish template " \
@@ -265,8 +288,7 @@ def publish(self, settings, item):
265288
publish_path = os.path.normpath(publish_path)
266289

267290
# Split the destination path into folder and filename
268-
destination_folder = os.path.split(publish_path)[0]
269-
movie_name = os.path.split(publish_path)[1]
291+
destination_folder, movie_name = os.path.split(publish_path)
270292
movie_name = os.path.splitext(movie_name)[0]
271293

272294
# Ensure that the destination path exists before rendering the sequence
@@ -277,7 +299,12 @@ def publish(self, settings, item):
277299
unreal_map_path = item.properties["unreal_map_path"]
278300
unreal.log("movie name: {}".format(movie_name))
279301
# Render the movie
280-
self._unreal_render_sequence_to_movie(destination_folder, unreal_map_path, unreal_asset_path, movie_name)
302+
if item.properties.get("use_movie_render_queue"):
303+
self.logger.info("Rendering %s with the Movie Render Queue." % publish_path)
304+
self._unreal_render_sequence_with_movie_queue(publish_path, unreal_map_path, unreal_asset_path)
305+
else:
306+
self.logger.info("Rendering %s with the Level Sequencer." % publish_path)
307+
self._unreal_render_sequence_with_sequencer(publish_path, unreal_map_path, unreal_asset_path)
281308

282309
# Increment the version number
283310
self._unreal_asset_set_version(unreal_asset_path, item.properties["version_number"])
@@ -405,28 +432,29 @@ def _unreal_asset_set_version(self, asset_path, version_number):
405432
for dialog in engine.created_qt_dialogs:
406433
dialog.raise_()
407434

408-
def _unreal_render_sequence_to_movie(self, destination_path, unreal_map_path, sequence_path, movie_name):
435+
def _unreal_render_sequence_with_sequencer(self, output_path, unreal_map_path, sequence_path):
409436
"""
410-
Renders a given sequence in a given level to a movie file
437+
Renders a given sequence in a given level to a movie file with the Level Sequencer.
411438
412-
:param destination_path: Destionation folder where to generate the movie file
413-
:param unreal_map_path: Path of the Unreal map in which to run the sequence
414-
:param sequence_path: Content Browser path of sequence to render
415-
:param movie_name: Filename of the movie that will be generated
439+
:param str output_path: Full path to the movie to render.
440+
:param str unreal_map_path: Path of the Unreal map in which to run the sequence.
441+
:param str sequence_path: Content Browser path of sequence to render.
416442
:returns: True if a movie file was generated, False otherwise
417443
string representing the path of the generated movie file
418444
"""
445+
output_folder, output_file = os.path.split(output_path)
446+
movie_name = os.path.splitext(output_file)[0]
447+
419448
# First, check if there's a file that will interfere with the output of the Sequencer
420449
# Sequencer can only render to avi file format
421-
output_filename = "{}.avi".format(movie_name)
422-
output_filepath = os.path.join(destination_path, output_filename)
423-
424-
if os.path.isfile(output_filepath):
450+
if os.path.isfile(output_path):
425451
# Must delete it first, otherwise the Sequencer will add a number in the filename
426452
try:
427-
os.remove(output_filepath)
453+
os.remove(output_path)
428454
except OSError:
429-
self.logger.debug("Couldn't delete {}. The Sequencer won't be able to output the movie to that file.".format(output_filepath))
455+
self.logger.error(
456+
"Couldn't delete {}. The Sequencer won't be able to output the movie to that file.".format(output_path)
457+
)
430458
return False, None
431459

432460
# Render the sequence to a movie file using the following command-line arguments
@@ -450,7 +478,7 @@ def _unreal_render_sequence_to_movie(self, destination_path, unreal_map_path, se
450478
sequence_path = "-LevelSequence={}".format(sequence_path)
451479
cmdline_args.append(sequence_path) # The sequence to render
452480

453-
output_path = '-MovieFolder="{}"'.format(destination_path)
481+
output_path = '-MovieFolder="{}"'.format(output_folder)
454482
cmdline_args.append(output_path) # output folder, must match the work template
455483

456484
movie_name_arg = "-MovieName={}".format(movie_name)
@@ -475,4 +503,128 @@ def _unreal_render_sequence_to_movie(self, destination_path, unreal_map_path, se
475503
# Send the arguments as a single string because some arguments could contain spaces and we don't want those to be quoted
476504
subprocess.call(" ".join(cmdline_args))
477505

478-
return os.path.isfile(output_filepath), output_filepath
506+
return os.path.isfile(output_path), output_path
507+
508+
def _unreal_render_sequence_with_movie_queue(self, output_path, unreal_map_path, sequence_path):
509+
"""
510+
Renders a given sequence in a given level with the Movie Render queue.
511+
512+
:param str output_path: Full path to the movie to render.
513+
:param str unreal_map_path: Path of the Unreal map in which to run the sequence.
514+
:param str sequence_path: Content Browser path of sequence to render.
515+
:returns: True if a movie file was generated, False otherwise
516+
string representing the path of the generated movie file
517+
"""
518+
output_folder, output_file = os.path.split(output_path)
519+
movie_name = os.path.splitext(output_file)[0]
520+
521+
qsub = unreal.MoviePipelineQueueEngineSubsystem()
522+
queue = qsub.get_queue()
523+
job = queue.allocate_new_job(unreal.MoviePipelineExecutorJob)
524+
job.sequence = unreal.SoftObjectPath(sequence_path)
525+
job.map = unreal.SoftObjectPath(unreal_map_path)
526+
# Set settings
527+
config = job.get_configuration()
528+
# https://docs.unrealengine.com/4.26/en-US/PythonAPI/class/MoviePipelineOutputSetting.html?highlight=setting#unreal.MoviePipelineOutputSetting
529+
output_setting = config.find_or_add_setting_by_class(unreal.MoviePipelineOutputSetting)
530+
output_setting.output_directory = unreal.DirectoryPath(output_folder)
531+
output_setting.output_resolution = unreal.IntPoint(1280, 720)
532+
output_setting.file_name_format = movie_name
533+
output_setting.override_existing_output = True # Overwrite existing files
534+
# Render to a movie
535+
config.find_or_add_setting_by_class(unreal.MoviePipelineAppleProResOutput)
536+
# TODO: check which codec we should use.
537+
# Default rendering
538+
config.find_or_add_setting_by_class(unreal.MoviePipelineDeferredPassBase)
539+
# Additional pass with detailed lighting?
540+
# render_pass = config.find_or_add_setting_by_class(unreal.MoviePipelineDeferredPass_DetailLighting)
541+
542+
# We render in a forked process that we can control.
543+
# It would be possible to render in from the running process using an
544+
# Executor, however it seems to sometimes deadlock if we don't let Unreal
545+
# process its internal events, rendering is asynchronous and being notified
546+
# when the render completed does not seem to be reliable.
547+
# Sample code:
548+
# exc = unreal.MoviePipelinePIEExecutor()
549+
# # If needed, we can store data in exc.user_data
550+
# # In theory we can set a callback to be notified about completion
551+
# def _on_movie_render_finished_cb(executor, result):
552+
# print("Executor %s finished with %s" % (executor, result))
553+
# # exc.on_executor_finished_delegate.add_callable(_on_movie_render_finished_cb)
554+
# r = qsub.render_queue_with_executor_instance(exc)
555+
556+
# We can't control the name of the manifest file, so we save and then rename the file.
557+
_, manifest_path = unreal.MoviePipelineEditorLibrary.save_queue_to_manifest_file(queue)
558+
manifest_path = os.path.abspath(manifest_path)
559+
manifest_dir, manifest_file = os.path.split(manifest_path)
560+
f, new_path = tempfile.mkstemp(
561+
suffix=os.path.splitext(manifest_file)[1],
562+
dir=manifest_dir
563+
)
564+
os.close(f)
565+
os.replace(manifest_path, new_path)
566+
567+
self.logger.debug("Queue manifest saved in %s" % new_path)
568+
# We now need a path local to the unreal project "Saved" folder.
569+
manifest_path = new_path.replace(
570+
"%s%s" % (
571+
os.path.abspath(
572+
os.path.join(unreal.SystemLibrary.get_project_directory(), "Saved")
573+
),
574+
os.path.sep,
575+
),
576+
"",
577+
)
578+
self.logger.debug("Manifest short path: %s" % manifest_path)
579+
# Command line parameters were retrieved by submitting a queue in Unreal Editor with
580+
# a MoviePipelineNewProcessExecutor executor.
581+
# https://docs.unrealengine.com/4.27/en-US/PythonAPI/class/MoviePipelineNewProcessExecutor.html?highlight=executor
582+
cmd_args = [
583+
sys.executable,
584+
"%s" % os.path.join(
585+
unreal.SystemLibrary.get_project_directory(),
586+
"%s.uproject" % unreal.SystemLibrary.get_game_name(),
587+
),
588+
"MoviePipelineEntryMap?game=/Script/MovieRenderPipelineCore.MoviePipelineGameMode",
589+
"-game",
590+
"-Multiprocess",
591+
"-NoLoadingScreen",
592+
"-FixedSeed",
593+
"-log",
594+
"-Unattended",
595+
"-messaging",
596+
"-SessionName=\"Publish2 Movie Render\"",
597+
"-nohmd",
598+
"-windowed",
599+
"-ResX=1280",
600+
"-ResY=720",
601+
# TODO: check what these settings are
602+
"-dpcvars=%s" % ",".join([
603+
"sg.ViewDistanceQuality=4",
604+
"sg.AntiAliasingQuality=4",
605+
"sg.ShadowQuality=4",
606+
"sg.PostProcessQuality=4",
607+
"sg.TextureQuality=4",
608+
"sg.EffectsQuality=4",
609+
"sg.FoliageQuality=4",
610+
"sg.ShadingQuality=4",
611+
"r.TextureStreaming=0",
612+
"r.ForceLOD=0",
613+
"r.SkeletalMeshLODBias=-10",
614+
"r.ParticleLODBias=-10",
615+
"foliage.DitheredLOD=0",
616+
"foliage.ForceLOD=0",
617+
"r.Shadow.DistanceScale=10",
618+
"r.ShadowQuality=5",
619+
"r.Shadow.RadiusThreshold=0.001000",
620+
"r.ViewDistanceScale=50",
621+
"r.D3D12.GPUTimeout=0",
622+
"a.URO.Enable=0",
623+
]),
624+
"-execcmds=r.HLOD 0",
625+
# This need to be a path relative the to the Unreal project "Saved" folder.
626+
"-MoviePipelineConfig=\"%s\"" % manifest_path,
627+
]
628+
self.logger.info("Running %s" % cmd_args)
629+
subprocess.call(cmd_args)
630+
return os.path.isfile(output_path), output_path

0 commit comments

Comments
 (0)