9
9
import subprocess
10
10
import sys
11
11
import datetime
12
+ import tempfile
12
13
13
14
HookBaseClass = sgtk .get_hook_baseclass ()
14
15
@@ -227,7 +228,29 @@ def validate(self, settings, item):
227
228
fields ["MM" ] = date .month
228
229
fields ["DD" ] = date .day
229
230
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
231
254
missing_keys = publish_template .missing_keys (fields )
232
255
if missing_keys :
233
256
error_msg = "Missing keys required for the publish template " \
@@ -265,8 +288,7 @@ def publish(self, settings, item):
265
288
publish_path = os .path .normpath (publish_path )
266
289
267
290
# 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 )
270
292
movie_name = os .path .splitext (movie_name )[0 ]
271
293
272
294
# Ensure that the destination path exists before rendering the sequence
@@ -277,7 +299,12 @@ def publish(self, settings, item):
277
299
unreal_map_path = item .properties ["unreal_map_path" ]
278
300
unreal .log ("movie name: {}" .format (movie_name ))
279
301
# 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 )
281
308
282
309
# Increment the version number
283
310
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):
405
432
for dialog in engine .created_qt_dialogs :
406
433
dialog .raise_ ()
407
434
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 ):
409
436
"""
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.
411
438
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.
416
442
:returns: True if a movie file was generated, False otherwise
417
443
string representing the path of the generated movie file
418
444
"""
445
+ output_folder , output_file = os .path .split (output_path )
446
+ movie_name = os .path .splitext (output_file )[0 ]
447
+
419
448
# First, check if there's a file that will interfere with the output of the Sequencer
420
449
# 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 ):
425
451
# Must delete it first, otherwise the Sequencer will add a number in the filename
426
452
try :
427
- os .remove (output_filepath )
453
+ os .remove (output_path )
428
454
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
+ )
430
458
return False , None
431
459
432
460
# 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
450
478
sequence_path = "-LevelSequence={}" .format (sequence_path )
451
479
cmdline_args .append (sequence_path ) # The sequence to render
452
480
453
- output_path = '-MovieFolder="{}"' .format (destination_path )
481
+ output_path = '-MovieFolder="{}"' .format (output_folder )
454
482
cmdline_args .append (output_path ) # output folder, must match the work template
455
483
456
484
movie_name_arg = "-MovieName={}" .format (movie_name )
@@ -475,4 +503,128 @@ def _unreal_render_sequence_to_movie(self, destination_path, unreal_map_path, se
475
503
# Send the arguments as a single string because some arguments could contain spaces and we don't want those to be quoted
476
504
subprocess .call (" " .join (cmdline_args ))
477
505
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