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:
- Solid black output. The SubViewport renders but shows nothing — usually because size is zero, there is no camera, or the background is black with no scene content visible.
- Renders once, then freezes. The first frame appears correctly but the image never updates. This is almost always
update_mode = UPDATE_ONCE. - Nothing displayed at all. The SubViewport exists in the tree but its output does not appear anywhere in the game window, because it has no display surface assigned.
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:
- Size must be non-zero. A newly created SubViewport has
size = Vector2i(0, 0). Nothing renders into zero pixels. - A camera must exist inside it. If you are rendering a 3D scene, the SubViewport needs its own
Camera3D. There is no shared camera with the main viewport. update_modemust beUPDATE_ALWAYSfor animated content. The default in some configurations isUPDATE_ONCE, which renders a single frame and stops.
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’sviewport_pathmust 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.