Skip to content

Split Screen

App: apps/split_screen_demo/

Local-multiplayer split-screen (Camera epic, ADR 0059; animated merge ADR 0060). SplitScreen.new(scene, players) gives each player its own third-person camera in its own screen region; the renderer draws the scene once per viewport with a divider between them. With dynamic merge/split the four views fuse into one group-framed camera when the players cluster, and split back into the grid when they spread — Kronnect-style — and the change is an animated blend, not a hard-switch.

import "engine" for Scene, SplitScreen, SplitLayout
var split = SplitScreen.new(scene, players) // players = List of Node
split.layout(SplitLayout.grid) // 2×2 for four players
split.distance(7)
split.height(2)
split.dynamic(true)
split.mergeDistance(5) // spread below this → merge to one view
split.splitDistance(11) // spread above this → split apart (hysteresis)
split.transitionDuration(0.6) // animate the merge/split over 0.6s (0 = instant)

draw() is just scene.draw() + the mesh draws — there is no per-viewport camera code.

Terminal window
% ./plume3d split_screen_demo

Four players hold at the corners (a 2×2 grid split), then sweep to a tight ring at the centre — triggering one animated merge into the group view that frames all four. Screenshots split_grid (spread), split_transition (mid-blend: cells converging, dividers fading), and split_merged (one group view).

GraphicsState.viewports carries a list of {rect, view, projection}; the renderer draws the scene once per viewport into its sub-rect (each with its own mesh-UBO instance), then clears thin divider strips. An empty list is the byte-for-byte legacy single-camera path. Group framing fits all players by their bounding sphere → fov pull-back.

The dynamic state machine decides the target (merged vs split) with a hysteresis band + dwell timer; the visual change is animated by mergeFraction 0→1 over transitionDuration. During the blend the split cells are held in place and each cell’s camera eases from its own player pose toward the shared group pose (smoothstep) while the dividers fade; then the cells collapse to one fullscreen group viewport. Because every cell converges to the same group pose, the cells show identical pixels at the instant they collapse — so there is no overlap artefact and no render-to- texture needed. transitionDuration(0) restores the instant switch.

v1 scope: up to 4 viewports; overlays (sprites/text/UI) render once full-screen. The Kronnect rect peel (viewports physically sliding) and picture-in-picture / overlapping views still need render-to-texture — a later, additive refinement. See ADR 0059 + 0060.