Godotでゲームを作っていると、こんな「画面の中にもう一つの画面が欲しい」場面に出会います。マップを見渡す ミニマップ を隅に置きたい。インベントリで装備を 3Dモデルでぐるぐる回して見せたい。監視カメラやポータルのように 別の場所の映像をリアルタイムに映したい。
これらは通常の1画面だけでは実現が難しく、専用のノード SubViewport の出番です。SubViewport は メイン画面とは独立した「仮想の画面」 を作り、その描画結果を テクスチャとして 自由に貼り付けられる機能。これを押さえると、UIとグラフィック表現の引き出しが一気に増えます。
この記事でわかること
SubViewportの正体—— 独立したレンダリング世界 とViewportTexture- ミニマップ の作り方(俯瞰カメラで別描画して隅に表示)
- 監視カメラ風 のレンダーテクスチャ+シェーダー演出
- 実践: インベントリの3Dプレビュー(ドラッグで回る装備)
Update Mode・解像度・cull_maskによる パフォーマンス最適化
SubViewportのコア概念:独立したレンダリング世界
SubViewport を理解する鍵は、 「メインの描画から独立している」 という一点です。このノードの下に置いたシーンは、独自のカメラ・ワールド・レンダリング設定を持ち、 別の場所で描かれます 。
そして、その描画結果をメイン画面に持ち込む橋渡し役が ViewportTexture です。SubViewport の出力をこのテクスチャにすると、TextureRect(UI用)・Sprite2D・3Dモデルのマテリアルなど、 テクスチャを貼れる場所ならどこにでも その映像を映せます。

基本的なセット アップ
- ノード追加 :
SubViewportContainerを追加し、その子にSubViewportを置く(コンテナを使うとサイズ管理が楽)。 - 中身を作る :
SubViewportの子として、映したいシーン(2D/3Dノードとカメラ)を組む。 - 表示する :別途
TextureRectを置き、そのTextureに「新規ViewportTexture」を設定して、対象のSubViewportを指定する。
この「独立した画面をテクスチャ化して貼る」流れさえ掴めば、以降の活用法はすべてこの応用です。
活用法1:ミニマップを作る
ミニマップは SubViewport の代表例です。 プレイヤーを追う俯瞰カメラで世界を別描画し、その映像を画面の隅に小さく表示 します。メイン画面とは別のカメラを持てるからこそ、同じ世界を「真上から」同時に映せるわけです。

プレイヤーだけでなく、敵やアイテムのアイコンも動的に表示する実践的なコードです。SubViewport 自身にアタッチして、ミニマップの描画ロジックをメインのゲームロジックから分離します。
extends SubViewport
@onready var minimap_camera: Camera2D = $MinimapCamera
# 敵アイコンを使い回すためキャッシュ(毎フレームの生成・検索を避ける)
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
# 俯瞰カメラをプレイヤーに追従させる
minimap_camera.global_position = player.global_position
for enemy in get_tree().get_nodes_in_group("enemies"):
if not is_instance_valid(enemy):
continue
# アイコンが無ければ1つだけ生成してキャッシュ
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]
# 警戒中かどうかでアイコンを差し替える
icon.texture = ICON_ALERT if enemy.is_in_alert_state() else ICON_NORMAL
icon.global_position = enemy.global_position
ポイントは、 ミニマップの中身(カメラ・アイコン)が SubViewport の中で完結している ことです。メインのゲームは自分の描画に集中でき、ミニマップの表示ロジックはこのノードだけを見れば分かる——役割がきれいに分かれます。
活用法2:監視カメラ風レンダーテクスチャ
SubViewport の映像を シェーダーと組み合わせる と、単なる表示を超えた演出ができます。定番は、別の場所を映すカメラの映像に走査線やノイズを乗せた 「監視カメラ」 です。

