[Godot] Making Use of SubViewport: Minimaps, 3D Previews, and Render Textures

Created: 2025-12-08Last updated: 2026-07-08

An explanation of Godot's SubViewport starting from the core concept of an independent virtual screen. Covers building minimaps, 3D inventory previews, and security-camera-style render textures, plus performance optimization through Update Mode and resolution, with concrete code and diagrams.

While making games in Godot, you run into situations where you want another screen inside the screen. A minimap in the corner for surveying the map. Equipment shown as a 3D model you can spin around in the inventory. Live footage from another location, like a security camera or a portal.

These are hard to pull off with a single screen, which is where the dedicated SubViewport node comes in. SubViewport creates a "virtual screen" independent of the main one and lets you apply its rendered result as a texture wherever you like. Once you have this, your UI and graphics toolbox expands considerably.

A SubViewport concept: a small sub-screen (a separate world with a camera and a cube) inside a large screen frame, with its footage being transferred to the main screen

What You'll Learn

  • What SubViewport really is: an independent rendering world plus ViewportTexture
  • How to build a minimap (rendered separately by an overhead camera and shown in a corner)
  • Security-camera-style render textures combined with shaders
  • Hands-on: a 3D inventory preview (equipment you spin by dragging)
  • Performance optimization through Update Mode, resolution, and cull_mask

Sponsored

The Core Concept of SubViewport: An Independent Rendering World

The key to understanding SubViewport is a single fact: it is independent of the main rendering. A scene placed under this node has its own camera, world, and rendering settings, and is drawn somewhere else.

ViewportTexture is the bridge that brings that rendered result onto the main screen. Turn a SubViewport's output into this texture and you can display its footage anywhere a texture can go: a TextureRect (for UI), a Sprite2D, the material of a 3D model, and so on.

A diagram of the flow where a SubViewport (an independent virtual screen showing a camera and a cube) is applied to a TextureRect, a Sprite2D, and a 3D mesh on the main screen via a ViewportTexture

Basic Setup

  1. Add the nodes: add a SubViewportContainer and place a SubViewport as its child (the container makes size management easier).
  2. Build the contents: as children of the SubViewport, build the scene you want to show (2D/3D nodes plus a camera).
  3. Display it: place a separate TextureRect, set its Texture to a "New ViewportTexture," and point it at the target SubViewport.

Once you have this flow of "turn an independent screen into a texture and apply it," every use case below is an application of the same idea.

Sponsored

Use Case 1: Building a Minimap

A minimap is the textbook example of SubViewport. You render the world separately with an overhead camera that follows the player and show that footage small in a corner of the screen. Being able to have a camera separate from the main one is exactly what lets you view the same world from directly above at the same time.

A game screen with the player standing in the center and a minimap frame in the bottom-right corner showing a dark blue dot for the player and light blue dots for enemies

Here's practical code that dynamically shows icons for enemies and items, not just the player. Attaching it to the SubViewport itself keeps the minimap's drawing logic separate from the main game logic.

extends SubViewport

@onready var minimap_camera: Camera2D = $MinimapCamera

# Cache enemy icons for reuse (avoids creating and searching every frame)
var enemy_icons: Dictionary = {}
const ICON_NORMAL = preload("res://assets/enemy_icon.png")
const ICON_ALERT  = preload("res://assets/enemy_alert_icon.png")

func _process(_delta: float) -> void:
    var player := get_tree().get_first_node_in_group("player")
    if not is_instance_valid(player):
        return
    # Make the overhead camera follow the player
    minimap_camera.global_position = player.global_position

    for enemy in get_tree().get_nodes_in_group("enemies"):
        if not is_instance_valid(enemy):
            continue
        # Create the icon once and cache it if it doesn't exist yet
        if not enemy_icons.has(enemy):
            var new_icon := Sprite2D.new()
            add_child(new_icon)
            enemy_icons[enemy] = new_icon
        var icon: Sprite2D = enemy_icons[enemy]
        # Swap the icon depending on whether the enemy is alerted
        icon.texture = ICON_ALERT if enemy.is_in_alert_state() else ICON_NORMAL
        icon.global_position = enemy.global_position

