220 lines
8.6 KiB
Python
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()
|