Interface · 3D Table Layer CameraRig & World3D

Camera Rig & 3D Table Environment

Beneath the 2D HUD canvas sits a full 3D viewport environment: a 24×16 field quad procedurally painted by a SubViewport, physical card quads flipping with 3D arc lifts, runner meeples traversing basepaths, and a 70mm lens CameraRig driven by 7 named cinematic poses, non-repeating handheld sway, and trauma² shake physics.

src/world3d/camera_rig.gd · src/world3d/table_world.gd · field_painter.gd · card_quad.gd · dice_3d.gd · meeple_3d.gd

Coordinate System & Runtime Mapping

The 3D world is ported from the Z-up coordinate system of the original Lua prototype. Coordinates in source files are defined in Lua world units and transformed at runtime to Godot's Y-up system:

Godot(x, y, z) = Vector3(lua.x, lua.z, -lua.y)   # CameraRig.lua_to_godot()

Poses are configured as (lateral_x, depth_y, height_z). The default table pose sits 17.5 units back from home plate and 13.0 units elevated.

The 7 Cinematic Camera Poses

All camera poses maintain a 70mm equivalent lens (0.48–0.62 rad / 27.5°–35.5° FOV). The camera never zooms by artificially manipulating FOV; it moves physically through world space so perspective and parallax remain grounded.

Pose KeyPosition (Lua)Look-At TargetFOVVisual Beat & Purpose

Transitions execute via CameraRig.punch_to(pose_name, dur = 0.6) — parallel tweens on position, target, and FOV utilizing TRANS_CUBIC / EASE_IN_OUT. The 180° seat rotation between Batting and Pitching (set_orientation) executes a 0.9s smooth flip punch.

Layered Handheld Sway Dynamics

To eliminate mechanical stillness, the camera drifts continuously using five incommensurate sinusoids, ensuring the Lissajous pattern never visibly loops:

FREQS = [0.73, 0.31, 0.89, 0.43, 0.61] Hz
AMP   = Vector3(0.12, 0.12, 0.06)  # lateral, depth, vertical

offset_x = sin(t · 0.73) · cos(t · 0.31) · 0.12
offset_y = cos(t · 0.89) · sin(t · 0.43) · 0.12
offset_z = sin(t · 0.61) · 0.06

The sway offset applies 100% to the camera position, but only at LOOKAT_DAMPING = 0.4 to the look-at target. The frame breathes while keeping the table action stabilized.

Trauma² Shake Mechanics

Trauma accumulation model: CameraRig.add_shake(amount) increments trauma (capped at 1.0). Trauma drains at a rate of 1.6 per second (full 1.0 trauma decays to zero in ~0.62s). Offset magnitude follows a trauma² × 0.45 polynomial with vertical shake damped to 60% of horizontal shake.

Procedural Field Rendering (FieldPainter)

The diamond field is not a static 3D mesh asset. A FieldPainter Control draws the baseball field into a 2048×1365 SubViewport with 2D draw calls, which is mapped directly as the albedo texture of an unshaded 24×16 PlaneMesh.

  • Felt base ground (#0A3B22).
  • Soft elliptical vignette in field green (#15663C).
  • 90° outfield grass fan struck from home plate at radius r × 1.45.
  • Dirt warning track & infield diamond (#EFBE77).
  • Infield grass diamond (#2CA152).
  • Pitcher's mound, rubber, bases, and crisp chalk foul lines.

Physical 3D Entities

EntityDimensions / SpeedMechanical Behavior
CardQuad (3D Card) 1.31 × 1.84 units
0.40s flip duration
Two unshaded quads with ±0.001 Z-offset to prevent Z-fighting. Rises 0.50 units during flip to simulate physical hand reveal.
Dice3D (Physical Dice) 44px face textures
Physics roll
3D cubes rolled into the dice tray; resolves offensive red faces and defensive blue stops.
Meeple3D (Base Runners) Team-coloured caps
0.35s / base
Traverses interpolated basepath waypoints exported directly from FieldPainter texture coordinates.