|
|
""" |
|
|
KAPS Visualization Engine Interface |
|
|
===================================== |
|
|
|
|
|
Pluggable renderer abstraction. |
|
|
Swap between engines without touching physics or AI. |
|
|
|
|
|
Supported engines: |
|
|
- ModernGL (default) - Clean OpenGL 3.3+, proper shaders |
|
|
- Panda3D (legacy) - If you really want cartoons |
|
|
- Headless - No rendering, just state logging |
|
|
|
|
|
Usage: |
|
|
from visualization.engine_interface import create_renderer |
|
|
|
|
|
renderer = create_renderer('moderngl') # or 'panda3d', 'headless' |
|
|
renderer.set_simulation(sim) |
|
|
renderer.run() |
|
|
""" |
|
|
|
|
|
from abc import ABC, abstractmethod |
|
|
from typing import Dict, Optional, Tuple, Any |
|
|
import numpy as np |
|
|
from dataclasses import dataclass |
|
|
|
|
|
|
|
|
@dataclass |
|
|
class RenderState: |
|
|
"""Common state representation for all renderers.""" |
|
|
|
|
|
|
|
|
buzzard_position: np.ndarray |
|
|
buzzard_velocity: np.ndarray |
|
|
buzzard_rotation: Optional[np.ndarray] = None |
|
|
|
|
|
|
|
|
tab_states: Dict[str, Dict] = None |
|
|
|
|
|
|
|
|
cables: list = None |
|
|
|
|
|
|
|
|
threats: list = None |
|
|
|
|
|
|
|
|
bola_active: bool = False |
|
|
bola_position: Optional[np.ndarray] = None |
|
|
bola_rotation: float = 0.0 |
|
|
|
|
|
|
|
|
camera_mode: str = "CHASE" |
|
|
camera_position: Optional[np.ndarray] = None |
|
|
camera_target: Optional[np.ndarray] = None |
|
|
|
|
|
|
|
|
hud_text: Dict[str, str] = None |
|
|
|
|
|
|
|
|
class RendererInterface(ABC): |
|
|
"""Abstract base for all KAPS renderers.""" |
|
|
|
|
|
@abstractmethod |
|
|
def initialize(self): |
|
|
"""Initialize the rendering engine.""" |
|
|
pass |
|
|
|
|
|
@abstractmethod |
|
|
def set_simulation(self, sim, env=None): |
|
|
"""Connect to KAPS simulation.""" |
|
|
pass |
|
|
|
|
|
@abstractmethod |
|
|
def update_state(self, state: RenderState): |
|
|
"""Push new state to renderer.""" |
|
|
pass |
|
|
|
|
|
@abstractmethod |
|
|
def render_frame(self, dt: float): |
|
|
"""Render one frame.""" |
|
|
pass |
|
|
|
|
|
@abstractmethod |
|
|
def run(self): |
|
|
"""Run the render loop (blocking).""" |
|
|
pass |
|
|
|
|
|
@abstractmethod |
|
|
def is_running(self) -> bool: |
|
|
"""Check if renderer is still active.""" |
|
|
pass |
|
|
|
|
|
@abstractmethod |
|
|
def shutdown(self): |
|
|
"""Clean up resources.""" |
|
|
pass |
|
|
|
|
|
|
|
|
@abstractmethod |
|
|
def set_camera_mode(self, mode: str): |
|
|
"""Set camera mode: CHASE, ORBIT, FREE, TOP_DOWN, etc.""" |
|
|
pass |
|
|
|
|
|
@abstractmethod |
|
|
def set_camera_position(self, position: np.ndarray, target: np.ndarray): |
|
|
"""Manually set camera position and target.""" |
|
|
pass |
|
|
|
|
|
|
|
|
class HeadlessRenderer(RendererInterface): |
|
|
"""No-op renderer for training without display.""" |
|
|
|
|
|
def __init__(self): |
|
|
self._running = False |
|
|
self._frame_count = 0 |
|
|
self.sim = None |
|
|
|
|
|
def initialize(self): |
|
|
self._running = True |
|
|
print("[Headless] Renderer initialized (no display)") |
|
|
|
|
|
def set_simulation(self, sim, env=None): |
|
|
self.sim = sim |
|
|
|
|
|
def update_state(self, state: RenderState): |
|
|
pass |
|
|
|
|
|
def render_frame(self, dt: float): |
|
|
self._frame_count += 1 |
|
|
|
|
|
def run(self): |
|
|
self._running = True |
|
|
print("[Headless] Running (no visual output)") |
|
|
|
|
|
def is_running(self) -> bool: |
|
|
return self._running |
|
|
|
|
|
def shutdown(self): |
|
|
self._running = False |
|
|
print(f"[Headless] Shutdown after {self._frame_count} frames") |
|
|
|
|
|
def set_camera_mode(self, mode: str): |
|
|
pass |
|
|
|
|
|
def set_camera_position(self, position: np.ndarray, target: np.ndarray): |
|
|
pass |
|
|
|
|
|
|
|
|
class ModernGLRenderer(RendererInterface): |
|
|
"""ModernGL-based renderer with proper shaders.""" |
|
|
|
|
|
def __init__(self): |
|
|
self._window = None |
|
|
self._running = False |
|
|
self.sim = None |
|
|
self.env = None |
|
|
|
|
|
def initialize(self): |
|
|
|
|
|
from visualization.modern_renderer import KAPSModernRenderer |
|
|
import moderngl_window as mglw |
|
|
|
|
|
self._renderer_class = KAPSModernRenderer |
|
|
self._running = True |
|
|
print("[ModernGL] Renderer initialized") |
|
|
|
|
|
def set_simulation(self, sim, env=None): |
|
|
self.sim = sim |
|
|
self.env = env |
|
|
|
|
|
def update_state(self, state: RenderState): |
|
|
if self._window: |
|
|
|
|
|
pass |
|
|
|
|
|
def render_frame(self, dt: float): |
|
|
|
|
|
pass |
|
|
|
|
|
def run(self): |
|
|
import moderngl_window as mglw |
|
|
|
|
|
|
|
|
class ConfiguredRenderer(self._renderer_class): |
|
|
pass |
|
|
|
|
|
ConfiguredRenderer.sim_instance = self.sim |
|
|
ConfiguredRenderer.env_instance = self.env |
|
|
|
|
|
|
|
|
original_init = ConfiguredRenderer.__init__ |
|
|
sim = self.sim |
|
|
env = self.env |
|
|
|
|
|
def new_init(self, **kwargs): |
|
|
original_init(self, **kwargs) |
|
|
self.set_simulation(sim, env) |
|
|
|
|
|
ConfiguredRenderer.__init__ = new_init |
|
|
|
|
|
mglw.run_window_config(ConfiguredRenderer) |
|
|
|
|
|
def is_running(self) -> bool: |
|
|
return self._running |
|
|
|
|
|
def shutdown(self): |
|
|
self._running = False |
|
|
|
|
|
def set_camera_mode(self, mode: str): |
|
|
pass |
|
|
|
|
|
def set_camera_position(self, position: np.ndarray, target: np.ndarray): |
|
|
pass |
|
|
|
|
|
|
|
|
class Panda3DRenderer(RendererInterface): |
|
|
"""Legacy Panda3D renderer (cartoonish but functional).""" |
|
|
|
|
|
def __init__(self): |
|
|
self._app = None |
|
|
self._running = False |
|
|
|
|
|
def initialize(self): |
|
|
print("[Panda3D] Use visual_trainer.py for Panda3D rendering") |
|
|
self._running = True |
|
|
|
|
|
def set_simulation(self, sim, env=None): |
|
|
pass |
|
|
|
|
|
def update_state(self, state: RenderState): |
|
|
pass |
|
|
|
|
|
def render_frame(self, dt: float): |
|
|
pass |
|
|
|
|
|
def run(self): |
|
|
|
|
|
from training.visual_trainer import VisualTrainer |
|
|
trainer = VisualTrainer() |
|
|
trainer.run() |
|
|
|
|
|
def is_running(self) -> bool: |
|
|
return self._running |
|
|
|
|
|
def shutdown(self): |
|
|
self._running = False |
|
|
|
|
|
def set_camera_mode(self, mode: str): |
|
|
pass |
|
|
|
|
|
def set_camera_position(self, position: np.ndarray, target: np.ndarray): |
|
|
pass |
|
|
|
|
|
|
|
|
|
|
|
RENDERERS = { |
|
|
'moderngl': ModernGLRenderer, |
|
|
'opengl': ModernGLRenderer, |
|
|
'panda3d': Panda3DRenderer, |
|
|
'panda': Panda3DRenderer, |
|
|
'headless': HeadlessRenderer, |
|
|
'none': HeadlessRenderer, |
|
|
} |
|
|
|
|
|
|
|
|
def create_renderer(engine: str = 'moderngl') -> RendererInterface: |
|
|
""" |
|
|
Create a renderer instance. |
|
|
|
|
|
Args: |
|
|
engine: One of 'moderngl', 'panda3d', 'headless' |
|
|
|
|
|
Returns: |
|
|
Initialized renderer |
|
|
""" |
|
|
engine = engine.lower() |
|
|
|
|
|
if engine not in RENDERERS: |
|
|
available = ', '.join(RENDERERS.keys()) |
|
|
raise ValueError(f"Unknown engine '{engine}'. Available: {available}") |
|
|
|
|
|
renderer = RENDERERS[engine]() |
|
|
renderer.initialize() |
|
|
|
|
|
return renderer |
|
|
|
|
|
|
|
|
def list_available_engines() -> list: |
|
|
"""List available rendering engines.""" |
|
|
return list(set(RENDERERS.values())) |
|
|
|
|
|
|
|
|
def get_recommended_engine() -> str: |
|
|
"""Get the recommended engine for this system.""" |
|
|
try: |
|
|
import moderngl |
|
|
return 'moderngl' |
|
|
except ImportError: |
|
|
pass |
|
|
|
|
|
try: |
|
|
from panda3d.core import PandaSystem |
|
|
return 'panda3d' |
|
|
except ImportError: |
|
|
pass |
|
|
|
|
|
return 'headless' |
|
|
|