- 監視したい場所に
SubViewportとCamera3Dを置く。 - メイン側にモニターとなる
MeshInstance3D(例:PlaneMesh)を用意する。 - モニターのメッシュに
ShaderMaterialを割り当て、映像に走査線とノイズを乗せる。
shader_type spatial;
uniform sampler2D screen_texture; // SubViewportの映像を ここに渡す
uniform float scanline_intensity = 0.1;
uniform float noise_amount = 0.05;
void fragment() {
// 縦位置に応じた走査線
float scanline = sin(UV.y * 800.0) * scanline_intensity;
// 位置から擬似ランダムなノイズを作る
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; // 映像を少し暗く乱して「監視カメラらしさ」を出す
}
最後に、SubViewport の映像(get_texture())をシェーダーの screen_texture に渡します。
extends MeshInstance3D
@export var camera_viewport: SubViewport # 監視カメラ側のSubViewportをInspectorで指定
func _ready() -> void:
if not camera_viewport:
return
var mat := get_surface_override_material(0) as ShaderMaterial
if mat:
# SubViewportの出力テクスチャをシェーダーへ橋渡し
mat.set_shader_parameter("screen_texture", camera_viewport.get_texture())
走査線やノイズを画面全体に流す表現は Fragment Shaderの基礎 の応用です。SubViewport は「素材となる映像」を用意し、シェーダーが「加工」を担当する——役割分担で覚えると分かりやすいです。
実践:インベントリの3D装備プレビューを組む
ここまでの「独立した画面をテクスチャに貼る」を、満足度の高いUI演出に落とし込みましょう。 インベントリ画面で、装備アイテムの3Dモデルをドラッグで回転プレビューする 機能です。
RPGの装備画面、キャラメイクの外見確認、ガチャ結果の3D表示——2D UIの中に3Dモデルを差し込みたい場面は多く、そのすべてが SubViewport の同じ型で作れます。

構成はこうです。SubViewport の中に、モデルを照らす最小限の3D環境を組みます。
- PreviewContainer (SubViewportContainer) … マウス入力を受ける
- SubViewport
- Camera3D
- DirectionalLight3D
- WorldEnvironment … 背景・環境光
- TargetModel (Node3D) … 回転させたい装備モデル
SubViewportContainer はGUI入力を受け取れるので、そこでドラッグを拾ってモデルを回します。
extends SubViewportContainer
@onready var model: Node3D = $SubViewport/TargetModel
var dragging := false
func _ready() -> void:
mouse_filter = Control.MOUSE_FILTER_STOP # 入力を確実に受け取る
func _gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
dragging = event.pressed # 押している間だけ回す
elif event is InputEventMouseMotion and dragging:
# 横方向のドラッグ量ぶんだけY軸回転
model.rotate_y(deg_to_rad(-event.relative.x * 0.5))
ポイントは2つです。
- 2D UIと3Dモデルを1画面に同居させられる :本来2Dのインベントリ画面に、
SubViewportを貼ったSubViewportContainerを1つ置くだけで、3Dモデルがそのまま「UIパーツ」になります。装備を切り替えたいときはTargetModelの中身を差し替えるだけです。 - 入力は
SubViewportContainerが受ける :3Dモデルを直接クリック判定するのは面倒ですが、コンテナが_gui_inputでドラッグを拾ってくれるので、回転や拡大の操作がシンプルに書けます。
回転にバネのような戻りや慣性を付けたくなったら、Tween で rotation を補間すると気持ちよくなります。
パフォーマンスと最適化
SubViewport は強力な反面、 追加のレンダリングパス を生むため、無計画に使うと重くなります。とくに効くのが更新頻度と解像度です。

