| | """ |
| | 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 |
| |
|
| | |
| | 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 |
| | |
| | |
| | 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)] |
| | |
| | |
| | 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 = [ |
| | |
| | ([[-sx, sy, -sz], [sx, sy, -sz], [sx, sy, sz], [-sx, sy, sz]], [0, 1, 0]), |
| | |
| | ([[sx, -sy, -sz], [-sx, -sy, -sz], [-sx, -sy, sz], [sx, -sy, sz]], [0, -1, 0]), |
| | |
| | ([[sx, sy, -sz], [sx, -sy, -sz], [sx, -sy, sz], [sx, sy, sz]], [1, 0, 0]), |
| | |
| | ([[-sx, -sy, -sz], [-sx, sy, -sz], [-sx, sy, sz], [-sx, -sy, sz]], [-1, 0, 0]), |
| | |
| | ([[-sx, sy, sz], [sx, sy, sz], [sx, -sy, sz], [-sx, -sy, sz]], [0, 0, 1]), |
| | |
| | ([[-sx, -sy, -sz], [sx, -sy, -sz], [sx, sy, -sz], [-sx, sy, -sz]], [0, 0, -1]), |
| | ] |
| | |
| | for verts, norm in faces: |
| | |
| | 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) |
| | |
| | |
| | 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())}") |
| | |
| | |
| | self.physics_accumulator = 0.0 |
| | self.physics_rate = 100 |
| | |
| | |
| | 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) |
| | |
| | |
| | self._create_meshes() |
| | self._create_grid() |
| | |
| | |
| | self.camera_distance = 150.0 |
| | self.camera_height = 60.0 |
| | self.camera_angle = 0.0 |
| | |
| | |
| | self.paused = False |
| | |
| | def _create_meshes(self): |
| | """Create GPU meshes.""" |
| | |
| | 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_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: |
| | |
| | self.sim = KAPSSimulation() |
| | self.sim.running = True |
| | print("RESET") |
| | |
| | def on_render(self, time: float, frame_time: float): |
| | """Render frame with real physics.""" |
| | |
| | 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 |
| | |
| | |
| | buzzard_pos = self.sim.mother_drone.position |
| | buzzard_vel = self.sim.mother_drone.velocity |
| | |
| | |
| | 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 |
| | |
| | |
| | self.ctx.clear(0.1, 0.12, 0.15) |
| | self.ctx.enable(moderngl.DEPTH_TEST) |
| | |
| | |
| | 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 |
| | |
| | |
| | self.line_prog['mvp'].write(mvp.astype('f4').tobytes()) |
| | self.grid_vao.render(moderngl.LINES) |
| | |
| | |
| | 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()) |
| | |
| | |
| | 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) |
| | |
| | |
| | cable_lines = [] |
| | cable_colors = [] |
| | |
| | for tab_id, tab in self.sim.tab_array.tabs.items(): |
| | tab_pos = tab.position |
| | |
| | |
| | model = Matrix44.from_translation(tab_pos) |
| | self.prog['model'].write(model.astype('f4').tobytes()) |
| | self.tab_vao.render(moderngl.TRIANGLES) |
| | |
| | |
| | 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 |
| | |
| | |
| | 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) |
| | |
| | |
| | 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() |
| |
|