hypertwist/UnrealHyperTwist/Content/Python/HyperTwistMoviePipelineReplayExecutor.py
2026-06-13 07:04:11 +00:00

220 lines
8.6 KiB
Python

import glob
import json
import os
import traceback
import unreal
def _log(message: str) -> None:
unreal.log(f"[HyperTwistMoviePipelineReplayExecutor] {message}")
def _log_error(message: str) -> None:
unreal.log_error(f"[HyperTwistMoviePipelineReplayExecutor] {message}")
def _get_parameter(parameters, key: str, default: str = "") -> str:
value = parameters.get(key, default)
return value.strip() if isinstance(value, str) else default
def _get_int_parameter(parameters, key: str, default: int) -> int:
value = _get_parameter(parameters, key, "")
if value == "":
return default
try:
return int(value)
except ValueError:
return default
def _resolve_render_success(results, matching_files) -> bool:
success_value = getattr(results, "success", None)
if success_value is None:
return len(matching_files) > 0
try:
return bool(success_value)
except Exception:
return len(matching_files) > 0
@unreal.uclass()
class HyperTwistMoviePipelineReplayExecutor(unreal.MoviePipelinePythonHostExecutor):
active_movie_pipeline = unreal.uproperty(unreal.MoviePipeline)
output_directory = unreal.uproperty(str)
output_base_name = unreal.uproperty(str)
output_kind = unreal.uproperty(str)
report_path = unreal.uproperty(str)
replay_path = unreal.uproperty(str)
render_map_path = unreal.uproperty(str)
def _post_init(self):
self.active_movie_pipeline = None
self.output_directory = ""
self.output_base_name = ""
self.output_kind = "video"
self.report_path = ""
self.replay_path = ""
self.render_map_path = ""
@unreal.ufunction(override=True)
def execute_delayed(self, in_pipeline_queue):
try:
(_cmd_tokens, _cmd_switches, cmd_parameters) = (
unreal.SystemLibrary.parse_command_line(unreal.SystemLibrary.get_command_line())
)
level_sequence_path = _get_parameter(cmd_parameters, "LevelSequence")
if level_sequence_path == "":
raise RuntimeError("Missing -LevelSequence= argument for replay export render.")
self.output_directory = _get_parameter(
cmd_parameters, "HyperTwistOutputDirectory", "C:/HyperTwist/media-export"
)
self.output_base_name = _get_parameter(
cmd_parameters, "HyperTwistOutputBaseName", "classic-cube-replay"
)
self.output_kind = _get_parameter(
cmd_parameters, "HyperTwistOutputKind", "video"
).lower()
self.report_path = _get_parameter(cmd_parameters, "HyperTwistExportReportPath")
self.replay_path = _get_parameter(cmd_parameters, "HyperTwistReplayPath")
self.render_map_path = _get_parameter(cmd_parameters, "HyperTwistRenderMapPath")
output_width = max(
16, _get_int_parameter(cmd_parameters, "HyperTwistOutputWidth", 1280)
)
output_height = max(
16, _get_int_parameter(cmd_parameters, "HyperTwistOutputHeight", 720)
)
output_frame_rate = max(
1, _get_int_parameter(cmd_parameters, "HyperTwistOutputFrameRate", 30)
)
custom_start_frame = max(
0, _get_int_parameter(cmd_parameters, "HyperTwistCustomStartFrame", 0)
)
custom_end_frame = max(
custom_start_frame + 1,
_get_int_parameter(
cmd_parameters, "HyperTwistCustomEndFrame", custom_start_frame + 1
),
)
pipeline_queue = (
in_pipeline_queue
if in_pipeline_queue is not None
else unreal.new_object(unreal.MoviePipelineQueue, outer=self)
)
self.pipeline_queue = pipeline_queue
new_job = pipeline_queue.allocate_new_job(unreal.MoviePipelineExecutorJob)
if self.render_map_path:
new_job.map = unreal.SoftObjectPath(self.render_map_path)
new_job.sequence = unreal.SoftObjectPath(level_sequence_path)
job_config = new_job.get_configuration()
output_setting = job_config.find_or_add_setting_by_class(
unreal.MoviePipelineOutputSetting
)
output_setting.output_directory = unreal.DirectoryPath(self.output_directory)
output_setting.file_name_format = self.output_base_name
output_setting.output_resolution = unreal.IntPoint(output_width, output_height)
output_setting.use_custom_frame_rate = True
output_setting.output_frame_rate = unreal.FrameRate(
numerator=output_frame_rate, denominator=1
)
output_setting.override_existing_output = True
output_setting.use_custom_playback_range = True
output_setting.custom_start_frame = custom_start_frame
output_setting.custom_end_frame = custom_end_frame
output_setting.zero_pad_frame_numbers = 4
output_setting.flush_disk_writes_per_shot = True
job_config.find_or_add_setting_by_class(unreal.MoviePipelineDeferredPassBase)
if self.output_kind == "still":
job_config.find_or_add_setting_by_class(
unreal.MoviePipelineImageSequenceOutput_PNG
)
else:
mp4_output = job_config.find_or_add_setting_by_class(
unreal.MoviePipelineMP4EncoderOutput
)
mp4_output.include_audio = False
mp4_output.constant_rate_factor = 18
mp4_output.encoding_rate_control = (
unreal.MoviePipelineMP4EncodeRateControlMode.QUALITY
)
job_config.initialize_transient_settings()
target_world = self.get_last_loaded_world()
if target_world is None:
raise RuntimeError(
"Movie render pipeline could not resolve the last loaded world."
)
self.active_movie_pipeline = unreal.new_object(
unreal.MoviePipeline,
outer=target_world,
)
self.active_movie_pipeline.on_movie_pipeline_work_finished_delegate.add_function_unique(
self, "on_movie_pipeline_finished"
)
_log(
"Starting replay export render "
f"(kind={self.output_kind}, map={self.render_map_path or '<current>'}, "
f"sequence={level_sequence_path}, "
f"frames={custom_start_frame}-{custom_end_frame}, output={self.output_directory})"
)
self.active_movie_pipeline.initialize(new_job)
except Exception as error:
_log_error(
f"Replay export render setup failed: {error}\n{traceback.format_exc()}"
)
self.active_movie_pipeline = None
self.on_executor_finished_impl()
@unreal.ufunction(override=True)
def on_begin_frame(self):
super(HyperTwistMoviePipelineReplayExecutor, self).on_begin_frame()
@unreal.ufunction(override=True)
def on_map_load(self, in_world):
pass
@unreal.ufunction(override=True)
def is_rendering(self):
return self.active_movie_pipeline is not None
@unreal.ufunction(ret=None, params=[unreal.MoviePipelineOutputData])
def on_movie_pipeline_finished(self, results):
extension = ".png" if self.output_kind == "still" else ".mp4"
pattern = os.path.join(self.output_directory, "**", f"{self.output_base_name}*{extension}")
matching_files = sorted(glob.glob(pattern, recursive=True))
success = _resolve_render_success(results, matching_files)
report = {
"reportVersion": "ht-classic-cube-mrq-export/v1",
"success": success,
"outputKind": self.output_kind,
"outputDirectory": self.output_directory,
"outputBaseName": self.output_base_name,
"replayPath": self.replay_path,
"files": matching_files,
}
if self.report_path:
report_directory = os.path.dirname(self.report_path)
if report_directory:
os.makedirs(report_directory, exist_ok=True)
with open(self.report_path, "w", encoding="utf-8") as handle:
json.dump(report, handle, indent=2)
_log(
f"Finished replay export render (success={success}, files={len(matching_files)})"
)
self.active_movie_pipeline = None
self.on_executor_finished_impl()