I am interested in generating a movie pipeline queue dynamically from python. I was able to figure out all the necessary pieces. However, when using the executor on the queue it will not execute. I’ve tried 3 different executors.
This executor Doesn’t have any error. However, it never actually renders the movie.
executor=unreal.MoviePipelineInProcessExecutor()
The following two complain that the map associated with the job has not been saved. However, made sure it is saved. I have also tried with the target map loaded and not loaded.
executor=unreal.MoviePipelineNewProcessExecutor()
executor=unreal.MoviePipelinePIEExecutor()
ERROR Message:
Message dialog closed, result: Ok, title: Message, text: One or more jobs in the queue have an unsaved map as their target map. Maps must be saved at least once before rendering.
To confirm, I am able to successfully add the sequence to the MovieRenderQueue through the UI. And and render from UI.
I was able to finally figure out the problem. The main issue was that specifying a map reference requires you to specify the world in the map as well.
umap = ‘/Game/NewMap’
Should be
umap = ‘/Game/NewMap.NewMap’
I also found out how to interface with the editor queue as well through the subsystem. This is useful if you want to inspect the generated queue from the GUI.
Here is a complete script.
import unreal
import os
umap = '/Game/NewMap.NewMap'
level_sequence='/Game/NewLevelSequence'
outdir=os.path.abspath(os.path.join(unreal.Paths().project_dir(),'out'))
fps=60
frame_count = 120
#Get movie queue subsystem for editor.
subsystem=unreal.get_editor_subsystem(unreal.MoviePipelineQueueSubsystem)
q=subsystem.get_queue()
executor=unreal.MoviePipelinePIEExecutor()
#Optional: empty queue first.
for j in q.get_jobs():
q.delete_job(j)
#Create new movie pipeline job
job = q.allocate_new_job()
job.set_editor_property('map',unreal.SoftObjectPath(umap))
job.set_editor_property('sequence',unreal.SoftObjectPath(level_sequence))
c=job.get_configuration()
render_pass_settings=c.find_or_add_setting_by_class(unreal.MoviePipelineDeferredPassBase)
output_setting=c.find_or_add_setting_by_class(unreal.MoviePipelineOutputSetting)
output_setting.output_directory=unreal.DirectoryPath(outdir)
png_setting=c.find_or_add_setting_by_class(unreal.MoviePipelineImageSequenceOutput_PNG)
error_callback=unreal.OnMoviePipelineExecutorErrored()
def movie_error(pipeline_executor,pipeline_with_error,is_fatal,error_text):
print(pipeline_executor)
print(pipeline_with_error)
print(is_fatal)
print(error_text)
error_callback.add_callable(movie_error)
def movie_finished(pipeline_executor,success):
print(pipeline_executor)
print(success)
finished_callback=unreal.OnMoviePipelineExecutorFinished()
finished_callback.add_callable(movie_finished)
executor = subsystem.render_queue_with_executor(unreal.MoviePipelinePIEExecutor)
if executor:
executor.set_editor_property('on_executor_errored_delegate',error_callback)
executor.set_editor_property('on_executor_finished_delegate',finished_callback)
The problem is that the python script finishes immediately after launching the queue. Then the editor just exits without waiting for queue to finish. Any attempt to stall in the python script just hangs the editor.
I cannot figure out how to make the editor stay open and wait on the finish callback.
As a workaround, I was planning to add the movie script as a startup script for the editor and then make the editor exit when the finish callback is called. (unreal.SystemLibrary.quit_editor())
Parameters
job_type (type(Class)) – Specify the specific Job type that should be created. Custom Executors can use custom Job types to allow the user to provide more information.
Excuse me, I imitate the same code with Mentln. But in my code all images render out without running the callback. I just want to ask what your ‘move the call back into main function’ means.
Thank you very much.
You need to make sure finished_callback is still defined when the executor needs it. Making it global instead of in the function creating the executor is a way to do so.
I found a workaround, if you run the script as a startup script (Project Settings - Plugin -Python - Startup Scripts) it executes and renders the queue as expected.
So what I did is to make a .bat file wich copies my .py script into the startup script location, then it starts unreal. Unreal runs the script on startup and renders the shot. After the rendering is done the .py startup file gets deleted.
Had some issues quitting unreal after the rendering is done so I run a parallel script lisning to the CPU usage and quitting unreal when its done rendering.
Some hassle to setup but it works for now untill I figure out a better way to do it.
I found another workaround. I start UE in headless mode and inside Python script run subprocess.call(args) to call UE in game mode and render MoviePipelineQueue. subprocess.call(args) will start new process and Python script will wait until its finished.
Basically, the work flow in Python script is following: load map, make changes, save changes and run subprocess.call(args).
I’m trying to use the callback method to post the render file when it’s ready, but it seems to be running the callback function before the render is finished, any ideas? about how to wait the render finish to execute a function ?
def render(level_map, master_sequence, sequence, start_frame, end_frame, output_path, file_name, project_check_in ):
level_sequencer = unreal.LevelSequence( unreal.load_asset(sequence) )
# Get movie queue subsystem for editor.
subsystem=unreal.get_editor_subsystem(unreal.MoviePipelineQueueSubsystem)
q=subsystem.get_queue()
executor=unreal.MoviePipelinePIEExecutor()
#Get movie queue subsystem for editor.
mpqSubsystem=unreal.get_editor_subsystem(unreal.MoviePipelineQueueSubsystem)
queue=mpqSubsystem.get_queue()
job = queue.allocate_new_job(unreal.MoviePipelineExecutorJob)
# #Create new movie pipeline job
job.map=unreal.SoftObjectPath(level_map)
job.sequence=unreal.SoftObjectPath(master_sequence)
job.job_name=file_name#job_name
# Create shot and add it to the job
job_shot = unreal.MoviePipelineExecutorShot(level_sequencer)
job_shot.outer_name = level_sequencer.get_name()
job_shot.inner_name = sequence
job_shot.enabled = True
job.shot_info = [job_shot]
c=unreal.MoviePipelineMasterConfig()
# Job output settings
output_setting = c.find_or_add_setting_by_class(unreal.MoviePipelineOutputSetting)
output_setting.output_directory = unreal.DirectoryPath(output_path)
output_setting.file_name_format = file_name
output_setting.output_resolution= 480, 640#1920, 1080
output_setting.use_custom_playback_range = True
output_setting.custom_start_frame = start_frame
output_setting.custom_end_frame = end_frame
# Job encoding settings
proRes = c.find_or_add_setting_by_class(unreal.MoviePipelineAppleProResOutput)
proRes.codec = unreal.AppleProResEncoderCodec.PRO_RES_422_PROXY
# Ensure deferred rendering on job so lights are properly rendered
c.find_or_add_setting_by_class(unreal.MoviePipelineDeferredPassBase)
# Use high res settings
highResSettings = c.find_or_add_setting_by_class(unreal.MoviePipelineHighResSetting)
highResSettings.texture_sharpness_bias = -0.1
job.set_configuration(c)
error_callback=unreal.OnMoviePipelineExecutorErrored()
def movie_error(pipeline_executor,pipeline_with_error,is_fatal,error_text):
print(pipeline_executor)
print(pipeline_with_error)
print(is_fatal)
print(error_text)
error_callback.add_callable(movie_error)
def movie_finished(pipeline_executor,success):
print(pipeline_executor)
print(success)
project_check_in() # Publish function, passed on arguments
finished_callback=unreal.OnMoviePipelineExecutorFinished()
finished_callback.add_callable(movie_finished)
executor = mpqSubsystem.render_queue_with_executor(unreal.MoviePipelinePIEExecutor)
if executor:
executor.set_editor_property('on_executor_errored_delegate',error_callback)
executor.set_editor_property('on_executor_finished_delegate',finished_callback)
I’ve done something similar with take_high_res_screenshot, in case that’s helpful. This code will take a screenshot from the point of view of “CameraOne”. It does fine on all the frames except the first one, so I added a bogus frame at the start of the level sequence (frame -1).
Note that the settings for high res screenshots are very persistent. If the quality is too low, you might look at the options (hidden under the three-bar menu in the upper left of the viewport).
I’m sure this is inelegant – I don’t really do this sort of event-based programming – but it works for me.
import unreal
import time
class Capture(object):
""" Register a function and run it on tick, """
def __init__(self, fileprefix="test_", frames=range(-1, 50)):
self.fileprefix = fileprefix
LES = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
LES.editor_set_game_view(True)
EAS = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
actors = EAS.get_all_level_actors()
self.cameraActor = unreal.EditorFilterLibrary.by_actor_label(actors, "CameraOne")[0]
# make a generator function so we can call next on it,
self.frames = frames
self.frameIter = iter(self.frames)
self.tickcount = 0
self.shotName = "warmup.png"
self.theFrame = next(self.frameIter)
print("setting frame")
print("frame is " + str(self.theFrame))
unreal.LevelSequenceEditorBlueprintLibrary.set_current_time(self.theFrame)
print("capture, self.tickcount=" + str(self.tickcount) + " :t=" + str(self.theFrame))
time.sleep(4)
# make a file name with a 4 digit frame number
self.shotName = self.fileprefix + str(self.theFrame).zfill(4) + ".png"
self.captureTask = unreal.AutomationLibrary.take_high_res_screenshot(
1920,
1080,
self.shotName,
camera=self.cameraActor,
capture_hdr=True,
delay=0.25
)
# register a callback on pretick
self.on_pre_tick = unreal.register_slate_pre_tick_callback(self.__pretick__)
def __pretick__(self, deltatime):
""" Function we will call every pretick, """
if self.captureTask.is_task_done():
try:
# Count
self.tickcount = self.tickcount + 1
# Get the next frame from our generator function
self.theFrame = next(self.frameIter)
print("setting frame")
print("frame is " + str(self.theFrame))
self.shotName = self.fileprefix + str(self.theFrame).zfill(4) + ".png"
unreal.LevelSequenceEditorBlueprintLibrary.set_current_time(self.theFrame)
time.sleep(2)
print("capture, self.tickcount=" + str(self.tickcount) + " :t=" + str(self.theFrame))
self.captureTask = unreal.AutomationLibrary.take_high_res_screenshot(
1920,
1080,
# actor.get_name() + ".hdr",
self.shotName,
camera=self.cameraActor,
capture_hdr=True
)
except Exception as error:
print(error)
unreal.unregister_slate_pre_tick_callback(self.on_pre_tick)
if __name__ == '__main__':
instance = Capture()