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 Key | Position (Lua) | Look-At Target | FOV | Visual 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
| Entity | Dimensions / Speed | Mechanical 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. |