The key point is that the minimap's contents (camera and icons) are fully contained inside the SubViewport. The main game can focus on its own rendering, and the minimap's display logic is understandable by looking at this one node. The roles separate cleanly.

Sponsored

Use Case 2: A Security-Camera Render Texture

Combining a SubViewport's footage with a shader gets you effects that go beyond simple display. The classic is a security camera: footage of another location with scanlines and noise laid over it.

A security camera diagram: a Camera3D in another room films the subject, and its footage is shown with scanlines on a monitor mesh in the foreground via a ViewportTexture
  1. Place a SubViewport and a Camera3D at the location you want to monitor.
  2. On the main side, prepare a MeshInstance3D to act as the monitor (a PlaneMesh, for example).
  3. Assign a ShaderMaterial to the monitor mesh and lay scanlines and noise over the footage.
shader_type spatial;

uniform sampler2D screen_texture; // The SubViewport's footage is passed in here
uniform float scanline_intensity = 0.1;
uniform float noise_amount = 0.05;

void fragment() {
    // Scanlines based on vertical position
    float scanline = sin(UV.y * 800.0) * scanline_intensity;
    // Build pseudo-random noise from the position
    float noise = (fract(sin(dot(UV, vec2(12.9898, 78.233))) * 43758.5453) - 0.5) * noise_amount;

    vec4 tex = texture(screen_texture, UV);
    ALBEDO = tex.rgb - scanline - noise; // Darken and disturb the footage a little for that security-camera look
}

Finally, pass the SubViewport's footage (get_texture()) into the shader's screen_texture.

extends MeshInstance3D

@export var camera_viewport: SubViewport # Point this at the security camera's SubViewport in the Inspector

func _ready() -> void:
    if not camera_viewport:
        return
    var mat := get_surface_override_material(0) as ShaderMaterial
    if mat:
        # Bridge the SubViewport's output texture into the shader
        mat.set_shader_parameter("screen_texture", camera_viewport.get_texture())

Running scanlines and noise across a whole screen is an application of Fragment Shader Basics. SubViewport supplies the raw footage and the shader handles the processing. Remembering it as that division of labor makes it easy to keep straight.

Sponsored

Hands-On: Building a 3D Equipment Preview for an Inventory

Let's turn "apply an independent screen as a texture" into a genuinely satisfying piece of UI: a rotatable 3D preview of equipment on the inventory screen, spun by dragging.

An RPG's equipment screen, checking your appearance in a character creator, showing gacha results in 3D. There are plenty of situations where you want a 3D model inside a 2D UI, and they all use the same SubViewport pattern.

An inventory UI with equipment slots (helmet, armor, gloves, boots) on the left and, in the center, a 3D helmet model on a turntable with rotation arrows and a cursor dragging it around

Here's the structure. Inside the SubViewport, build a minimal 3D environment to light the model.

- PreviewContainer (SubViewportContainer)  … receives mouse input
  - SubViewport
    - Camera3D
    - DirectionalLight3D
    - WorldEnvironment          … background and ambient light
    - TargetModel (Node3D)      … the equipment model you want to rotate

SubViewportContainer can receive GUI input, so pick up the drag there and rotate the model.

extends SubViewportContainer

@onready var model: Node3D = $SubViewport/TargetModel

var dragging := false

func _ready() -> void:
    mouse_filter = Control.MOUSE_FILTER_STOP # Make sure input reaches us

func _gui_input(event: InputEvent) -> void:
    if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
        dragging = event.pressed          # Only rotate while the button is held
    elif event is InputEventMouseMotion and dragging:
        # Rotate around Y by the horizontal drag distance
        model.rotate_y(deg_to_rad(-event.relative.x * 0.5))

