workshop rework
This commit is contained in:
1 parent
a3a80dfa16
commit
fb39e290c0
115 files changed
+1865
-497
No files matched your search
@@ -0,0 +1,183 @@
|
||||
"""MRQ rendering with retained delegates, isolated output and completion status."""
|
||||
from datetime import datetime
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import uuid
|
||||
|
||||
import unreal
|
||||
|
||||
from .paths import PROJECT, executable
|
||||
from .video import encode
|
||||
from .progress import report
|
||||
|
||||
_active = None
|
||||
PRESETS = {"preview": (640, 360, 1, 8), "final": (1280, 720, 4, 32)}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Renderer:
|
||||
"""Reusable render settings. Dimensions in pixels; warmup in frames."""
|
||||
|
||||
width: int = 640
|
||||
height: int = 360
|
||||
samples: int = 1
|
||||
warmup: int = 8
|
||||
mp4: bool = True
|
||||
ffmpeg: str = None
|
||||
|
||||
def __post_init__(self):
|
||||
self._validate()
|
||||
|
||||
def _validate(self):
|
||||
for name, minimum in (("width", 1), ("height", 1), ("samples", 1), ("warmup", 0)):
|
||||
value = getattr(self, name)
|
||||
if type(value) is not int or value < minimum:
|
||||
raise ValueError(f"{name} musi byc liczba calkowita >= {minimum}.")
|
||||
|
||||
@classmethod
|
||||
def from_preset(cls, name="preview", **overrides):
|
||||
if name not in PRESETS:
|
||||
raise ValueError("Nieznany preset. Dostepne: " + ", ".join(PRESETS))
|
||||
settings = dict(zip(("width", "height", "samples", "warmup"), PRESETS[name]))
|
||||
settings.update(overrides)
|
||||
return cls(**settings)
|
||||
|
||||
def render(self, sequence, *, single_frame=None):
|
||||
self._validate()
|
||||
return _start(sequence, self, single_frame=single_frame)
|
||||
|
||||
|
||||
def is_rendering():
|
||||
return _active is not None
|
||||
|
||||
|
||||
def _write_status(data):
|
||||
target = os.environ.get("WORKSHOP_STATUS")
|
||||
if target:
|
||||
path = Path(target)
|
||||
temporary = path.with_suffix(".tmp")
|
||||
temporary.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
temporary.replace(path)
|
||||
|
||||
|
||||
def _on_error(executor, pipeline, fatal, message):
|
||||
if _active is not None:
|
||||
_active["errors"].append(str(message))
|
||||
unreal.log_error("[Workshop] MRQ: " + str(message))
|
||||
|
||||
|
||||
def _on_finished(executor, success):
|
||||
global _active
|
||||
state = _active
|
||||
if state is None:
|
||||
return
|
||||
result = {"success": False, "output": str(state["output"]), "settings": state["settings"]}
|
||||
try:
|
||||
if not success or state["errors"]:
|
||||
raise RuntimeError("Render nie powiodl sie: " + "; ".join(state["errors"]))
|
||||
frames = state["output"] / "frames"
|
||||
count = len(list(frames.glob("frame_*.png")))
|
||||
if count != state["frames"]:
|
||||
raise RuntimeError(f"Zapisano {count} klatek zamiast {state['frames']}.")
|
||||
if state["mp4"]:
|
||||
report("encoding")
|
||||
result["video"] = str(encode(frames, state["fps"], ffmpeg=state["ffmpeg"],
|
||||
expected_frames=state["frames"]))
|
||||
result["success"] = True
|
||||
unreal.log("[Workshop] Gotowe: " + str(state["output"]))
|
||||
except Exception as exc:
|
||||
result["error"] = str(exc)
|
||||
unreal.log_error("[Workshop] " + str(exc))
|
||||
finally:
|
||||
(state["output"] / "result.json").write_text(json.dumps(result, indent=2), encoding="utf-8")
|
||||
_write_status(result)
|
||||
_active = None
|
||||
if os.environ.get("WORKSHOP_BATCH") == "1":
|
||||
report("closing")
|
||||
unreal.EditorPythonScripting.set_keep_python_script_alive(False)
|
||||
|
||||
|
||||
def _begin_render(delta_seconds):
|
||||
"""Start after the script and pending Slate window updates have returned."""
|
||||
if _active is None:
|
||||
return
|
||||
_active["ticks"] -= 1
|
||||
if _active["ticks"] > 0:
|
||||
return
|
||||
unreal.unregister_slate_post_tick_callback(_active.pop("tick_handle"))
|
||||
try:
|
||||
subsystem = unreal.get_editor_subsystem(unreal.MoviePipelineQueueSubsystem)
|
||||
report("rendering", output=str(_active["output"]), frames=_active["frames"])
|
||||
# This installs the Sequencer playback-context guard needed during PIE.
|
||||
subsystem.render_queue_instance_with_executor_instance(_active["queue"], _active["executor"])
|
||||
except Exception as exc:
|
||||
_active["errors"].append(str(exc))
|
||||
_on_finished(_active["executor"], False)
|
||||
|
||||
|
||||
def start(sequence, preset="preview", mp4=True, ffmpeg=None, single_frame=None):
|
||||
"""Shortcut preserved for existing workshop scripts."""
|
||||
return Renderer.from_preset(preset, mp4=mp4, ffmpeg=ffmpeg).render(sequence, single_frame=single_frame)
|
||||
|
||||
|
||||
def _start(sequence, renderer, single_frame=None):
|
||||
global _active
|
||||
if _active is not None:
|
||||
raise RuntimeError("Trwa juz render warsztatowy.")
|
||||
if single_frame is not None and not 0 <= single_frame < sequence.frames:
|
||||
raise ValueError("Klatka poza zakresem sekwencji.")
|
||||
mp4 = renderer.mp4 and single_frame is None
|
||||
ffmpeg_path = executable("ffmpeg", renderer.ffmpeg) if mp4 else None
|
||||
subsystem = unreal.get_editor_subsystem(unreal.MoviePipelineQueueSubsystem)
|
||||
if subsystem.is_rendering():
|
||||
raise RuntimeError("Movie Render Queue jest zajete.")
|
||||
report("preparing_render")
|
||||
# Own queue: do not clear the user's Movie Render Queue.
|
||||
queue = unreal.MoviePipelineQueue()
|
||||
job = queue.allocate_new_job(unreal.MoviePipelineExecutorJob)
|
||||
sequence.save(open_editor=False)
|
||||
unreal.LevelSequenceEditorBlueprintLibrary.close_level_sequence()
|
||||
job.map = unreal.SoftObjectPath(sequence.scene.map_path)
|
||||
job.sequence = unreal.SoftObjectPath(sequence.asset.get_path_name())
|
||||
job.job_name = sequence.scene.name
|
||||
width, height, samples, warmup = renderer.width, renderer.height, renderer.samples, renderer.warmup
|
||||
stamp = datetime.now().strftime("%Y%m%d_%H%M%S") + "_" + uuid.uuid4().hex[:6]
|
||||
output = PROJECT / "renders" / sequence.scene.name / stamp
|
||||
(output / "frames").mkdir(parents=True)
|
||||
config = job.get_configuration()
|
||||
settings = config.find_or_add_setting_by_class(unreal.MoviePipelineOutputSetting)
|
||||
settings.output_directory = unreal.DirectoryPath(str(output / "frames"))
|
||||
settings.file_name_format = "frame_{frame_number_rel}"
|
||||
settings.zero_pad_frame_numbers = 4
|
||||
settings.output_resolution = unreal.IntPoint(width, height)
|
||||
settings.use_custom_frame_rate = True
|
||||
settings.output_frame_rate = unreal.FrameRate(sequence.fps, 1)
|
||||
settings.use_custom_playback_range = True
|
||||
settings.custom_start_frame = single_frame if single_frame is not None else 0
|
||||
settings.custom_end_frame = single_frame + 1 if single_frame is not None else sequence.frames
|
||||
settings.flush_disk_writes_per_shot = True
|
||||
config.find_or_add_setting_by_class(unreal.MoviePipelineImageSequenceOutput_PNG)
|
||||
config.find_or_add_setting_by_class(unreal.MoviePipelineDeferredPassBase)
|
||||
aa = config.find_or_add_setting_by_class(unreal.MoviePipelineAntiAliasingSetting)
|
||||
aa.spatial_sample_count = samples
|
||||
aa.temporal_sample_count = 1
|
||||
aa.engine_warm_up_count = warmup
|
||||
aa.render_warm_up_count = warmup
|
||||
aa.render_warm_up_frames = True
|
||||
executor = unreal.MoviePipelinePIEExecutor()
|
||||
executor.on_executor_errored_delegate.add_callable_unique(_on_error)
|
||||
executor.on_executor_finished_delegate.add_callable_unique(_on_finished)
|
||||
_active = dict(executor=executor, queue=queue, output=output, errors=[], fps=sequence.fps,
|
||||
frames=1 if single_frame is not None else sequence.frames,
|
||||
mp4=mp4, ffmpeg=ffmpeg_path, ticks=2,
|
||||
settings=dict(width=width, height=height, samples=samples, warmup=warmup, fps=sequence.fps))
|
||||
try:
|
||||
_write_status({"state": "rendering", "output": str(output)})
|
||||
_active["tick_handle"] = unreal.register_slate_post_tick_callback(_begin_render)
|
||||
except Exception:
|
||||
_active = None
|
||||
raise
|
||||
unreal.log("[Workshop] Render: " + str(output))
|
||||
return output
|
||||
Reference in new issue
Block a user