File size: 12,247 Bytes
26fa66a |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 |
"""
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()
|