Update Modeを用途に合わせる :常時更新のAlwaysは最終手段。UIの3Dプレビューのように 開いている時だけ描けばよい ならWhen Visible、静止画で足りるならOnce、手動制御はDisabledを選びます。ミニマップのように毎フレーム動くものだけAlwaysにします。- 解像度を最小限に :3Dプレビューが画面上で256×256pxなら、
SubViewportの解像度も256×256で十分。表示サイズより大きく描くのは無駄です。 cull_maskで描画対象を絞る(3D) :Camera3Dのcull_maskで、そのビューポートに映すべきレイヤーだけを描画対象にできます。プレビュー用カメラに背景ステージまで描かせない、といった無駄を省けます。
「まず作って、重かったら Update Mode と解像度から見直す」で十分です。とくにモバイル向けでは、各 SubViewport がドローコールを増やすことを頭の隅に置いておきましょう。
よくある間違いとベストプラクティス
| よくある間違い | ベストプラクティス |
|---|---|
Update Mode を常に Always にする | 必要な時だけ更新 。UIなら When Visible、手動なら Disabled。毎フレーム更新を避ける |
| 表示サイズより高い解像度で描く | 表示サイズに合わせて解像度を最小化 (3Dプレビューなら256×256など) |
3Dカメラの cull_mask を放置 | 描画レイヤーを分離 し、そのビューポートに不要なオブジェクトを描かせない |
1つの SubViewport に何もかも詰める | 役割ごとに分割 (ミニマップ用・プレビュー用)。個別最適がしやすくなる |
| 軽い代替を検討しない | コストと表現を天秤に 。静止プレビューはただの画像、単純なミニマップはUI描画でも足りる |
おまけ:先に知っておくと良いこと
基本の3パターンが作れたら、次はこのあたりを知っておくと表現が広がります。
- 入力を通すか止めるか :
SubViewportContainerのmouse_filterと、SubViewportのHandle Input Locallyで、クリックを中の3Dシーンまで届けるか、コンテナで受け止めるかを切り替えられます。3Dプレビューでは「コンテナで受ける」が基本です。 - 画面全体のポストプロセス :
SubViewportにゲーム全体を描いてから、その映像にシェーダーをかけると、色収差・ブラー・レトロ風などの Fragment Shader 系ポストプロセスを画面全体に適用できます。 - エフェクトや粒と重ねる :プレビューやモニタ ーに GPUParticles2D の粒や光を足すと、演出の説得力が増します。
- 3D空間の基礎から :3Dプレビューで
Camera3Dやライトの置き方に迷ったら、Node3DとMeshInstance3Dの基本 が土台になります。
まとめ
SubViewportは、独立したカメラ・シーンを持つ「仮想の画面」。その出力をViewportTexture(get_texture())でテクスチャ化して貼る- ミニマップ は俯瞰カメラで別描画、監視カメラ はシェーダーで加工、3Dプレビュー は2D UIに3Dを同居——すべて同じ「テクスチャに貼る」応用
- 最適化 は
Update Mode(When Visible/Once)・解像度の最小化・cull_maskが三本柱
| 活用法 | 主な目的 | パフォーマンスの鍵 |
|---|---|---|
| ミニマップ | ゲーム世界を俯瞰で見せる | cull_mask で描画対象を限定 |
| 3Dプレビュー | 2D UI内で3Dモデルを見せる | 解像度を最小化・When Visible |
| レンダーテクスチャ | 別映像をシェーダーで加工 | 更新頻度とシェーダー負荷のバランス |
まずは SubViewportContainer + SubViewport を1つ置き、中に Camera3D と適当なメッシュを入れて「2D画面の中に3Dが映る 」感覚を掴んでみてください。そこからミニマップにもプレビューにも広げられます。
さらに学ぶために
- Fragment Shaderの基礎 ——レンダーテクスチャを加工する走査線・ノイズ・ポストプロセス
- Node3DとMeshInstance3Dで学ぶ3D空間 ——3Dプレビューのカメラ・ライト配置の土台
- GPUParticles2Dで実現するエフェクト ——プレビューやモニターに重ねる粒の演出
- Tweenで実現する滑らかなアニメーション ——プレビュー回転に慣性や戻りを付ける
- Godot公式ドキュメント:Viewports ——SubViewportとViewportTextureの一次情報