Quick answer: Set the SubViewport’s size to a non-zero value, set update_mode to UPDATE_ALWAYS for live content, ensure a Camera3D (or Camera2D) exists inside the SubViewport, and display it via a SubViewportContainer or a TextureRect with a ViewportTexture.

Your SubViewport renders a solid black rectangle. Or it showed something once and then froze. Maybe you’re building a minimap, a 3D model viewer inside a 2D UI panel, or a security camera effect — all of these are common SubViewport use cases that hit the same cluster of setup pitfalls. Let’s go through every one of them.

The Symptom

You will encounter one of these three failure modes:

What Causes This

A SubViewport is an off-screen render target. It renders its child scene independently of the main viewport, but it has no visible output by default. To display its content, you must connect it to a display surface. Three additional settings must be correct before anything appears:

The Fix: SubViewportContainer (Simplest)

The easiest way to display a SubViewport in a 2D UI is to make it a child of a SubViewportContainer. The container automatically stretches the SubViewport output to fill its rect and handles input forwarding.

# Scene tree:
# SubViewportContainer  (Control node, sized to desired display area)
#   SubViewport         (set size to match container, or use stretch = true)
#     Camera3D
#     MeshInstance3D (your 3D content)

# In GDScript on the SubViewportContainer:
func _ready() -> void:
    var vp: SubViewport = $SubViewport
    vp.size = Vector2i(512, 512)
    vp.render_target_update_mode = SubViewport.UPDATE_ALWAYS
    vp.transparent_bg = true   # required for overlay use cases

Enable Stretch on the SubViewportContainer if you want the viewport to automatically match the container’s size as it resizes.

The Fix: ViewportTexture on Sprite2D or TextureRect

When you need the SubViewport output as a texture on a mesh, sprite, or UI element outside a container, use a ViewportTexture. This is also how you render a 3D scene onto a surface in a 2D game:

func _ready() -> void:
    var vp: SubViewport = $ModelViewport
    vp.size = Vector2i(256, 256)
    vp.render_target_update_mode = SubViewport.UPDATE_ALWAYS

    # Assign the SubViewport's output to a TextureRect
    var tex_rect: TextureRect = $UI/ModelPreview
    var vp_tex = ViewportTexture.new()
    vp_tex.viewport_path = vp.get_path()
    tex_rect.texture = vp_tex

# Or via the Inspector:
# TextureRect > Texture > New ViewportTexture > assign SubViewport node

The ViewportTexture’s viewport_path must point to the correct node path at the time the texture is used. If the SubViewport is added dynamically, assign the path after it has been added to the scene tree, not before.

Fixing update_mode: The Frozen Frame Problem

If your SubViewport renders a single correct frame and then freezes, the cause is update_mode = UPDATE_ONCE. This is useful for static previews (render a thumbnail, save it, stop rendering) but wrong for live content. Switch to UPDATE_ALWAYS for animated scenes:

# Constants on SubViewport:
#   UPDATE_DISABLED  - never renders automatically
#   UPDATE_ONCE      - renders one frame, then reverts to DISABLED
#   UPDATE_ALWAYS    - renders every frame (default for live content)
#   UPDATE_WHEN_VISIBLE - only renders when visible in the viewport

var vp: SubViewport = $SubViewport

# For a live 3D model viewer:
vp.render_target_update_mode = SubViewport.UPDATE_ALWAYS

# For a static screenshot / thumbnail (renders once, saves GPU):
vp.render_target_update_mode = SubViewport.UPDATE_ONCE
# Trigger a fresh render on demand:
await RenderingServer.frame_post_draw
var img = vp.get_texture().get_image()   # capture the rendered frame

transparent_bg and the Camera Inside

Two more settings that frequently cause the black screen:

transparent_bg. By default SubViewport.transparent_bg = false, which means the background is rendered as solid black. If you are compositing the SubViewport output over your UI (e.g. a 3D character floating over a 2D menu), set transparent_bg = true and make sure the display surface (TextureRect, SubViewportContainer) has a transparent background too.

Camera inside the SubViewport. Each SubViewport is an isolated render context. It does not share the main scene’s camera. For a 3D SubViewport, add a Camera3D as a child (or grandchild) of the SubViewport. For a 2D SubViewport rendering UI, a Camera2D is optional but the SubViewport’s own 2D canvas is always active:

# Minimal working 3D-in-2D setup (scene tree):
# SubViewportContainer
#   SubViewport  (size: 512x512, update_mode: UPDATE_ALWAYS)
#     Camera3D   (positioned to frame the model)
#     DirectionalLight3D
#     MeshInstance3D  <-- the model you want to show

# GDScript to rotate the model in the SubViewport:
@onready var model: MeshInstance3D = $SubViewport/MeshInstance3D

func _process(delta: float) -> void:
    model.rotate_y(delta * 1.2)

Related Issues

See also: Fix: Godot ViewportTexture Showing Black Screen.

See also: Fix: Godot Render Target Not Updating After Scene Change.

Set the size, set a camera, set UPDATE_ALWAYS — three lines and your SubViewport comes to life.