""" Physics-Based KAPS Renderer ============================ Connects to REAL physics simulation - not animated garbage. This renderer: 1. Runs actual KAPSSimulation.step() 2. Visualizes the REAL state from physics engine 3. Shows actual tether tension, TAB aerodynamics, etc. """ import numpy as np import moderngl import moderngl_window as mglw from pyrr import Matrix44 import sys import os # Add parent to path sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) from src.main import KAPSSimulation def create_sphere_mesh(radius: float = 1.0, segments: int = 16, rings: int = 12): """Create sphere vertices.""" vertices = [] normals = [] for ring in range(rings): theta1 = np.pi * ring / rings theta2 = np.pi * (ring + 1) / rings for seg in range(segments): phi1 = 2 * np.pi * seg / segments phi2 = 2 * np.pi * (seg + 1) / segments # Four corners of quad p1 = [np.sin(theta1) * np.cos(phi1), np.sin(theta1) * np.sin(phi1), np.cos(theta1)] p2 = [np.sin(theta1) * np.cos(phi2), np.sin(theta1) * np.sin(phi2), np.cos(theta1)] p3 = [np.sin(theta2) * np.cos(phi2), np.sin(theta2) * np.sin(phi2), np.cos(theta2)] p4 = [np.sin(theta2) * np.cos(phi1), np.sin(theta2) * np.sin(phi1), np.cos(theta2)] # Two triangles for p in [p1, p2, p3, p1, p3, p4]: vertices.extend([p[0] * radius, p[1] * radius, p[2] * radius]) normals.extend(p) return np.array(vertices, dtype='f4'), np.array(normals, dtype='f4') def create_box_mesh(size: tuple = (1, 1, 1)): """Create box vertices for Buzzard fuselage.""" sx, sy, sz = size vertices = [] normals = [] faces = [ # Front (+Y) ([[-sx, sy, -sz], [sx, sy, -sz], [sx, sy, sz], [-sx, sy, sz]], [0, 1, 0]), # Back (-Y) ([[sx, -sy, -sz], [-sx, -sy, -sz], [-sx, -sy, sz], [sx, -sy, sz]], [0, -1, 0]), # Right (+X) ([[sx, sy, -sz], [sx, -sy, -sz], [sx, -sy, sz], [sx, sy, sz]], [1, 0, 0]), # Left (-X) ([[-sx, -sy, -sz], [-sx, sy, -sz], [-sx, sy, sz], [-sx, -sy, sz]], [-1, 0, 0]), # Top (+Z) ([[-sx, sy, sz], [sx, sy, sz], [sx, -sy, sz], [-sx, -sy, sz]], [0, 0, 1]), # Bottom (-Z) ([[-sx, -sy, -sz], [sx, -sy, -sz], [sx, sy, -sz], [-sx, sy, -sz]], [0, 0, -1]), ] for verts, norm in faces: # Two triangles per face for idx in [0, 1, 2, 0, 2, 3]: vertices.extend(verts[idx]) normals.extend(norm) return np.array(vertices, dtype='f4'), np.array(normals, dtype='f4') VERTEX_SHADER = """ #version 330 in vec3 in_position; in vec3 in_normal; in vec3 in_color; out vec3 v_position; out vec3 v_normal; out vec3 v_color; uniform mat4 model; uniform mat4 view; uniform mat4 projection; void main() { v_position = vec3(model * vec4(in_position, 1.0)); v_normal = mat3(transpose(inverse(model))) * in_normal; v_color = in_color; gl_Position = projection * view * model * vec4(in_position, 1.0); } """ FRAGMENT_SHADER = """ #version 330 in vec3 v_position; in vec3 v_normal; in vec3 v_color; out vec4 fragColor; uniform vec3 light_pos; uniform vec3 view_pos; void main() { vec3 ambient = 0.3 * v_color; vec3 norm = normalize(v_normal); vec3 light_dir = normalize(light_pos - v_position); float diff = max(dot(norm, light_dir), 0.0); vec3 diffuse = diff * v_color; vec3 view_dir = normalize(view_pos - v_position); vec3 halfway = normalize(light_dir + view_dir); float spec = pow(max(dot(norm, halfway), 0.0), 32.0); vec3 specular = 0.3 * spec * vec3(1.0); fragColor = vec4(ambient + diffuse + specular, 1.0); } """ LINE_VERTEX = """ #version 330 in vec3 in_position; in vec3 in_color; out vec3 v_color; uniform mat4 mvp; void main() { v_color = in_color; gl_Position = mvp * vec4(in_position, 1.0); } """ LINE_FRAGMENT = """ #version 330 in vec3 v_color; out vec4 fragColor; void main() { fragColor = vec4(v_color, 1.0); } """ class PhysicsRenderer(mglw.WindowConfig): """ Renders the REAL KAPS physics simulation. """ gl_version = (3, 3) title = "KAPS - Physics Simulation" window_size = (1280, 720) aspect_ratio = 16 / 9 resizable = True samples = 4 def __init__(self, **kwargs): super().__init__(**kwargs) self.ctx.enable(moderngl.DEPTH_TEST) # REAL PHYSICS print("Initializing KAPS physics simulation...") self.sim = KAPSSimulation() self.sim.running = True print(f" Mother drone at: {self.sim.mother_drone.position}") print(f" TABs: {list(self.sim.tab_array.tabs.keys())}") # Physics timestep accumulator self.physics_accumulator = 0.0 self.physics_rate = 100 # Steps per second # Create shaders self.prog = self.ctx.program(vertex_shader=VERTEX_SHADER, fragment_shader=FRAGMENT_SHADER) self.line_prog = self.ctx.program(vertex_shader=LINE_VERTEX, fragment_shader=LINE_FRAGMENT) # Create meshes self._create_meshes() self._create_grid() # Camera self.camera_distance = 150.0 self.camera_height = 60.0 self.camera_angle = 0.0 # Pause control self.paused = False def _create_meshes(self): """Create GPU meshes.""" # Buzzard - elongated box box_v, box_n = create_box_mesh((3, 12, 2)) box_c = np.tile([0.2, 0.4, 0.8], len(box_v) // 3).astype('f4') box_data = np.zeros(len(box_v) // 3, dtype=[ ('in_position', 'f4', 3), ('in_normal', 'f4', 3), ('in_color', 'f4', 3) ]) box_data['in_position'] = box_v.reshape(-1, 3) box_data['in_normal'] = box_n.reshape(-1, 3) box_data['in_color'] = box_c.reshape(-1, 3) self.buzzard_vbo = self.ctx.buffer(box_data.tobytes()) self.buzzard_vao = self.ctx.vertex_array( self.prog, [(self.buzzard_vbo, '3f 3f 3f', 'in_position', 'in_normal', 'in_color')] ) # TAB - small airfoil box tab_v, tab_n = create_box_mesh((4, 2, 0.5)) tab_c = np.tile([0.8, 0.8, 0.8], len(tab_v) // 3).astype('f4') tab_data = np.zeros(len(tab_v) // 3, dtype=[ ('in_position', 'f4', 3), ('in_normal', 'f4', 3), ('in_color', 'f4', 3) ]) tab_data['in_position'] = tab_v.reshape(-1, 3) tab_data['in_normal'] = tab_n.reshape(-1, 3) tab_data['in_color'] = tab_c.reshape(-1, 3) self.tab_vbo = self.ctx.buffer(tab_data.tobytes()) self.tab_vao = self.ctx.vertex_array( self.prog, [(self.tab_vbo, '3f 3f 3f', 'in_position', 'in_normal', 'in_color')] ) def _create_grid(self): """Ground grid.""" lines = [] colors = [] for i in range(-500, 501, 50): lines.extend([i, -500, 0, i, 500, 0]) lines.extend([-500, i, 0, 500, i, 0]) colors.extend([0.3, 0.4, 0.3] * 4) grid_data = np.zeros(len(lines) // 3, dtype=[('in_position', 'f4', 3), ('in_color', 'f4', 3)]) grid_data['in_position'] = np.array(lines, dtype='f4').reshape(-1, 3) grid_data['in_color'] = np.array(colors, dtype='f4').reshape(-1, 3) self.grid_vbo = self.ctx.buffer(grid_data.tobytes()) self.grid_vao = self.ctx.vertex_array(self.line_prog, [(self.grid_vbo, '3f 3f', 'in_position', 'in_color')]) def key_event(self, key, action, modifiers): """Handle keyboard input.""" if action == self.wnd.keys.ACTION_PRESS: if key == self.wnd.keys.SPACE: self.paused = not self.paused print(f"{'PAUSED' if self.paused else 'RUNNING'}") elif key == self.wnd.keys.R: # Reset simulation self.sim = KAPSSimulation() self.sim.running = True print("RESET") def on_render(self, time: float, frame_time: float): """Render frame with real physics.""" # Step physics (fixed timestep) if not self.paused: self.physics_accumulator += frame_time steps = 0 while self.physics_accumulator >= 1.0 / self.physics_rate and steps < 10: self.sim.step() self.physics_accumulator -= 1.0 / self.physics_rate steps += 1 # Get REAL positions from simulation buzzard_pos = self.sim.mother_drone.position buzzard_vel = self.sim.mother_drone.velocity # Camera follows Buzzard self.camera_angle = time * 0.2 cam_offset = np.array([ np.cos(self.camera_angle) * self.camera_distance, np.sin(self.camera_angle) * self.camera_distance, self.camera_height ]) camera_pos = buzzard_pos + cam_offset camera_target = buzzard_pos # Clear self.ctx.clear(0.1, 0.12, 0.15) self.ctx.enable(moderngl.DEPTH_TEST) # Matrices proj = Matrix44.perspective_projection(60.0, self.aspect_ratio, 0.1, 2000.0) view = Matrix44.look_at(tuple(camera_pos), tuple(camera_target), (0, 0, 1)) mvp = proj * view # Grid self.line_prog['mvp'].write(mvp.astype('f4').tobytes()) self.grid_vao.render(moderngl.LINES) # Lighting self.prog['light_pos'].value = tuple(camera_pos + np.array([100, 100, 200])) self.prog['view_pos'].value = tuple(camera_pos) self.prog['view'].write(view.astype('f4').tobytes()) self.prog['projection'].write(proj.astype('f4').tobytes()) # Render Buzzard at REAL position model = Matrix44.from_translation(buzzard_pos) speed = np.linalg.norm(buzzard_vel) if speed > 0.1: yaw = np.arctan2(buzzard_vel[0], buzzard_vel[1]) model = model @ Matrix44.from_z_rotation(yaw) self.prog['model'].write(model.astype('f4').tobytes()) self.buzzard_vao.render(moderngl.TRIANGLES) # Render TABs at REAL positions cable_lines = [] cable_colors = [] for tab_id, tab in self.sim.tab_array.tabs.items(): tab_pos = tab.position # Draw TAB model = Matrix44.from_translation(tab_pos) self.prog['model'].write(model.astype('f4').tobytes()) self.tab_vao.render(moderngl.TRIANGLES) # Cable from Buzzard to TAB (with sag) if tab.is_attached: segments = 10 for i in range(segments): t1, t2 = i / segments, (i + 1) / segments p1 = buzzard_pos * (1 - t1) + tab_pos * t1 p2 = buzzard_pos * (1 - t2) + tab_pos * t2 # Catenary sag sag = 5.0 * 4 * t1 * (1 - t1) p1[2] -= sag sag = 5.0 * 4 * t2 * (1 - t2) p2[2] -= sag cable_lines.extend(list(p1) + list(p2)) cable_colors.extend([0.8, 0.3, 0.2] * 2) # Draw cables if cable_lines: cable_data = np.zeros(len(cable_lines) // 3, dtype=[('in_position', 'f4', 3), ('in_color', 'f4', 3)]) cable_data['in_position'] = np.array(cable_lines, dtype='f4').reshape(-1, 3) cable_data['in_color'] = np.array(cable_colors, dtype='f4').reshape(-1, 3) cable_vbo = self.ctx.buffer(cable_data.tobytes()) cable_vao = self.ctx.vertex_array(self.line_prog, [(cable_vbo, '3f 3f', 'in_position', 'in_color')]) cable_vao.render(moderngl.LINES) cable_vbo.release() def run(): """Run the physics renderer.""" mglw.run_window_config(PhysicsRenderer) if __name__ == "__main__": run()