There are two points to take away.

  • 2D UI and 3D models can live on one screen: place a single SubViewportContainer with a SubViewport on an otherwise 2D inventory screen and a 3D model becomes just another UI part. To switch equipment, you only swap out the contents of TargetModel.
  • SubViewportContainer handles the input: hit-testing a 3D model directly is a hassle, but the container picks up drags through _gui_input, so rotation and zoom controls stay simple to write.

When you want spring-back or inertia on the rotation, interpolating rotation with a Tween makes it feel much better.

Sponsored

Performance and Optimization

SubViewport is powerful, but it creates an additional rendering pass, so using it carelessly gets expensive. Update frequency and resolution matter most.

A contrast showing that Update Mode Always redraws every frame and costs more, while When Visible only draws while visible and stays cheap
  • Match Update Mode to the use case: always-on Always is the last resort. When it only needs drawing while open, like a 3D preview in a UI, use When Visible; when a still image is enough, use Once; for manual control, use Disabled. Reserve Always for things that move every frame, like a minimap.
  • Keep the resolution minimal: if the 3D preview is 256x256 px on screen, 256x256 is plenty for the SubViewport's resolution. Drawing larger than the display size is waste.
  • Narrow what gets drawn with cull_mask (3D): Camera3D's cull_mask lets you render only the layers that belong in that viewport. It saves you from having a preview camera draw the entire background stage.

"Build it first, and if it's slow, start with Update Mode and resolution" is good enough. On mobile in particular, keep in the back of your mind that each SubViewport adds draw calls.

Sponsored

Common Mistakes and Best Practices

Common MistakeBest Practice
Leaving Update Mode on Always everywhereUpdate only when needed. When Visible for UI, Disabled for manual control. Avoid per-frame updates
Rendering at a higher resolution than the display sizeMinimize the resolution to match the display size (256x256 for a 3D preview, for instance)
Ignoring a 3D camera's cull_maskSeparate rendering layers so a viewport doesn't draw objects it doesn't need
Cramming everything into one SubViewportSplit by role (one for the minimap, one for the preview). It makes individual optimization easier
Not considering a cheaper alternativeWeigh cost against expression. A static preview can just be an image, and a simple minimap can be drawn with UI

Bonus: Good to Know for Later

Once you can build the three basic patterns, these are the next things that widen your options.

  • Passing input through or stopping it: SubViewportContainer's mouse_filter and SubViewport's Handle Input Locally decide whether clicks reach the 3D scene inside or get caught by the container. For 3D previews, catching them at the container is the default.
  • Full-screen post-processing: render your whole game into a SubViewport and apply a shader to that footage, and you can apply Fragment Shader post-processing such as chromatic aberration, blur, or a retro look across the entire screen.
  • Layer with effects and particles: adding particles and light from GPUParticles2D to a preview or monitor makes the presentation more convincing.
  • Start from the 3D basics: if you're unsure how to place the Camera3D and lights for a 3D preview, Node3D and MeshInstance3D basics is the foundation.

Summary

  • SubViewport is a "virtual screen" with its own camera and scene. Turn its output into a texture with ViewportTexture (get_texture()) and apply it
  • Minimaps render separately with an overhead camera, security cameras process the footage with a shader, and 3D previews put 3D inside a 2D UI. All are applications of the same "apply it as a texture" idea
  • Optimization rests on three pillars: Update Mode (When Visible/Once), minimizing resolution, and cull_mask
Use CaseMain PurposeKey to Performance
MinimapShow the game world from aboveLimit what's drawn with cull_mask
3D previewShow a 3D model inside a 2D UIMinimize resolution, use When Visible
Render textureProcess separate footage with a shaderBalance update frequency against shader cost

Start by placing one SubViewportContainer plus SubViewport, dropping a Camera3D and any old mesh inside, and getting a feel for "3D showing up inside a 2D screen." From there you can grow it into a minimap or a preview.

Further Reading