agentleFS
dima-rdnvdima-rdnv/claude-skills/unreal-sequencer-python/SKILL.md

unreal-sequencer-python

Skill2 starsChanged 5 days agoMITView on GitHub

Building Unreal Engine 5.8 Level Sequences from Python and rendering them with Movie Render Queue — cine camera with keyed transform, a camera on a movable Camera Rig Rail (key only the rail position), animating Blueprint variables and post-process settings (time of day, locked exposure), MRQ jobs/presets/executors and their traps, camera cut track, animating a Material Parameter Collection value, a LevelSequenceActor that auto-plays on Play, re-running the build safely, and checking the result in Play-In-Editor through the Unreal MCP. Load with unreal-mcp.

The file

We split this skill into the rules it gives an agent

2 rules. About 0 are common, 1 is distinctive, and 1 is very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    After-tonemapping post-process effects (titles, screen tints) then work on linear values — render them as a separate pass instead.

    line 190
  2. Very specific:

    After Effects 26.5 (OCIO, ACEScg project) can import a multilayer EXR sequence as ImportAsType.COMP_CROPPED_LAYERS (one layer per EXR …

    line 215

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Level Sequences from Python

Verified Oct 2026, UE 5.8.4, run through ue_exec.py (see unreal-mcp). Checked in PIE: the camera cut took over the view and the MPC value drove the material.

Create or reuse the sequence

AT, EAL = unreal.AssetToolsHelpers.get_asset_tools(), unreal.EditorAssetLibrary
seq = EAL.load_asset(path) if EAL.does_asset_exist(path) else \
      AT.create_asset(name, folder, unreal.LevelSequence, unreal.LevelSequenceFactoryNew())
for b in list(seq.get_bindings()): b.remove()
for t in list(seq.get_tracks()): seq.remove_track(t)
seq.set_display_rate(unreal.FrameRate(24, 1))
seq.set_playback_start(0); seq.set_playback_end(720)      # display-rate frames

Don't delete and recreate a sequence (or material) that is loaded/used: delete_asset fails with "Failed to unload all packages during ForceDeleteObjects" (an Ensure in the log) and the following create_asset returns None. Reuse and clear it instead.

Camera with keyed transform

cam = eas.spawn_actor_from_class(unreal.CineCameraActor, start)
cc = cam.get_cine_camera_component()
cc.set_editor_property("current_focal_length", 24.0)
fs = cc.get_editor_property("focus_settings")
fs.set_editor_property("focus_method", unreal.CameraFocusMethod.DISABLE)   # no depth of field
cc.set_editor_property("focus_settings", fs)

binding = seq.add_possessable(cam)
sec = binding.add_track(unreal.MovieScene3DTransformTrack).add_section()
sec.set_range(0, 720)
ch = {c.get_name().rsplit("_", 1)[0]: c for c in sec.get_all_channels()}
ch["Location.Y"].add_key(unreal.FrameNumber(0), -34000.0, 0.0,
                         unreal.MovieSceneTimeUnit.DISPLAY_RATE, unreal.MovieSceneKeyInterpolation.LINEAR)
  • Channel names carry a numeric suffix (Location.X_1, Rotation.Z_3 ...) — strip it. Rotation.X = roll, .Y = pitch, .Z = yaw.
  • unreal.Rotator(roll=, pitch=, yaw=) — use keywords; the positional order is roll, pitch, yaw.

Camera on a rail the user can move (Camera Rig Rail)

Better than keyed transforms when the user wants to place the path themselves: the sequence keys only the rail position, so moving / rotating / stretching the rail never breaks the animation.

rail = eas.spawn_actor_from_class(unreal.CameraRig_Rail, start)
rail.set_editor_property("lock_orientation_to_rail", True)      # camera looks along the rail
rail.set_editor_property("show_rail_visualization", False)      # see below
# convention: actor at the start, facing along the path; spline along local X -> moving, rotating or
# piloting the rail actor moves the whole path
rail.set_actor_location_and_rotation(start, unreal.MathLibrary.find_look_at_rotation(start, end), False, False)
sp = rail.get_rail_spline_component()
sp.clear_spline_points(False)
for x in (0.0, (end - start).length()):
    sp.add_spline_point(unreal.Vector(x, 0, 0), unreal.SplineCoordinateSpace.LOCAL, False)
for i in range(2): sp.set_spline_point_type(i, unreal.SplinePointType.LINEAR, False)
sp.update_spline()
snap = unreal.AttachmentRule.SNAP_TO_TARGET
cam.attach_to_actor(rail, "", snap, snap, unreal.AttachmentRule.KEEP_WORLD, False)  # -> RailCameraMount
t = seq.add_possessable(rail).add_track(unreal.MovieSceneFloatTrack)
t.set_property_name_and_path("Current Position On Rail", "CurrentPositionOnRail")
s = t.add_section(); s.set_range(0, 480)
ch = s.get_all_channels()[0]          # key (f/480)**3 on every frame, LINEAR
cam_binding = seq.add_possessable(cam)  # no transform track; use it for the camera cut

From the engine source (CameraRig_Rail.cpp): position is by distance (GetLocationAtDistanceAlongSpline(CurrentPositionOnRail * SplineLength)), so an eased key curve keeps its speed profile whatever the rail length. The mount is updated in the rail's Tick (ShouldTickIfViewportsOnly = true) and on PostEditChange/Move — after moving the playhead from Python, read the camera's transform in a LATER remote call. To predict the camera's path without the editor (e.g. to bake something along it): get_location_at_distance_along_spline / get_rotation_at_distance_along_spline (WORLD) composed with the camera's transform relative to the RailCameraMount component (MathLibrary.compose_transforms(rel, at)) — matched the scrubbed camera to the centimetre.

  • show_rail_visualization also drives the preview mount mesh (SM_RailRig_Mount), which sits right in front of the lens when you look through the camera in the editor (hidden in game/MRQ anyway). Off = no track mesh, but selecting the rail still draws its spline and points to drag.
  • Trap — Sequencer restores the old transform after you re-parent: the camera had a keyed transform track; the script removed the bindings, attached the camera to the rail (snap = zero offset), and on the next evaluation Sequencer restored the camera's pre-animated transform — its old WORLD location landed as the RELATIVE offset (camera 1 km away, moving with the rail). Zero the relative location/rotation in a later call (set_actor_relative_location/rotation); it doesn't recur once no transform track is left.

Camera cut

cut = seq.add_track(unreal.MovieSceneCameraCutTrack).add_section()
cut.set_range(0, 720)
cut.set_camera_binding_id(seq.get_portable_binding_id(seq, binding))

Animating a Material Parameter Collection value

t = seq.add_track(unreal.MovieSceneMaterialParameterCollectionTrack)
t.set_editor_property("mpc", unreal.load_asset("/Game/.../MPC_X"))
s = t.add_section(); s.set_range(0, 720)
s.add_scalar_parameter_key("Time", unreal.FrameNumber(0), 0.0)   # creates the channel ("Time_1")
c = [c for c in s.get_all_channels() if "Time" in c.get_name()][0]
for k in list(c.get_keys()): c.remove_key(k)                      # then key it like any channel

Play it on Play

lsa = eas.spawn_actor_from_class(unreal.LevelSequenceActor, loc)
lsa.set_sequence(seq)
ps = lsa.get_editor_property("playback_settings")
ps.set_editor_property("auto_play", True)
lsa.set_editor_property("playback_settings", ps)

The camera cut track takes over the player's view during playback. To keep free control of the player camera while the sequence still animates everything else (e.g. a material parameter), set ps.set_editor_property("disable_camera_cuts", True) on the same playback settings (verified).

Easing and hard cuts

  • Non-linear motion (e.g. an accelerating camera, distance ~ (t/T)^3): key the channel on every frame with LINEAR interpolation — exact and simple (481 keys for 20 s at 24 fps is fine).
  • Hard switches of a parameter: keys with unreal.MovieSceneKeyInterpolation.CONSTANT (0 at frame 0, 1 at the switch frame).

Previewing a moment in the editor

LSE = unreal.LevelSequenceEditorBlueprintLibrary
if LSE.get_current_level_sequence() != seq: LSE.open_level_sequence(seq)
LSE.set_current_time(frame)            # deprecated overload but works in 5.8; display-rate frames
LSE.refresh_current_level_sequence()

This evaluates every track on the editor world (camera position, MPC values). While a sequence is open in Sequencer, it re-evaluates its MPC track and overwrites values you set directly with MaterialLibrary.set_scalar_parameter_value — preview through the playhead instead (verified: a direct preview got reset to frame-0 values before the capture).

Reading it back

seq.get_bindings() → b.get_tracks() → t.get_sections() → s.get_all_channels() → c.get_keys() → k.get_time().frame_number.value, k.get_value(). MovieSceneObjectBindingID prints as an empty struct from Python — verify the cut in PIE.

Animating other actors' properties

  • Blueprint variables with spaces (Ultra Dynamic Sky "Time of Day"): binding add_possessable(actor), MovieSceneFloatTrack, set_property_name_and_path("Time of Day", "Time of Day") — worked (read back with actor.get_editor_property("Time of Day") after scrubbing). Python attribute access uses the same spaced name.
  • Struct members: PostProcessVolume exposure lock animated with paths "Settings.AutoExposureMinBrightness" / "...MaxBrightness" (overrides already on). With a locked histogram range, set auto_exposure_speed_up/down high (100): renders start from a default exposure and the 32 warm-up frames were not enough to adapt (the red came out at 58 % brightness).
  • Night → day with a flat emissive colour: scale the emissive by 2^(EV − EV_day) as the locked EV changes so it reads the same on screen.

Movie Render Queue from Python (5.8)

sub = unreal.get_editor_subsystem(unreal.MoviePipelineQueueSubsystem); q = sub.get_queue()
job = q.allocate_new_job(unreal.MoviePipelineExecutorJob)
job.set_editor_property("sequence", unreal.SoftObjectPath(seq.get_path_name()))
job.set_editor_property("map", unreal.SoftObjectPath("/Game/.../L_Map.L_Map"))
cfg = job.get_configuration()            # or job.set_configuration(preset_asset)
o = cfg.find_or_add_setting_by_class(unreal.MoviePipelineOutputSetting)
o.set_editor_property("output_resolution", unreal.IntPoint(w, h))   # + output_directory (DirectoryPath),
# file_name_format "{sequence_name}.{frame_number}", use_custom_playback_range / custom_start/end_frame
cfg.find_or_add_setting_by_class(unreal.MoviePipelineDeferredPassBase)
cfg.find_or_add_setting_by_class(unreal.MoviePipelineImageSequenceOutput_PNG)
aa = cfg.find_or_add_setting_by_class(unreal.MoviePipelineAntiAliasingSetting)   # samples, warm-up counts
sub.render_queue_with_executor(unreal.MoviePipelineNewProcessExecutor)           # or ...PIEExecutor
  • Presets: AssetTools.create_asset(name, path, unreal.MoviePipelinePrimaryConfig, unreal.MoviePipelinePrimaryConfigFactory()), fill with the same calls, save_loaded_asset.
  • PIE executor starts Play-in-Editor: any loaded Blueprint with compile errors pops the modal "Blueprint Asset Compilation Error … Play in Editor / Cancel" (it checks every loaded UBlueprint with BS_Error; Electric Dreams' ambience BP needs the Soundscape plugin). Removing the actors doesn't unload the class (the undo buffer holds them) — enable the plugin (restart) or use the new-process executor.
  • New-process executor renders in UnrealEditor-Cmd from DISK (save everything first — unsaved materials render stale) and keeps the editor free, but it locks the packages it loaded: editor saves of those fail ("MoveFile was unable to move … Error saving") until it ends. Cancel: sub.get_active_executor().cancel_all_jobs(). First job ~5 min (level load + shaders), later ones faster.
  • Camera cuts: a cut inside a subsequence won over the parent shot's cut in MRQ — keep cuts only at the level that is rendered.
  • EXR: MoviePipelineImageSequenceOutput_EXR has compression (DWAA/DWAB/ZIP/PIZ/…), multilayer, multipart. Only the MULTILAYER writer honours compression — single-layer EXRs came out ZIP whatever was set (it falls back to the generic image writer). One pass in multilayer = plain R/G/B/A channels; DWAB cut a 2884x1800 frame from 20 MB (ZIP) to 3.6 MB. The beauty is half float (the deferred pass is 16-bit; "Use High Precision (32-bit) Output" exists only for additional post-process passes).
  • Linear / ACES: MoviePipelineColorSetting: disable_tone_curve = True + ocio_configuration (OpenColorIODisplayConfiguration: is_enabled, color_configuration = OpenColorIOColorConversionSettings: configuration_source (an OpenColorIOConfiguration asset from OpenColorIOConfigurationFactoryNew, configuration_file FilePath "ocio://cg-config-latest" = the built-in ACES CG config, desired_color_spaces [OpenColorIOColorSpace with color_space_name]), source_color_space "Linear Rec.709 (sRGB)", destination_color_space "ACEScg"). After-tonemapping post-process effects (titles, screen tints) then work on linear values — render them as a separate pass instead. Failed in practice: with colour spaces set by name only from Python, the render stamped "OCIO INVALID" over every frame (the plugin's error pattern; the log only said the config "should be resaved"). Safe route: tone curve off, no OCIO in UE (scene-linear Linear Rec.709 / sRGB primaries), and let the comp's OCIO convert (input colour space "Linear Rec.709 (sRGB)"). Check one frame before a full render when OCIO is on.
  • Extra pass (e.g. a matte): deferred pass additional_post_process_materials = [MoviePipelinePostProcessPass with enabled, material, name, high_precision_output]; the material is Post Process / After Tonemapping. In a multilayer EXR it arrived as FinalImage<Name>.R/G/B/A (float). Not needed: disable_multisample_effects (that would drop DoF / motion blur for the beauty).
  • Depth pass: same mechanism, a PP material with SceneTexture SceneDepth (cm, in .r of its float4 Color output — a Custom node input D is float4, so use D.r) -> e.g. metres in R, 0 in G/B (keeps the 32-bit layer small). The PP target is half float per sample (max 65504): cap the sky below that or it accumulates to inf. 32-bit layers stay lossless in a DWAB file (DWA's lossy step is for half R/G/B). A failed PP material silently falls back to the default and the layer carries the scene colour: check Failed to compile Material in the editor log and look at the layer's values.
  • Speed: spatial x temporal samples cost about the product; a frame of 96 (6 x 16) cinematic samples at 2884x1800 took 9.5 s on an RTX 4090 while a quick 32-sample test suggested ~5 s — measure the real job's first ~20 frames and re-plan before trusting an overnight estimate.
  • Speed (UE 5.8, RTX 4090, Lumen + Nanite + UDS clouds, 2884x1800): 8 temporal samples 0.63 s / frame (ZIP EXR), ~1.0 s with DWAB + a 32-bit matte pass + OCIO; 1 sample + TSR 0.2 s / frame (720 frames in 3 min 45 s including start-up) — the "fast" preset. ffmpeg 7.1 can't decode DWAB EXRs (black frames). Start-up ~1-3 min; the first run after material changes also compiles shaders (~7 min, 16 ShaderCompileWorkers).
  • Editor Python globals persist between remote runs: a module-level X = globals().get("X", default) picked up 16 from an older run instead of the new default 8 — pass every such value explicitly.
  • After Effects 26.5 (OCIO, ACEScg project) can import a multilayer EXR sequence as ImportAsType.COMP_CROPPED_LAYERS (one layer per EXR layer); COMP is refused.
  • Media (a title movie) in renders: a video file isn't frame-accurate in MRQ; use an image sequence. For an overlay we baked the frames into one tiled texture and picked the frame from the sequence time in a post-process material (exact, no media player) — see unreal-materials-python.

Checking in Play-In-Editor through the Unreal MCP

EditorAppToolset.StartPIE with {"options": {"bSimulate": false, "playMode": "PlayMode_InViewPort", "warmupSeconds": 6}} (returns after warm-up), then EditorAppToolset.CaptureEditorImage (whole editor window, ~350 KB → decode with unreal-mcp/scripts/decode_capture.py), later captures after a background sleep, then StopPIE. CaptureViewport needs a capture pose and would not show the sequence camera.

Related traps

  • /Engine/BasicShapes/Plane has no collision — a pawn falls through a floor made of it; use a thin scaled Cube.
  • The Film/Video template's DefaultInput.ini has bAlwaysShowTouchInterface=True (on-screen joysticks in Play). The joystick check (SVirtualJoystick::ShouldDisplayTouchInterface) reads the in-memory config, not the settings object, so editing the ini or the CDO alone needs an editor restart. Without restarting: set it on the CDO and call save_key_mappings() (= SaveConfig):
    s = unreal.InputSettings.get_default_object()
    s.set_editor_property("always_show_touch_interface", False); s.save_key_mappings()
    (verified: joysticks gone in the next PIE). Also set it in Config/DefaultInput.ini to persist.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.