Split screen

Two SubViewports side by side with one camera each, both looking at the same World3D. Each pane follows its own cube.

The editor above is running this project. Change a line and it reloads.

Godot classes used

SubViewportSubViewportContainerCamera3D

The code

scripts/main.gd
extends Node

const SPEED: float = 6.0

var blue: Node3D
var green: Node3D
var blue_camera: Camera3D
var green_camera: Camera3D
var elapsed: float = 0.0
var manual: bool = false


func _ready() -> void:
	_add_environment()
	_add_light()
	_add_arena()

	blue = _add_runner(Color('#8da5f3'))
	green = _add_runner(Color('#8eef97'))

	var row: HBoxContainer = HBoxContainer.new()
	row.add_theme_constant_override('separation', 6)
	add_child(row)
	row.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)

	blue_camera = _add_pane(row)
	green_camera = _add_pane(row)

	_add_hud()
	_place_cameras()

	print('two SubViewports share one World3D, wasd drives the blue cube and the arrows the green one')


func _process(delta: float) -> void:
	elapsed += delta

	var left: Vector3 = Vector3(
		float(Input.is_key_pressed(KEY_D)) - float(Input.is_key_pressed(KEY_A)),
		0.0,
		float(Input.is_key_pressed(KEY_S)) - float(Input.is_key_pressed(KEY_W))
	)
	var right: Vector3 = Vector3(
		float(Input.is_key_pressed(KEY_RIGHT)) - float(Input.is_key_pressed(KEY_LEFT)),
		0.0,
		float(Input.is_key_pressed(KEY_DOWN)) - float(Input.is_key_pressed(KEY_UP))
	)

	if left != Vector3.ZERO or right != Vector3.ZERO:
		manual = true

	if manual:
		blue.position += left.normalized() * SPEED * delta
		green.position += right.normalized() * SPEED * delta
	else:
		blue.position = Vector3(cos(elapsed * 0.7) * 7.0, 0.7, sin(elapsed * 0.7) * 7.0)
		green.position = Vector3(cos(-elapsed * 0.5 + 2.2) * 4.5, 0.7, sin(-elapsed * 0.5 + 2.2) * 4.5)

	blue.rotation.y = elapsed * 1.4
	green.rotation.y = -elapsed * 1.4

	_place_cameras()


func _place_cameras() -> void:
	_aim(blue_camera, blue, Vector3(0, 4.5, 8))
	_aim(green_camera, green, Vector3(6, 5.5, -6))


func _aim(camera: Camera3D, target: Node3D, offset: Vector3) -> void:
	camera.position = target.position + offset
	camera.look_at(target.position, Vector3.UP)


func _add_pane(row: HBoxContainer) -> Camera3D:
	var container: SubViewportContainer = SubViewportContainer.new()
	container.stretch = true
	container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	container.size_flags_vertical = Control.SIZE_EXPAND_FILL
	row.add_child(container)

	var pane: SubViewport = SubViewport.new()
	pane.render_target_update_mode = SubViewport.UPDATE_ALWAYS
	pane.msaa_3d = Viewport.MSAA_4X
	container.add_child(pane)

	var camera: Camera3D = Camera3D.new()
	pane.add_child(camera)
	camera.current = true

	return camera


func _add_runner(color: Color) -> Node3D:
	var mesh: BoxMesh = BoxMesh.new()
	mesh.size = Vector3(1.4, 1.4, 1.4)

	var material: StandardMaterial3D = StandardMaterial3D.new()
	material.albedo_color = color
	material.roughness = 0.4

	var runner: MeshInstance3D = MeshInstance3D.new()
	runner.mesh = mesh
	runner.material_override = material
	add_child(runner)

	return runner


func _add_arena() -> void:
	var ground_mesh: PlaneMesh = PlaneMesh.new()
	ground_mesh.size = Vector2(50, 50)

	var ground_material: StandardMaterial3D = StandardMaterial3D.new()
	ground_material.albedo_color = Color('#2a2f3d')

	var ground: MeshInstance3D = MeshInstance3D.new()
	ground.mesh = ground_mesh
	ground.material_override = ground_material
	add_child(ground)

	var pillar_mesh: BoxMesh = BoxMesh.new()
	pillar_mesh.size = Vector3(1.2, 4, 1.2)

	var pillar_material: StandardMaterial3D = StandardMaterial3D.new()
	pillar_material.albedo_color = Color('#fc7f7f')
	pillar_material.roughness = 0.6

	for i in 14:
		var angle: float = TAU * i / 14.0

		var pillar: MeshInstance3D = MeshInstance3D.new()
		pillar.mesh = pillar_mesh
		pillar.material_override = pillar_material
		pillar.position = Vector3(cos(angle), 0, sin(angle)) * 17.0 + Vector3(0, 2, 0)
		add_child(pillar)


func _add_hud() -> void:
	var hud: CanvasLayer = CanvasLayer.new()
	hud.layer = 2
	add_child(hud)

	var view: Vector2 = get_viewport().get_visible_rect().size

	for item in [{ text = 'P1  wasd', color = Color('#8da5f3'), x = 40.0 }, { text = 'P2  arrows', color = Color('#8eef97'), x = view.x * 0.5 + 40.0 }]:
		var label: Label = Label.new()
		label.text = item.text
		label.add_theme_font_size_override('font_size', 24)
		label.add_theme_color_override('font_color', item.color)
		label.position = Vector2(item.x, 30)
		hud.add_child(label)


func _add_environment() -> void:
	var sky_material: ProceduralSkyMaterial = ProceduralSkyMaterial.new()
	sky_material.sky_top_color = Color(0.05, 0.06, 0.12)
	sky_material.sky_horizon_color = Color(0.14, 0.16, 0.24)
	sky_material.ground_bottom_color = Color(0.02, 0.02, 0.04)
	sky_material.ground_horizon_color = Color(0.1, 0.11, 0.16)

	var sky: Sky = Sky.new()
	sky.sky_material = sky_material

	var env: Environment = Environment.new()
	env.background_mode = Environment.BG_SKY
	env.sky = sky
	env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
	env.ambient_light_color = Color(0.44, 0.48, 0.6)
	env.ambient_light_energy = 0.7

	var world: WorldEnvironment = WorldEnvironment.new()
	world.environment = env
	add_child(world)


func _add_light() -> void:
	var light: DirectionalLight3D = DirectionalLight3D.new()
	light.rotation_degrees = Vector3(-70, -35, 0)
	light.light_energy = 1.2
	light.shadow_enabled = true
	add_child(light)

More Camera examples