147 lines
5.4 KiB
Python
147 lines
5.4 KiB
Python
"""
|
|
ml/MetricsOverlay.py - Camera-Facing (Billboard) 3D Floating Text Overlay
|
|
"""
|
|
from typing import List, Tuple, Optional
|
|
import numpy as np
|
|
import pybullet as p
|
|
|
|
|
|
class MetricsHUD:
|
|
"""Renders real-time telemetry as black text floating in 3D, always facing the active camera."""
|
|
|
|
def __init__(self, physics_client_id: int = 0):
|
|
self.client_id = physics_client_id
|
|
self._text_id: Optional[int] = None
|
|
|
|
def _get_camera_facing_orientation(self) -> List[float]:
|
|
"""Calculates a quaternion that points the text towards the current GUI camera."""
|
|
try:
|
|
cam_info = p.getDebugVisualizerCamera(physicsClientId=self.client_id)
|
|
# cam_info index 8: yaw, index 9: pitch
|
|
yaw = cam_info[8]
|
|
pitch = cam_info[9]
|
|
|
|
# Orient the text normal toward the camera view direction
|
|
# PyBullet text default faces local +Z/-Y depending on roll,
|
|
# converting visualizer yaw/pitch to Euler angles (roll, pitch, yaw in radians)
|
|
roll_rad = 0.0
|
|
pitch_rad = np.radians(pitch + 90.0)
|
|
yaw_rad = np.radians(yaw)
|
|
|
|
text_orientation = p.getQuaternionFromEuler(
|
|
[pitch_rad, roll_rad, yaw_rad],
|
|
physicsClientId=self.client_id
|
|
)
|
|
return text_orientation
|
|
except Exception:
|
|
# Fallback default orientation if camera info call fails
|
|
return [0.0, 0.0, 0.0, 1.0]
|
|
|
|
def update(
|
|
self,
|
|
episode: int,
|
|
step: int,
|
|
robot_rewards: List[float],
|
|
cmd_vel: np.ndarray,
|
|
fps: float = 0.0,
|
|
avg_height: float = 0.0,
|
|
roll_pitch: Tuple[float, float] = (0.0, 0.0)
|
|
) -> None:
|
|
"""Updates floating black text block in 3D space with billboarding."""
|
|
sorted_rewards = sorted(robot_rewards, reverse=True)
|
|
top1 = f"{sorted_rewards[0]:+.2f}" if len(sorted_rewards) > 0 else "0.00"
|
|
top2 = f"{sorted_rewards[1]:+.2f}" if len(sorted_rewards) > 1 else "0.00"
|
|
top3 = f"{sorted_rewards[2]:+.2f}" if len(sorted_rewards) > 2 else "0.00"
|
|
|
|
vx = cmd_vel[0] if len(cmd_vel) > 0 else 0.0
|
|
vy = cmd_vel[1] if len(cmd_vel) > 1 else 0.0
|
|
omega = cmd_vel[3] if len(cmd_vel) > 3 else 0.0
|
|
|
|
hud_text = (
|
|
f"=== JACKBOT METRICS ===\n"
|
|
f"Episode: {episode}\n"
|
|
f"Global Step: {step}\n"
|
|
f"FPS: {fps:.1f}\n"
|
|
f"----------------------\n"
|
|
f"Top Rewards: [{top1}, {top2}, {top3}]\n"
|
|
f"Cmd (X,Y,W): [{vx:+.2f}, {vy:+.2f}, {omega:+.2f}]\n"
|
|
f"Height: {avg_height:.3f} m\n"
|
|
f"Roll/Pitch: {roll_pitch[0]:+.1f}° / {roll_pitch[1]:+.1f}°"
|
|
)
|
|
|
|
# Position above origin in simulation world
|
|
text_position = [-0.8, -0.8, 1.2]
|
|
text_color = [0, 0, 0] # Pure black
|
|
|
|
# Calculate dynamic orientation to align text flat against camera plane
|
|
text_orientation = self._get_camera_facing_orientation()
|
|
|
|
if self._text_id is None:
|
|
self._text_id = p.addUserDebugText(
|
|
text=hud_text,
|
|
textPosition=text_position,
|
|
textColorRGB=text_color,
|
|
textSize=0.1,
|
|
textOrientation=text_orientation,
|
|
physicsClientId=self.client_id
|
|
)
|
|
else:
|
|
self._text_id = p.addUserDebugText(
|
|
text=hud_text,
|
|
textPosition=text_position,
|
|
textColorRGB=text_color,
|
|
textSize=0.1,
|
|
textOrientation=text_orientation,
|
|
replaceItemUniqueId=self._text_id,
|
|
physicsClientId=self.client_id
|
|
)
|
|
|
|
def reset(self) -> None:
|
|
"""Removes the active debug text item so a new episode starts with a clean overlay."""
|
|
if self._text_id is not None:
|
|
try:
|
|
p.removeUserDebugItem(self._text_id, physicsClientId=self.client_id)
|
|
except Exception:
|
|
pass
|
|
self._text_id = None
|
|
|
|
|
|
class LeaderCrown:
|
|
"""Renders a floating crown or star emoji above the leading robot in PyBullet."""
|
|
|
|
def __init__(self, physics_client_id: int = 0):
|
|
self.client_id = physics_client_id
|
|
self._text_id = None
|
|
|
|
def update(self, leader_pos: list[float]):
|
|
"""Positions a floating crown ~0.35m directly above the lead robot's base."""
|
|
crown_pos = [leader_pos[0], leader_pos[1], leader_pos[2] + 0.35]
|
|
|
|
# You can use "👑 CROWN", "⭐ LEADER", or "★ TOP1"
|
|
crown_text = "👑"
|
|
|
|
if self._text_id is None:
|
|
self._text_id = p.addUserDebugText(
|
|
text=crown_text,
|
|
textPosition=crown_pos,
|
|
textColorRGB=[1.0, 0.84, 0.0],
|
|
textSize=2.0,
|
|
physicsClientId=self.client_id
|
|
)
|
|
else:
|
|
self._text_id = p.addUserDebugText(
|
|
text=crown_text,
|
|
textPosition=crown_pos,
|
|
textColorRGB=[1.0, 0.84, 0.0],
|
|
textSize=2.0,
|
|
replaceItemUniqueId=self._text_id,
|
|
physicsClientId=self.client_id
|
|
)
|
|
|
|
def reset(self):
|
|
if self._text_id is not None:
|
|
try:
|
|
p.removeUserDebugItem(self._text_id, physicsClientId=self.client_id)
|
|
except Exception:
|
|
pass
|
|
self._text_id = None |