tostido's picture
Add blueprints archive: ARACHNE-001, MARIONETTE-001, AIRFOIL-CORDAGE-SYSTEM, PERSPECTIVE
26fa66a
"""
Cinematic Camera System for KAPS Visual Trainer
================================================
Dynamic camera perspectives that follow the Buzzard from multiple angles:
CAMERA MODES:
- CHASE: Behind and above, following velocity vector
- ORBIT: Circular orbit around the Buzzard
- CINEMATIC: Smooth transitions between dramatic angles
- COCKPIT: First-person from Buzzard's perspective
- TACTICAL: Top-down overview
- SIDE: Traditional side view
- DYNAMIC: Automatic scene transitions based on action
The camera should always give contextual awareness of:
1. Where the Buzzard is going (velocity direction)
2. Where threats are coming from
3. Where the TAB airfoils are positioned
"""
import numpy as np
from typing import Optional, Tuple, List
from enum import Enum, auto
from dataclasses import dataclass
import time
class CameraMode(Enum):
"""Available camera perspectives."""
CHASE = auto() # Behind and above, following velocity
ORBIT = auto() # Circular orbit
CINEMATIC = auto() # Smooth dramatic transitions
COCKPIT = auto() # First-person from Buzzard
TACTICAL = auto() # Top-down overview
SIDE = auto() # Side view
DYNAMIC = auto() # Automatic scene changes
FREE = auto() # User-controlled
@dataclass
class CameraState:
"""Current camera state."""
position: np.ndarray # World position
look_at: np.ndarray # Target point
up_vector: np.ndarray # Up direction
fov: float = 60.0 # Field of view
# For smooth transitions
target_position: np.ndarray = None
target_look_at: np.ndarray = None
transition_speed: float = 3.0
def __post_init__(self):
if self.target_position is None:
self.target_position = self.position.copy()
if self.target_look_at is None:
self.target_look_at = self.look_at.copy()
class CinematicSequence:
"""A sequence of camera angles for dramatic effect."""
def __init__(self):
self.shots: List[dict] = []
self.current_shot = 0
self.shot_start_time = 0
def add_shot(self,
offset: np.ndarray,
duration: float,
fov: float = 60,
look_offset: np.ndarray = None):
"""Add a camera shot to the sequence."""
self.shots.append({
'offset': offset,
'duration': duration,
'fov': fov,
'look_offset': look_offset or np.zeros(3)
})
def get_current_shot(self, elapsed: float) -> Optional[dict]:
"""Get current shot based on elapsed time."""
if not self.shots:
return None
total = 0
for i, shot in enumerate(self.shots):
total += shot['duration']
if elapsed < total:
return shot
# Loop back
return self.shots[0]
class CinematicCamera:
"""
Dynamic cinematic camera system.
Provides multiple perspectives and smooth transitions.
"""
def __init__(self):
# Current state
self.mode = CameraMode.CHASE
self.state = CameraState(
position=np.array([0.0, -100.0, 50.0]),
look_at=np.array([0.0, 0.0, 0.0]),
up_vector=np.array([0.0, 0.0, 1.0])
)
# Target tracking
self.target_position = np.array([0.0, 0.0, 1000.0]) # Buzzard position
self.target_velocity = np.array([0.0, 50.0, 0.0]) # Buzzard velocity
# Mode-specific settings
self.chase_distance = 80.0
self.chase_height = 30.0
self.chase_lag = 0.1 # Lag behind velocity for dramatic effect
self.orbit_radius = 100.0
self.orbit_speed = 0.3 # Radians per second
self.orbit_angle = 0.0
self.orbit_height = 40.0
self.tactical_height = 300.0
# Cinematic sequences
self.sequences = self._create_default_sequences()
self.active_sequence: Optional[CinematicSequence] = None
self.sequence_start_time = 0.0
# Transition state
self.in_transition = False
self.transition_progress = 0.0
self.transition_duration = 2.0
self.prev_state: Optional[CameraState] = None
# Dynamic mode timing
self.dynamic_last_change = time.time()
self.dynamic_interval = 8.0 # Change every 8 seconds
self.dynamic_mode_sequence = [
CameraMode.CHASE,
CameraMode.ORBIT,
CameraMode.SIDE,
CameraMode.TACTICAL,
CameraMode.CHASE,
]
self.dynamic_mode_index = 0
def _create_default_sequences(self) -> dict:
"""Create default cinematic sequences."""
sequences = {}
# Dramatic reveal sequence
reveal = CinematicSequence()
reveal.add_shot(np.array([0, -150, 20]), 3.0, fov=40) # Low far approach
reveal.add_shot(np.array([100, -50, 80]), 2.0, fov=50) # Sweep right high
reveal.add_shot(np.array([0, -60, 40]), 2.0, fov=60) # Settle into chase
sequences['reveal'] = reveal
# Action sequence - quick cuts
action = CinematicSequence()
action.add_shot(np.array([-50, -30, 20]), 1.5, fov=70) # Close left
action.add_shot(np.array([50, -30, 20]), 1.5, fov=70) # Close right
action.add_shot(np.array([0, 50, 100]), 1.0, fov=50) # High front
action.add_shot(np.array([0, -80, 30]), 2.0, fov=60) # Pull back
sequences['action'] = action
return sequences
def set_mode(self, mode: CameraMode, transition: bool = True):
"""Change camera mode with optional transition."""
if mode == self.mode:
return
if transition:
self.prev_state = CameraState(
position=self.state.position.copy(),
look_at=self.state.look_at.copy(),
up_vector=self.state.up_vector.copy(),
fov=self.state.fov
)
self.in_transition = True
self.transition_progress = 0.0
self.mode = mode
print(f"[CAMERA] Mode: {mode.name}")
def start_sequence(self, name: str):
"""Start a cinematic sequence."""
if name in self.sequences:
self.active_sequence = self.sequences[name]
self.sequence_start_time = time.time()
print(f"[CAMERA] Starting sequence: {name}")
def stop_sequence(self):
"""Stop active sequence."""
self.active_sequence = None
def update(self,
target_pos: np.ndarray,
target_vel: np.ndarray,
dt: float,
threats: List = None) -> Tuple[np.ndarray, np.ndarray, float]:
"""
Update camera position.
Returns: (position, look_at, fov)
"""
self.target_position = target_pos
self.target_velocity = target_vel
# Handle cinematic sequence
if self.active_sequence:
elapsed = time.time() - self.sequence_start_time
shot = self.active_sequence.get_current_shot(elapsed)
if shot:
new_pos = target_pos + shot['offset']
new_look = target_pos + shot['look_offset']
self.state.target_position = new_pos
self.state.target_look_at = new_look
self.state.fov = shot['fov']
# Handle dynamic mode changes
elif self.mode == CameraMode.DYNAMIC:
now = time.time()
if now - self.dynamic_last_change > self.dynamic_interval:
self.dynamic_last_change = now
self.dynamic_mode_index = (self.dynamic_mode_index + 1) % len(self.dynamic_mode_sequence)
next_mode = self.dynamic_mode_sequence[self.dynamic_mode_index]
# Temporarily switch to compute target, but keep dynamic
self._compute_mode_position(next_mode, threats)
else:
# Compute position for current mode
self._compute_mode_position(self.mode, threats)
# Smooth transition
if self.in_transition:
self.transition_progress += dt / self.transition_duration
if self.transition_progress >= 1.0:
self.transition_progress = 1.0
self.in_transition = False
self.prev_state = None
t = self._ease_in_out(self.transition_progress)
if self.prev_state:
self.state.position = self._lerp(self.prev_state.position,
self.state.target_position, t)
self.state.look_at = self._lerp(self.prev_state.look_at,
self.state.target_look_at, t)
else:
# Smooth follow
alpha = min(1.0, dt * self.state.transition_speed)
self.state.position = self._lerp(self.state.position,
self.state.target_position, alpha)
self.state.look_at = self._lerp(self.state.look_at,
self.state.target_look_at, alpha)
return self.state.position, self.state.look_at, self.state.fov
def _compute_mode_position(self, mode: CameraMode, threats: List = None):
"""Compute target camera position for a mode."""
pos = self.target_position
vel = self.target_velocity
# Normalize velocity for direction
speed = np.linalg.norm(vel)
if speed > 0.1:
vel_dir = vel / speed
else:
vel_dir = np.array([0, 1, 0]) # Default forward
if mode == CameraMode.CHASE:
# Behind and above, along velocity vector
behind = -vel_dir * self.chase_distance
up = np.array([0, 0, self.chase_height])
self.state.target_position = pos + behind + up
self.state.target_look_at = pos + vel_dir * 20 # Look ahead
elif mode == CameraMode.ORBIT:
# Circular orbit around target
self.orbit_angle += 0.016 * self.orbit_speed # Assume ~60fps
x = self.orbit_radius * np.cos(self.orbit_angle)
y = self.orbit_radius * np.sin(self.orbit_angle)
self.state.target_position = pos + np.array([x, y, self.orbit_height])
self.state.target_look_at = pos
elif mode == CameraMode.COCKPIT:
# First person from Buzzard
forward = vel_dir * 5 # Just ahead
self.state.target_position = pos + np.array([0, 0, 2])
self.state.target_look_at = pos + forward * 50
elif mode == CameraMode.TACTICAL:
# Top-down
self.state.target_position = pos + np.array([0, 0, self.tactical_height])
self.state.target_look_at = pos
elif mode == CameraMode.SIDE:
# Side view
right = np.cross(vel_dir, np.array([0, 0, 1]))
if np.linalg.norm(right) < 0.1:
right = np.array([1, 0, 0])
else:
right = right / np.linalg.norm(right)
self.state.target_position = pos + right * 80 + np.array([0, 0, 20])
self.state.target_look_at = pos
elif mode == CameraMode.CINEMATIC:
# Dramatic low angle
behind = -vel_dir * 40
self.state.target_position = pos + behind + np.array([20, 0, -10])
self.state.target_look_at = pos + vel_dir * 30
elif mode == CameraMode.FREE:
# Don't update automatically
pass
def _lerp(self, a: np.ndarray, b: np.ndarray, t: float) -> np.ndarray:
"""Linear interpolation."""
return a + (b - a) * t
def _ease_in_out(self, t: float) -> float:
"""Smooth easing function."""
return t * t * (3 - 2 * t)
# =========================================================================
# USER INPUT HANDLERS
# =========================================================================
def handle_orbit_drag(self, dx: float, dy: float):
"""Handle mouse drag for orbit control."""
if self.mode == CameraMode.FREE:
# Adjust orbit angle and height
self.orbit_angle += dx * 0.01
self.orbit_height = np.clip(self.orbit_height + dy * 2, 10, 200)
def handle_zoom(self, direction: float):
"""Handle zoom in/out."""
factor = 0.9 if direction > 0 else 1.1
self.chase_distance = np.clip(self.chase_distance * factor, 30, 300)
self.orbit_radius = np.clip(self.orbit_radius * factor, 40, 400)
# =============================================================================
# CAMERA MODE HOTKEYS
# =============================================================================
CAMERA_KEYBINDS = {
'c': 'cycle_mode', # Cycle through modes
'v': CameraMode.CHASE, # Chase cam
'b': CameraMode.ORBIT, # Orbit cam
'n': CameraMode.TACTICAL, # Tactical (top-down)
'm': CameraMode.DYNAMIC, # Dynamic cinematic
',': CameraMode.COCKPIT, # First person
}