reckfellENGINE

Viewports: worlds, input and frame capture

Independent previews, update modes, sizing and correct PNG capture.

On this page

A viewport renders a world into a surface. The root Window is the game's main viewport; a SubViewport provides another render target for a model preview, minimap, split-screen view or texture displayed inside a scene.

Creating a SubViewport does not by itself display its image. Show GetTexture() through a TextureRect, Sprite or material, or place the SubViewport directly under a SubViewportContainer. A texture displayed on a 3D mesh does not automatically forward clicks to the viewport.

Build an independent 3D preview

  1. Create a SubViewport with a nonzero size of at least 2 × 2 pixels.
  2. Assign a new World3D if the preview must be isolated from the main game's rendering/physics world.
  3. Add the preview content and a Camera3D below that SubViewport. Make the intended camera current. Give lit materials a suitable light and Environment; a camera alone does not illuminate them.
  4. Set an appropriate update mode. Use Always for a continuously animated offscreen preview, or request Once for a single refresh.
  5. Display its GetTexture() where the player should see it. Use the actual SubViewport output size to budget render cost.

The helper below creates an isolated, manually managed viewport and saves a completed frame. It does not populate a scene or create a finished-looking model preview.

C#
using System;
using System.Threading.Tasks;
using Reckfell;

public static class ViewportTools
{
    public static SubViewport CreateIndependent3D(Entity parent, Vector2I size)
    {
        if (!parent.IsInsideTree() || size.X < 2 || size.Y < 2)
            throw new ArgumentException("Use a parent in the tree and a size of at least 2 by 2.");
        var view = new SubViewport
        {
            Size = size,
            World3D = new World3D(),
            RenderTargetUpdateMode = SubViewport.UpdateMode.Disabled,
        };
        parent.AddChild(view);
        return view;
    }

    // Call on the main thread. Keep the viewport alive until this task completes.
    // Use on a manually managed viewport, not one controlled by a container.
    public static async Task<Error> SavePngAsync(SubViewport view, string path)
    {
        if (!view.IsInsideTree() || view.Size.X < 2 || view.Size.Y < 2 ||
            view.GetParent() is SubViewportContainer)
            throw new ArgumentException("Use a valid, manually managed SubViewport.");
        var previous = view.RenderTargetUpdateMode;
        try
        {
            view.RenderTargetUpdateMode = SubViewport.UpdateMode.Once;
            await view.ToSignal(RenderingServer.Singleton, RenderingServer.SignalName.FramePostDraw);
            using var image = view.GetTexture().GetImage();
            return image.SavePng(path);
        }
        finally
        {
            view.RenderTargetUpdateMode = previous;
        }
    }
}

After CreateIndependent3D(this, new Vector2I(512, 512)), add your preview camera/content to the returned viewport. It starts with rendering Disabled. For a live preview, set RenderTargetUpdateMode to Always; for a one-shot PNG, await ViewportTools.SavePngAsync(view, "user://preview.png") and check for Error.Ok.

Call the async helper from the main thread while the render loop is running. Keep the viewport alive until it completes. It requests one frame, waits for RenderingServer.FramePostDraw, saves the Image and restores the previous requested update mode. It refuses a viewport managed by a SubViewportContainer, which owns its sizing/visibility behavior. GPU readback and file writing can stall; do not use this helper to record every game frame. PNG is not a raw HDR capture.

Understand update and clear modes

Update modeIntended use
DisabledKeep the last rendered texture until another update is requested.
OnceRender a frame, then stop automatic rendering. Assign Once again to request another frame.
WhenVisibleUpdate when the texture is used visibly; this is not simply an Entity visibility flag.
WhenParentVisibleTie updates to the parent viewport's visibility.
AlwaysUpdate even if nobody displays the texture. Costs rendering work every frame.

After Once is consumed, the SubViewport property getter still returns the requested Once in 1.0.0. Do not use it as a “new frame pending” or completion flag. Await frame completion and inspect the expected result. Restoring a previous Once request can schedule another one-shot; use Disabled as the resting state for a manually captured preview.

Disabling viewport rendering does not pause child Entities, physics or _Process(). Pause or manage those separately when needed. RenderTargetClearMode is a separate choice: clear every rendered frame, never clear, or clear once. Never clearing can leave previous pixels, which is useful for deliberate accumulation but often explains unwanted trails. It does not control update frequency.

