Introduction: Taking Game Expression to the Next Level with SubViewport
Have you encountered these challenges while developing in Godot Engine?
- Want to place a minimap in the corner of the screen to clearly show a vast map to players.
- Want to preview items in 3D models that characters are equipping in the inventory screen.
- Want effects like portals or security cameras that display real-time views of other locations.
These challenges are difficult to solve with a single game screen alone. However, mastering Godot's powerful SubViewport node makes all of them possible.
SubViewport creates a "virtual screen" independent from the main screen, allowing you to freely reuse its rendering result as a texture. This dramatically improves UI, game logic, and graphic expression.
SubViewport Core Concepts: An Independent Rendering World
The key to understanding SubViewport is that it's "independent from the main rendering pipeline." Scenes placed under this node have their own world, camera, and rendering settings, and are drawn in a completely separate location.
And ViewportTexture serves as the bridge to bring those rendering results into the main scene. By setting the SubViewport's output to this texture, you can apply it to TextureRect (for UI), Sprite2D, or even 3D model materials.
Basic Setup
- Add Nodes: Add a
SubViewportContainerto the scene, then add aSubViewportas its child. (UsingSubViewportContainermakes size management easier) - Create Content: Build the scene you want to display (e.g., 2D/3D nodes, camera) as children of
SubViewport. - Display: Place a
TextureRectat the same level asSubViewportContainer, set itsTextureproperty to "NewViewportTexture", and specify theSubViewportpath in the inspector.
Use Case 1: Implementing a Feature-Rich Minimap
Minimaps are a representative use case for SubViewport. Let's look at implementations that go beyond simple player-following.
Advanced GDScript: Displaying Multiple Targets and Icon Control
Practical code that displays not just the player, but enemies and items on the minimap, changing icons based on state.
# minimap_controller.gd (Attached to SubViewport)
extends SubViewport
@onready var player: CharacterBody2D = get_tree().get_first_node_in_group("player")
@onready var minimap_camera: Camera2D = $MinimapCamera
# Minimap scale (conversion ratio from world coordinates to minimap coordinates)
@export var map_scale: float = 0.1
# Cache references to enemy icons (avoid per-frame node searches)
var enemy_icons: Dictionary = {}
# Cache textures with preload (avoid per-frame load())
const ICON_NORMAL = preload("res://assets/enemy_icon.png")
const ICON_ALERT = preload("res://assets/enemy_alert_icon.png")
func _process(delta: float) -> void:
if not is_instance_valid(player):
return
# Make camera follow player
minimap_camera.global_position = player.global_position
# Convert enemy positions to minimap coordinates and display
update_enemy_icons()
func update_enemy_icons() -> void:
# Get existing enemies and update icons
for enemy in get_tree().get_nodes_in_group("enemies"):
if not is_instance_valid(enemy):
continue
# Create icon if none exists
if not enemy_icons.has(enemy):
var icon = TextureRect.new()
icon.texture = ICON_NORMAL
add_child(icon)
enemy_icons[enemy] = icon
var icon: TextureRect = enemy_icons[enemy]
# Change icon based on state
icon.texture = ICON_ALERT if enemy.is_in_alert_state() else ICON_NORMAL
# Convert world coordinates to minimap local coordinates
# TextureRect uses position (not global_position)
var relative_pos = enemy.global_position - minimap_camera.global_position
icon.position = relative_pos * map_scale + size / 2
This code has the SubViewport itself monitor enemy positions and dynamically create/update icons within the minimap. This separates main game logic from minimap rendering logic.
Use Case 2: Interactive 3D Previews in 2D UI
Player satisfaction greatly improves if they can rotate and view 3D models of characters or items in inventory screens. This is also SubViewport's specialty.
Implementation Steps and Interaction
- Place
SubViewportContainerandSubViewportin the UI scene. - Inside
SubViewport, placeCamera3D,WorldEnvironment(for background and ambient light),DirectionalLight3D, and the 3D model you want to display (MeshInstance3D, etc.). - Attach a script to
SubViewportContainerto receive mouse input and rotate the 3D model.
# 3d_preview_viewport.gd (Attached to SubViewportContainer)
extends SubViewportContainer
@onready var subviewport: SubViewport = $SubViewport
@onready var model: Node3D = $SubViewport/TargetModel # Model to rotate
var is_dragging = false
func _ready() -> void:
# Set mouse_filter to receive mouse input
# Explicitly set as default may not pass input
mouse_filter = Control.MOUSE_FILTER_STOP
func _gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT:
is_dragging = event.is_pressed()
if event is InputEventMouseMotion and is_dragging:
# Rotate model on Y-axis based on mouse movement
model.rotate_y(deg_to_rad(-event.relative.x * 0.5))
Since SubViewportContainer can receive GUI input, this kind of UI coordination is very easy.
Use Case 3: Advanced Visual Effects with Render Textures
SubViewport's true value emerges when combining its output as a texture with shaders. This enables dynamic expression beyond static UI displays.
Use Case: Security Camera with Post-Process Shaders
Apply effects like noise and scan lines to camera footage from another location to create a "security camera" look.
- Set up a
SubViewportandCamera3Dat the location you want to monitor. - In the main scene, prepare a
MeshInstance3D(e.g.,PlaneMesh) as the monitor. - Apply a
ShaderMaterialto the monitor mesh with the following shader code.
// security_camera_shader.gdshader
shader_type spatial;
uniform sampler2D screen_texture;
uniform float noise_amount = 0.05;
uniform float scanline_intensity = 0.1;
void fragment() {
vec2 uv = UV;
// Scan lines
float scanline = sin(uv.y * 800.0) * scanline_intensity;
// Noise
float noise = (fract(sin(dot(uv, vec2(12.9898, 78.233))) * 43758.5453) - 0.5) * noise_amount;
vec4 color = texture(screen_texture, uv);
ALBEDO = color.rgb - scanline - noise;
}
- Use GDScript to pass the
SubViewport'sViewportTextureto the shader'sscreen_textureparameter.
# monitor_screen.gd (Attached to MeshInstance3D)
extends MeshInstance3D
@export var camera_viewport: SubViewport
func _ready() -> void:
if not camera_viewport:
return
var material: ShaderMaterial = self.get_surface_override_material(0)
if material:
var texture = camera_viewport.get_texture()
material.set_shader_parameter("screen_texture", texture)
Performance, Common Mistakes and Best Practices
SubViewport is powerful, but unplanned use leads to performance degradation. Refer to the following table for efficient implementation.
| Common Mistake | Best Practice |
|---|---|
Always setting Update Mode to Always | Update only when needed. Use When Visible for UI displays, Disabled for manual updates, and avoid per-frame updates in _process. |
| Setting unnecessarily high resolution | Optimize resolution. For 3D previews, use minimum resolution matching display size, like 256x256. |
Not considering camera culling_mask | Separate render layers. Have minimap cameras only render layers that should appear on the minimap, eliminating unnecessary calculations. |
Cramming everything into one SubViewport | Split SubViewport by role. Separate for minimap, 3D preview, etc. Makes management easier and allows individual optimization. |
| Not considering alternatives | Balance cost and expression. Keep lighter alternatives in mind: static images for static previews, logic-based UI drawing for simple minimaps. |
Especially in mobile game development, use SubViewport cautiously. Always keep in mind that each viewport generates additional draw calls and rendering passes.
Next Steps and Related Topics
Once you've mastered SubViewport, you can tackle more advanced expressions:
- Dynamic Decals: Render bullet holes or character footprints via
SubViewportand project onto meshes. - Fluid Simulation Visualization: Bake calculation results to textures via
SubViewportto create interactive water surfaces. - Multi-Pass Rendering: Render the same scene multiple times with different shaders or settings to composite outline drawing or special effects.
Godot's official documentation and community tutorials are excellent resources for these topics.
Summary
This article explained SubViewport from basics to applications, with concrete code examples and optimization techniques. SubViewport isn't just a UI part—it's a crucial key for extending Godot's rendering pipeline and dramatically enhancing your game's expressiveness.
| Use Case | Primary Purpose | Performance Key |
|---|---|---|
| Feature-Rich Minimap | Provide bird's-eye view of game world situation. | Optimize Update Mode, limit render targets with culling_mask. |
| Interactive 3D Preview | Attractively display 3D models within 2D UI. | Keep SubViewport resolution to minimum. |
| Render Textures | Combine real-time footage with shaders for advanced visual effects. | Balance shader complexity with viewport update frequency. |
Use these techniques to make your Godot projects even more unique and attractive.