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.
In this repository26 other files it gives its agents.
- aftereffects-bridgeSkill
- aftereffects-scriptingSkill
- cinema4d-alembicSkill
- cinema4d-mcpSkill
- cinema4d-motion-trackerSkill
- cinema4d-particlesSkill
- cinema4d-redshiftSkill
- cinema4d-render-previewSkill
- cinema4d-riggingSkill
- cinema4d-takesSkill
- houdini-flipbookSkill
- houdini-mcpSkill
- houdini-network-layoutSkill
- houdini-pop-particlesSkill
- houdini-pyro-smokeSkill
- houdini-to-cinema4dSkill
- houdini-vdb-surfacingSkill
- premiere-bridgeSkill
- project-notesSkill
- unreal-content-managementSkill
- unreal-editor-toolsSkill
- unreal-led-volume-rigSkill
- unreal-materials-pythonSkill
- unreal-mcpSkill
- unreal-ultra-dynamic-skySkill
- vo-timingSkill
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
- Distinctive: line 190
After-tonemapping post-process effects (titles, screen tints) then work on linear values — render them as a separate pass instead.
- Very specific: line 215
After Effects 26.5 (OCIO, ACEScg project) can import a multilayer EXR sequence as ImportAsType.COMP_CROPPED_LAYERS (one layer per EXR …
≈ “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.
Files
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 framesDon'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 cutFrom 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_visualizationalso 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 channelPlay 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 withactor.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, setauto_exposure_speed_up/downhigh (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-Cmdfrom 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_EXRhascompression(DWAA/DWAB/ZIP/PIZ/…),multilayer,multipart. Only the MULTILAYER writer honourscompression— 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(anOpenColorIOConfigurationasset fromOpenColorIOConfigurationFactoryNew,configuration_fileFilePath"ocio://cg-config-latest"= the built-in ACES CG config,desired_color_spaces[OpenColorIOColorSpace withcolor_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= [MoviePipelinePostProcessPasswithenabled,material,name,high_precision_output]; the material is Post Process / After Tonemapping. In a multilayer EXR it arrived asFinalImage<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
.rof its float4Coloroutput — a Custom node inputDis float4, so useD.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: checkFailed to compile Materialin 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);COMPis 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/Planehas no collision — a pawn falls through a floor made of it; use a thin scaled Cube.- The Film/Video template's
DefaultInput.inihasbAlwaysShowTouchInterface=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 callsave_key_mappings()(= SaveConfig):
(verified: joysticks gone in the next PIE). Also set it ins = unreal.InputSettings.get_default_object() s.set_editor_property("always_show_touch_interface", False); s.save_key_mappings()Config/DefaultInput.inito persist.
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
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.