Share worlds deliberately

RequirementConfiguration
Two 3D cameras viewing the same gameLeave the child World3D unassigned to inherit, or assign the same World3D explicitly.
An isolated 3D previewAssign a new World3D. Objects register in their nearest viewport's effective world.
Resolve what world a viewport really usesCall FindWorld3D(). The World3D property can be null while the effective world is inherited.
A viewport-owned 3D worldEnable OwnWorld3D. It takes precedence over an assigned/inherited source; it does not clone the Entity hierarchy.
Independent 2D contentThe default: each viewport has its own World2D.
Shared 2D contentAssign the same World2D explicitly; resolve it with FindWorld2D().

OwnWorld3D can duplicate an assigned World3D resource; the World3D property remains the assigned source, while FindWorld3D() returns the effective owned world. Disabling OwnWorld3D restores use of the source/inherited world. A separate World3D is not a copy of your game scene.

Camera selection is per viewport. Making one Camera3D current replaces the current camera in that viewport, not a camera in another viewport. A shared world does not mean a shared camera. Physics interaction across separate 3D worlds requires a deliberate bridge; sharing the EntityTree alone does not merge worlds.

Size, containers and 2D coordinates

  • SubViewport.Size is render-target resolution. Scaling a TextureRect that displays it does not create more detail in that target.
  • A direct parent SubViewportContainer with Stretch = true controls child resolution. StretchShrink = 2 makes a 128 × 96 container render a 64 × 48 child texture. Change the container/shrink setting rather than fighting its child Size setter.
  • Size2DOverride plus Size2DOverrideStretch = true changes the 2D logical coordinate area. It does not change output texture dimensions: a 128 × 96 target can show a stretched 64 × 48 canvas.
  • A container's show/hide handling can change its child update mode. Do not treat a container-owned viewport like an independent manually scheduled target.

Root-window scaling is configured separately; see multiple resolutions.

Route input to the right surface

A direct SubViewportContainer parent handles its child's display/input path, subject to Control focus, mouse filtering and visibility. A TextureRect, Sprite or 3D material displaying a ViewportTexture is not equivalent.

For manual forwarding, convert positions from the displayed image into target viewport coordinates, accounting for stretch, crop, aspect bars and any 3D hit/UV mapping. Then use PushInput(event, inLocalCoords: true) for an event already in viewport coordinates. The flag does not calculate your mapping; with false, the engine applies its screen-to-local transform, which cannot infer an arbitrary mesh's UVs.

PushInput runs the viewport input pipeline: _Input, GUI, then unhandled stages if not consumed. SetInputAsHandled() prevents later unhandled processing. Despite its name, GuiDisableInput blocks the PushInput entry path, including _Input, not only GUI controls. Input-handled state may be shared through a container's parent; do not assume every child has isolated consumption state.

Diagnose a blank, frozen or incorrect preview

SymptomCheck
Empty/old first captureWait for frame completion after requesting an update; do not read immediately in _Ready().
Preview never refreshesUpdate mode and whether the texture is actually displayed; use Always only when continuous offscreen updates are needed.
Main-world objects appear in an isolated previewResolve FindWorld3D(); an unassigned World3D can inherit the parent.
Camera change affects the wrong viewThe camera's nearest viewport and that viewport's current camera.
Size assignment is ignoredA stretching parent SubViewportContainer may own it.
Clicks miss controlsCoordinate conversion, input disable flag, focus and event consumption.
3D shadow is missingLight/caster settings and viewport shadow-atlas configuration; a viewport does not inherit every desired quality setting automatically.
Spatial audio is absentCheck the viewport's intended 2D/3D listener configuration, not just its camera.

The native checks use Windows Forward+/Vulkan: actual pixels, world/camera selection, update/processing separation, synthetic input, sizing and PNG capture. Physical pointer forwarding, audio listening, other renderers, platforms and exports are not claimed tested by this example.

Reckfell Engine documentationBack to top ↑
QUICK LINKSGetting startedEntity hierarchyC# & .NET 10Scenes and prefabs
Reckfell guides and full API K
reckfellENGINE
RECKFELL ACCOUNT

Sign in

Sign in to your Reckfell account.

One account for the entire Reckfell platform.