Viewport Control Skill
Methods
| Method | Description |
|---|---|
get_viewport_info() | Get full viewport state (type, camera, FOV, exposure, layout) |
set_viewport_type(type) | Switch perspective/orthographic views |
get_viewport_type() | Get current view type as string |
set_view_mode(mode) | Switch rendering mode (lit, wireframe, unlit, etc.) |
get_view_mode() | Get current rendering mode |
set_fov(degrees) | Set field of view (5-170, default 90) |
get_fov() | Get current FOV |
set_near_clip_plane(distance) | Set near clipping plane (-1 = engine default) |
set_far_clip_plane(distance) | Set far clipping plane (0 = infinity) |
set_exposure(fixed, ev100) | Set fixed/auto exposure with EV100 value |
set_exposure_game_settings() | Reset exposure to auto (game settings) |
set_game_view(enable) | Toggle Game View (hides editor icons) |
set_allow_cinematic_control(allow) | Allow Sequencer to control viewport camera |
set_realtime(enable) | Toggle realtime rendering |
set_camera_speed(speed) | Set camera movement speed (1-8) |
set_viewport_layout(name) | Switch viewport layout (single, quad, etc.) |
get_viewport_layout() | Get current layout name |
capture_scene(location, rotation, width, height, out_png, ortho_width=0, fov=90, manual_ev100=0) | Synchronous SceneCapture2D → PNG that works while the editor is backgrounded (see below) |
🔀 Camera get/set moved to the engine.
ViewportServiceno longer hasset_camera_location/set_camera_rotation. Reading and positioning the viewport camera is now Unreal 5.8's nativeEditorAppToolset(GetCameraTransform/SetCameraTransform, plusFocusOnActorsto frame actors) — reach it withcall_tool(rundescribe_toolsetonEditorAppToolsetfor exact action names/params).ViewportServicestill owns view type, view mode, FOV, clip planes, exposure, game view, layout, realtime, and camera speed.
Critical Rules
⚠️ A black capture usually means no lighting, not a broken mesh
The most common cause of an all-black (or blown-out white) capture is the level, not the camera and not the asset you just built. Two things to check before you start debugging geometry:
- A level made with
new_level()has no lighting at all — no sun, no sky, no atmosphere. Usenew_level_from_template(path, "/Engine/Maps/Templates/Template_Default")instead (see thelevel-actorsskill). If you must light an empty level by hand you need a DirectionalLight that genuinely points down —unreal.Rotator(roll=0, pitch=-48, yaw=125), and note the argument order is (roll, pitch, yaw), so a positionally-passed pitch aims the sun at the sky and the scene stays black — plus a SkyLight withreal_time_capture, and a SkyAtmosphere. - Auto-exposure lies about colour. With adaptation on, a dark material renders near-white and a
bright one renders grey, so you cannot judge a material from a capture. Add an unbound
PostProcessVolume with
auto_exposure_method = AEM_MANUAL; thenauto_exposure_biasis a brightness stop — higher is brighter, and the usable window is narrow (0 was pitch black and 13.5 blown out in one scene; 7.5 was correct). Tune it by capturing, not by reasoning.
Keep the volume out of frame (move it far below the set) — an unbound volume still applies globally but its bounds box draws in the editor viewport.
⚠️ Valid View Types for set_viewport_type
Only these exact strings are accepted (case-insensitive):
| String | View |
|---|---|
"perspective" | Perspective (3D) |
"top" | Top-down (XY) |
"bottom" | Bottom-up (-XY) |
"left" | Left side (-XZ) |
"right" | Right side (XZ) |
"front" | Front (-YZ) |
"back" | Back (YZ) |
python# ✅ CORRECT unreal.ViewportService.set_viewport_type("perspective") unreal.ViewportService.set_viewport_type("top") # ❌ WRONG — these are not valid strings unreal.ViewportService.set_viewport_type("ortho") unreal.ViewportService.set_viewport_type("iso")
⚠️ Valid View Modes for set_view_mode
| String | Rendering Mode |
|---|---|
"lit" | Default fully lit |
"unlit" | No lighting |
"wireframe" | Wireframe overlay |
"detaillighting" | Detail lighting |
"lightingonly" | Lighting only (no textures) |
"lightcomplexity" | Light complexity heatmap |
"shadercomplexity" | Shader complexity heatmap |
"pathtracing" | Path tracing |
"clay" | Clay rendering |
⚠️ Valid Layout Names for set_viewport_layout
| Name | Layout |
|---|---|
"OnePane" | Single viewport (default) |
"TwoPanesHoriz" | Two side-by-side |
"TwoPanesVert" | Two stacked |
"ThreePanesLeft" | Large left + 2 right |
"ThreePanesRight" | Large right + 2 left |
"ThreePanesTop" | Large top + 2 bottom |
"ThreePanesBottom" | Large bottom + 2 top |
"FourPanesLeft" | Large left + 3 |
"FourPanesRight" | Large right + 3 |
"FourPanesTop" | Large top + 3 |
"FourPanesBottom" | Large bottom + 3 |
"FourPanes2x2" | Quad view (2x2 grid) |
"Quad" | Alias for FourPanes2x2 |
🚨 FOV Only Works in Perspective Mode
Setting FOV has no effect in orthographic views. Always switch to perspective first:
pythonunreal.ViewportService.set_viewport_type("perspective") unreal.ViewportService.set_fov(75.0)
🚨 Realtime Mode vs On-Demand
When set_realtime(False), the viewport only repaints on interaction. All ViewportService methods force a redraw after changes, so this is transparent to Python callers — but be aware users won't see continuous animation/particles until realtime is re-enabled.
⚠️ Multi-pane read-back caveat:
get_viewport_info().is_realtimereflects the active pane. In a multi-pane layout (e.g.FourPanes2x2) where the active pane isn't the primary,set_realtime(True)can succeed yetis_realtimereads backFalse. Verify realtime inOnePanelayout, or don't rely on the read-back to gate logic when in a split layout. (Other fields like view type / FOV / camera read back correctly across layouts.)
📸 Capture that works while backgrounded
CaptureViewport / CaptureEditorImage (and HighResShot) return a stale frame when the
MCP-driven editor is minimised or backgrounded — the level viewport does not pump frames. The
reliable path is ViewportService.capture_scene(...): it spawns a transient ASceneCapture2D,
renders one frame synchronously with CaptureScene(), reads the pixels back and writes the PNG
in-call. No viewport pumping, no editor_invalidate_viewports() + sleep dance.
pythonimport unreal # Perspective grab of a spot in the level (works even when the editor window is hidden) res = unreal.ViewportService.capture_scene( unreal.Vector(1200, -800, 900), # camera location unreal.Rotator(pitch=-20, yaw=45, roll=0), # kwargs — Rotator positional order is (roll,pitch,yaw) 1280, 720, # width, height (px) "C:/temp/shot.png", # absolute path, or relative → Saved/VibeUE/Captures manual_ev100=-6.0) # 0 = auto (black when hidden); negative EV = brighter fixed exposure — see below if res.b_success: print("wrote", res.output_path, res.file_size_bytes, "bytes") else: print("failed:", res.error_message)
Baked-in facts (measured — these are why the hand-rolled recipe existed):
- Alpha. The capture source is
SCS_FINAL_COLOR_LDR, which yields alpha 255.SCS_BASE_COLORwrites alpha 0, producing a PNG that renders as a blank white page in most viewers even though the file is a full-size capture.capture_sceneforces the exported alpha to 255 regardless, so its PNGs are always opaque. (Check the file size, not just the preview — a truly empty PNG is a few KB.) - Format. The render target is
RTF_RGBA8. The engine default (RTF_RGBA16f, a float format) writes non-PNG bytes;capture_scenenever uses it. - Exposure. Leave
manual_ev100=0(default) to keep the engine's automatic exposure — fine for a foreground / PIE window, but black when the editor is hidden (no converged eye adaptation). A backgrounded editor needs a fixed exposure: passmanual_ev100!= 0 andcapture_sceneswitches to a manual exposure decoupled from the physical camera, where the value is the exposure TARGET in EV100 — exactly like a camera's metered EV: higher EV100 is DARKER (it assumes a brighter scene and stops down), lower/negative is brighter. Measured on a daylit scene from a backgrounded editor (mean RGB luminance):0(auto) → 16.4,+4→ 4.3,-4→ 43.8,-6→ 63.4,-8→ 86.0. A daylit backgrounded scene reads well around-6to-8; try-4first for bright scenes. Tune by capturing.Note: the engine reads its underlying
AutoExposureBiasas apow(2,bias)exposure compensation (so its docs say "positive brightens"), but in this decoupled-Manual SceneCapture path the observed, repeatable behaviour is the opposite — higher = darker — so treatmanual_ev100as a camera EV target. Do not carry over any old "bias ~10–14 to brighten" advice; that was from a build that left the physical camera coupled, and here a large positive value is near-black. - Cleanup. The transient capture actor and its render target are destroyed inside the call — no stray actors are left in the level.
Minimap / top-down map recipe (orthographic, north-up):
pythonimport unreal # Ortho capture centred over the map, looking straight down, north-up. # ortho_width = the world-space span you want to cover (e.g. the landscape size in uu). map_size = 500000.0 # uu across res = unreal.ViewportService.capture_scene( unreal.Vector(0, 0, 100000), # high above centre; Z only needs to clear geometry unreal.Rotator(pitch=-90, yaw=-90, roll=0), # pitch=-90 looks down; yaw=-90 makes +X point up = north-up 2048, 2048, "minimap.png", # → Saved/VibeUE/Captures/minimap.png ortho_width=map_size, manual_ev100=-6.0) # fixed exposure for a hidden editor; LOWER (more negative) = brighter
ortho_width > 0selects orthographic projection (andfovis ignored);ortho_width == 0(default) is perspective usingfov.- The rotation
(pitch=-90, yaw=-90, roll=0)yields a north-up image (world +X points to the top). This is a convention of the caller's rotation, not something the API forces — pass a different yaw to rotate the map. - For flat map labels, a
TextRenderActorat rotation(roll=0, pitch=90, yaw=90)reads correctly in this north-up view.
Workflows
Inspect Current Viewport State
pythonimport unreal info = unreal.ViewportService.get_viewport_info() view_mode = unreal.ViewportService.get_view_mode() print(f"Type: {info.viewport_type}") print(f"View Mode: {view_mode}") print(f"Location: {info.location}") print(f"Rotation: {info.rotation}") print(f"FOV: {info.fov}") print(f"Layout: {info.layout}") print(f"Realtime: {info.is_realtime}") print(f"Game View: {info.is_game_view}")
Cycle Through Orthographic Views
pythonimport unreal for view in ["top", "front", "right", "perspective"]: unreal.ViewportService.set_viewport_type(view)
Switch to Quad View and Back
pythonimport unreal # Switch to quad (2x2) layout unreal.ViewportService.set_viewport_layout("Quad") # Switch back to single pane unreal.ViewportService.set_viewport_layout("OnePane")
Set Up Architecture Review Camera
pythonimport unreal # Perspective with narrow FOV for minimal distortion unreal.ViewportService.set_viewport_type("perspective") unreal.ViewportService.set_fov(60.0) unreal.ViewportService.set_game_view(True) unreal.ViewportService.set_exposure(True, 1.0) # Fixed exposure unreal.ViewportService.set_realtime(True)
Wireframe Debugging
pythonimport unreal # Switch to wireframe to inspect mesh topology unreal.ViewportService.set_view_mode("wireframe") # Return to normal lit view unreal.ViewportService.set_view_mode("lit")
Position Camera at Specific Location
Camera placement now lives in the engine EditorAppToolset (SetCameraTransform), called via
call_tool. From Python you can still drive it through the editor subsystem:
pythonimport unreal # Move camera to a bird's-eye view (location, rotation) # ⚠️ Rotator positional order is (Roll, Pitch, Yaw) — always use kwargs unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem).set_level_viewport_camera_info( unreal.Vector(0, 0, 5000), unreal.Rotator(pitch=-90))
To frame specific actors instead of guessing coordinates, prefer the engine
EditorAppToolset.FocusOnActors action (or load the level-actors skill for
ActorService.get_actor_view_camera, which auto-computes a framing position from bounds).
Configure Clipping Planes
pythonimport unreal # Tighten near clip for close-up work unreal.ViewportService.set_near_clip_plane(1.0) # Set far clip to avoid rendering distant objects unreal.ViewportService.set_far_clip_plane(50000.0) # Reset to defaults unreal.ViewportService.set_near_clip_plane(-1) # Engine default unreal.ViewportService.set_far_clip_plane(0) # Infinity
Exposure Control
pythonimport unreal # Fix exposure for consistent lighting review unreal.ViewportService.set_exposure(True, 1.0) # Adjust to bright/dark scene unreal.ViewportService.set_exposure(True, -2.0) # Darker unreal.ViewportService.set_exposure(True, 4.0) # Brighter # Return to auto exposure (game settings) unreal.ViewportService.set_exposure_game_settings()
Sample scripts (run via execute_python_code)
scripts/set_camera.txt— position the editor camera and set the view mode.
Additional gotchas
CaptureViewportthroughcall_toolneeds BOTH optional params present ({"captureTransform":{}, "annotations":{}});captureTransformis ignored (it always frames the world origin), and the ~1 MB base64 result must be decoded to a file before it can be viewed.- Outside PIE the MCP-driven viewport does not pump frames, so captures are stale — static actors render but dynamic FX are absent and
HighResShotnever fires. CallPerformanceService.set_background_throttling(False)first; asset thumbnails still render fine while backgrounded. CaptureEditorImagereturns the previous frame unless you calleditor_invalidate_viewports()and wait briefly first.- Use
capture_scenefor a real off-screen render instead of hand-building aSceneCapture2D; a backgrounded editor has no converged eye adaptation, so fix the exposure (AEM_MANUAL, bias ~10-14) or the shot comes out black. - Never A/B a capture against one taken earlier in the session — streaming, LOD and lighting drift between moments; after a material change, recompile and discard one warm-up capture or the thumbnail shows the default checker.


