tostido's picture
Add blueprints archive: ARACHNE-001, MARIONETTE-001, AIRFOIL-CORDAGE-SYSTEM, PERSPECTIVE
26fa66a
"""
Modern OpenGL Renderer for KAPS
================================
Clean, professional visualization using ModernGL.
NOT cartoony - proper shading, depth, and lighting.
This plugs directly into the existing physics and AI systems.
"""
import numpy as np
import moderngl
import moderngl_window as mglw
from moderngl_window import geometry
from pyrr import Matrix44, Vector3, matrix44
import time
from typing import Dict, List, Optional, Tuple
from dataclasses import dataclass
# Vertex shader with proper lighting
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 with Blinn-Phong lighting
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;
uniform vec3 ambient_color;
uniform float ambient_strength;
uniform float specular_strength;
uniform float shininess;
void main() {
// Ambient
vec3 ambient = ambient_strength * ambient_color;
// Diffuse
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 * vec3(1.0, 0.98, 0.95);
// Specular (Blinn-Phong)
vec3 view_dir = normalize(view_pos - v_position);
vec3 halfway_dir = normalize(light_dir + view_dir);
float spec = pow(max(dot(norm, halfway_dir), 0.0), shininess);
vec3 specular = specular_strength * spec * vec3(1.0, 1.0, 1.0);
// Combine
vec3 result = (ambient + diffuse + specular) * v_color;
// Gamma correction
result = pow(result, vec3(1.0/2.2));
fragColor = vec4(result, 1.0);
}
"""
# Line shader for cables
LINE_VERTEX_SHADER = """
#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_SHADER = """
#version 330
in vec3 v_color;
out vec4 fragColor;
void main() {
fragColor = vec4(v_color, 1.0);
}
"""
def create_cylinder_mesh(radius: float = 1.0, height: float = 2.0, segments: int = 16):
"""Create cylinder vertices for fuselage."""
vertices = []
normals = []
# Side faces
for i in range(segments):
angle1 = 2 * np.pi * i / segments
angle2 = 2 * np.pi * (i + 1) / segments
x1, z1 = np.cos(angle1) * radius, np.sin(angle1) * radius
x2, z2 = np.cos(angle2) * radius, np.sin(angle2) * radius
# Two triangles per segment
# Bottom triangle
vertices.extend([x1, -height/2, z1, x2, -height/2, z2, x1, height/2, z1])
n1 = [np.cos(angle1), 0, np.sin(angle1)]
n2 = [np.cos(angle2), 0, np.sin(angle2)]
normals.extend(n1 + n2 + n1)
# Top triangle
vertices.extend([x1, height/2, z1, x2, -height/2, z2, x2, height/2, z2])
normals.extend(n1 + n2 + n2)
return np.array(vertices, dtype='f4'), np.array(normals, dtype='f4')
def create_cone_mesh(radius: float = 1.0, height: float = 2.0, segments: int = 16):
"""Create cone vertices for nose/tail."""
vertices = []
normals = []
for i in range(segments):
angle1 = 2 * np.pi * i / segments
angle2 = 2 * np.pi * (i + 1) / segments
x1, z1 = np.cos(angle1) * radius, np.sin(angle1) * radius
x2, z2 = np.cos(angle2) * radius, np.sin(angle2) * radius
# Triangle from base to tip
vertices.extend([x1, 0, z1, x2, 0, z2, 0, height, 0])
# Approximate normals
n1 = np.array([np.cos(angle1), 0.5, np.sin(angle1)])
n1 = n1 / np.linalg.norm(n1)
n2 = np.array([np.cos(angle2), 0.5, np.sin(angle2)])
n2 = n2 / np.linalg.norm(n2)
nt = np.array([0, 1, 0])
normals.extend(list(n1) + list(n2) + list(nt))
return np.array(vertices, dtype='f4'), np.array(normals, dtype='f4')
def create_wing_mesh(span: float = 6.0, chord: float = 2.0, thickness: float = 0.3):
"""Create delta wing mesh."""
vertices = []
normals = []
# Simple delta wing - nose, left tip, right tip, trailing edges
# Top surface
verts_top = [
[chord, 0, thickness/2], # Nose
[-chord*0.3, span/2, thickness/4], # Right tip
[-chord*0.3, -span/2, thickness/4], # Left tip
[-chord*0.5, 0, thickness/4], # Tail center
]
# Top triangles
tris_top = [
(0, 1, 3), # Nose to right to tail
(0, 3, 2), # Nose to tail to left
]
for tri in tris_top:
for idx in tri:
vertices.extend(verts_top[idx])
normals.extend([0, 0, 1])
# Bottom surface
verts_bot = [
[chord, 0, -thickness/4],
[-chord*0.3, span/2, -thickness/4],
[-chord*0.3, -span/2, -thickness/4],
[-chord*0.5, 0, -thickness/4],
]
tris_bot = [
(0, 3, 1), # Reversed winding
(0, 2, 3),
]
for tri in tris_bot:
for idx in tri:
vertices.extend(verts_bot[idx])
normals.extend([0, 0, -1])
# Leading edges
vertices.extend(verts_top[0] + verts_bot[0] + verts_top[1])
normals.extend([1, 0, 0] * 3)
vertices.extend(verts_bot[0] + verts_bot[1] + verts_top[1])
normals.extend([1, 0, 0] * 3)
vertices.extend(verts_top[0] + verts_top[2] + verts_bot[0])
normals.extend([1, 0, 0] * 3)
vertices.extend(verts_bot[0] + verts_top[2] + verts_bot[2])
normals.extend([1, 0, 0] * 3)
return np.array(vertices, dtype='f4'), np.array(normals, dtype='f4')
@dataclass
class EntityState:
"""State of a renderable entity."""
position: np.ndarray
velocity: np.ndarray
rotation: np.ndarray = None
color: Tuple[float, float, float] = (0.5, 0.5, 0.5)
scale: float = 1.0
class KAPSModernRenderer(mglw.WindowConfig):
"""
Modern OpenGL renderer for KAPS visualization.
Clean, professional look with proper lighting and shaders.
"""
gl_version = (3, 3)
title = "KAPS - Kinetic Active Protection System"
window_size = (1280, 720)
aspect_ratio = 16 / 9
resizable = True
samples = 4 # Anti-aliasing
def __init__(self, **kwargs):
super().__init__(**kwargs)
# Enable depth testing
self.ctx.enable(moderngl.DEPTH_TEST)
# Shaders
self.prog = self.ctx.program(
vertex_shader=VERTEX_SHADER,
fragment_shader=FRAGMENT_SHADER,
)
self.line_prog = self.ctx.program(
vertex_shader=LINE_VERTEX_SHADER,
fragment_shader=LINE_FRAGMENT_SHADER,
)
# Create meshes
self._create_meshes()
# Camera - orbiting for proper 3D view
self.camera_distance = 200.0
self.camera_height = 80.0
self.camera_angle = 0.0 # Will animate
self.camera_pos = np.array([150.0, -150.0, 100.0])
self.camera_target = np.array([0.0, 0.0, 50.0])
self.camera_up = np.array([0.0, 0.0, 1.0])
# Create ground grid
self._create_ground_grid()
# Entity states - TABs spread in 3D space around Buzzard
self.buzzard_state = EntityState(
position=np.array([0.0, 0.0, 50.0]),
velocity=np.array([0.0, 30.0, 0.0]),
color=(0.2, 0.35, 0.8)
)
# TABs at diagonal positions for proper 3D spread
cable_len = 40.0
self.tab_states: Dict[str, EntityState] = {
"UP": EntityState(np.array([25, 25, 75]), np.zeros(3), color=(0.2, 0.8, 0.2)),
"DOWN": EntityState(np.array([-25, -25, 25]), np.zeros(3), color=(0.8, 0.2, 0.2)),
"LEFT": EntityState(np.array([-30, 20, 50]), np.zeros(3), color=(0.8, 0.8, 0.2)),
"RIGHT": EntityState(np.array([30, -20, 50]), np.zeros(3), color=(0.8, 0.2, 0.8)),
}
self.tabs_attached = {"UP": True, "DOWN": True, "LEFT": True, "RIGHT": True}
# Simulation interface
self.sim = None
self.env = None
# Time tracking
self.time = 0
def _create_meshes(self):
"""Create GPU meshes."""
# Buzzard fuselage (cylinder + cones) - BIGGER
cyl_v, cyl_n = create_cylinder_mesh(radius=6.0, height=30.0, segments=16)
nose_v, nose_n = create_cone_mesh(radius=6.0, height=12.0, segments=16)
tail_v, tail_n = create_cone_mesh(radius=4.0, height=8.0, segments=16)
# Offset nose and tail
nose_v = nose_v.reshape(-1, 3)
nose_v[:, 1] += 15 # Move to front
nose_v = nose_v.flatten()
tail_v = tail_v.reshape(-1, 3)
tail_v[:, 1] = -tail_v[:, 1] - 15 # Flip and move to back
tail_v = tail_v.flatten()
# Combine fuselage
fuse_v = np.concatenate([cyl_v, nose_v, tail_v])
fuse_n = np.concatenate([cyl_n, nose_n, tail_n])
fuse_c = np.tile([0.2, 0.35, 0.8], len(fuse_v) // 3).astype('f4')
fuse_data = np.zeros(len(fuse_v) // 3, dtype=[
('in_position', 'f4', 3),
('in_normal', 'f4', 3),
('in_color', 'f4', 3),
])
fuse_data['in_position'] = fuse_v.reshape(-1, 3)
fuse_data['in_normal'] = fuse_n.reshape(-1, 3)
fuse_data['in_color'] = fuse_c.reshape(-1, 3)
self.buzzard_vbo = self.ctx.buffer(fuse_data.tobytes())
self.buzzard_vao = self.ctx.vertex_array(
self.prog,
[(self.buzzard_vbo, '3f 3f 3f', 'in_position', 'in_normal', 'in_color')]
)
self.buzzard_vertex_count = len(fuse_v) // 3
# TAB wings
wing_v, wing_n = create_wing_mesh(span=20.0, chord=10.0, thickness=2.0)
wing_c = np.tile([0.8, 0.8, 0.8], len(wing_v) // 3).astype('f4') # Brighter
wing_data = np.zeros(len(wing_v) // 3, dtype=[
('in_position', 'f4', 3),
('in_normal', 'f4', 3),
('in_color', 'f4', 3),
])
wing_data['in_position'] = wing_v.reshape(-1, 3)
wing_data['in_normal'] = wing_n.reshape(-1, 3)
wing_data['in_color'] = wing_c.reshape(-1, 3)
self.wing_vbo = self.ctx.buffer(wing_data.tobytes())
self.wing_vao = self.ctx.vertex_array(
self.prog,
[(self.wing_vbo, '3f 3f 3f', 'in_position', 'in_normal', 'in_color')]
)
self.wing_vertex_count = len(wing_v) // 3
# Cable line buffer (dynamic)
self.cable_vbo = self.ctx.buffer(reserve=1024 * 6 * 4) # Reserve space
def _create_ground_grid(self):
"""Create a ground reference grid for depth perception."""
lines = []
colors = []
grid_size = 500
spacing = 50
for i in range(-grid_size, grid_size + 1, spacing):
# X lines
lines.extend([i, -grid_size, 0, i, grid_size, 0])
colors.extend([0.3, 0.4, 0.3] * 2)
# Y lines
lines.extend([-grid_size, i, 0, grid_size, i, 0])
colors.extend([0.3, 0.4, 0.3] * 2)
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')]
)
self.grid_line_count = len(lines) // 3
def set_simulation(self, sim, env=None):
"""Connect to KAPS simulation."""
self.sim = sim
self.env = env
def update_from_sim(self):
"""Pull state from simulation."""
if self.sim is None:
return
# Buzzard
self.buzzard_state.position = self.sim.mother_drone.position.copy()
self.buzzard_state.velocity = self.sim.mother_drone.velocity.copy()
# TABs
for tab_id in ["UP", "DOWN", "LEFT", "RIGHT"]:
tab = self.sim.tab_array.tabs.get(tab_id)
if tab:
self.tab_states[tab_id].position = tab.position.copy()
self.tab_states[tab_id].velocity = tab.velocity.copy()
self.tabs_attached[tab_id] = tab.is_attached
# Camera follows buzzard
self.camera_target = self.buzzard_state.position.copy()
# Chase camera position
vel = self.buzzard_state.velocity
speed = np.linalg.norm(vel)
if speed > 1:
behind = -vel / speed * 80
else:
behind = np.array([0, -80, 0])
self.camera_pos = self.camera_target + behind + np.array([0, 0, 40])
def on_render(self, time: float, frame_time: float):
"""Render frame."""
self.time = time
# Update from simulation if connected
self.update_from_sim()
# Orbit camera around scene for demo (when no sim)
if self.sim is None:
self.camera_angle = time * 0.3 # Slow orbit
self.camera_pos = np.array([
np.cos(self.camera_angle) * self.camera_distance,
np.sin(self.camera_angle) * self.camera_distance,
self.camera_height
])
self.camera_target = np.array([0.0, 0.0, 50.0])
# Animate TABs orbiting around Buzzard
orbit_radius = 60.0
for i, tab_id in enumerate(["UP", "DOWN", "LEFT", "RIGHT"]):
angle = time * 0.5 + i * np.pi / 2 # Phase offset
z_offset = 20 * np.sin(time * 0.7 + i) # Bobbing
self.tab_states[tab_id].position = self.buzzard_state.position + np.array([
np.cos(angle) * orbit_radius,
np.sin(angle) * orbit_radius,
z_offset
])
# Clear
self.ctx.clear(0.1, 0.15, 0.2) # Dark blue-grey sky
self.ctx.enable(moderngl.DEPTH_TEST)
self.ctx.enable(moderngl.CULL_FACE)
# Matrices
proj = Matrix44.perspective_projection(60.0, self.aspect_ratio, 0.1, 2000.0)
view = Matrix44.look_at(
tuple(self.camera_pos),
tuple(self.camera_target),
tuple(self.camera_up),
)
# Draw ground grid first
mvp = proj * view
self.line_prog['mvp'].write(mvp.astype('f4').tobytes())
self.grid_vao.render(moderngl.LINES)
# Lighting
self.prog['light_pos'].value = tuple(self.camera_pos + np.array([50, 50, 100]))
self.prog['view_pos'].value = tuple(self.camera_pos)
self.prog['ambient_color'].value = (0.6, 0.7, 0.9)
self.prog['ambient_strength'].value = 0.3
self.prog['specular_strength'].value = 0.5
self.prog['shininess'].value = 32.0
self.prog['view'].write(view.astype('f4').tobytes())
self.prog['projection'].write(proj.astype('f4').tobytes())
# Render Buzzard
model = Matrix44.from_translation(self.buzzard_state.position)
# Orient along velocity
vel = self.buzzard_state.velocity
if np.linalg.norm(vel) > 1:
yaw = np.arctan2(vel[0], 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 - orient to face direction of orbit
for i, (tab_id, state) in enumerate(self.tab_states.items()):
if not self.tabs_attached.get(tab_id, False):
continue
model = Matrix44.from_translation(state.position)
# Calculate direction from Buzzard to TAB for orientation
to_buzzard = self.buzzard_state.position - state.position
dist = np.linalg.norm(to_buzzard)
if dist > 1:
# Orient so leading edge faces tangent to orbit (perpendicular to cable)
# First get the cable direction
cable_dir = to_buzzard / dist
# Tangent is perpendicular to cable in XY plane
tangent = np.array([-cable_dir[1], cable_dir[0], 0])
tangent = tangent / (np.linalg.norm(tangent) + 0.001)
# Yaw to face tangent direction
yaw = np.arctan2(tangent[0], tangent[1])
# Bank angle - tilt into the turn
bank = np.pi / 6 # 30 degree bank
model = model * Matrix44.from_z_rotation(yaw) * Matrix44.from_y_rotation(bank)
self.prog['model'].write(model.astype('f4').tobytes())
self.wing_vao.render(moderngl.TRIANGLES)
# Render cables
self._render_cables(proj, view)
def _update_wing_color(self, color: Tuple[float, float, float]):
"""Update wing VBO with new color."""
# For efficiency, we'd want per-instance coloring, but for now:
pass # Using uniform color from original mesh
def _render_cables(self, proj, view):
"""Render cable lines with catenary sag."""
lines = []
colors = []
segments = 12 # Points per cable for curve
for tab_id, state in self.tab_states.items():
if not self.tabs_attached.get(tab_id, False):
continue
# Cable from buzzard to TAB
start = self.buzzard_state.position
end = state.position
# Tension based on distance
dist = np.linalg.norm(end - start)
tension = min(1.0, dist / 80.0)
sag = (1.0 - tension) * 15.0 + 5.0 # More sag when slack
# Color: green when slack, red when taut
color = (0.3 + tension * 0.6, 0.7 * (1 - tension), 0.2)
# Generate curved cable points (catenary approximation)
for i in range(segments):
t1 = i / segments
t2 = (i + 1) / segments
# Lerp positions
p1 = start * (1 - t1) + end * t1
p2 = start * (1 - t2) + end * t2
# Add sag (parabolic, max at middle)
sag1 = sag * 4 * t1 * (1 - t1)
sag2 = sag * 4 * t2 * (1 - t2)
p1[2] -= sag1
p2[2] -= sag2
lines.extend(list(p1) + list(p2))
colors.extend(list(color) * 2)
if not lines:
return
# Create line VAO
line_data = np.array(lines, dtype='f4')
color_data = np.array(colors, dtype='f4')
combined = np.zeros(len(line_data) // 3, dtype=[
('in_position', 'f4', 3),
('in_color', 'f4', 3),
])
combined['in_position'] = line_data.reshape(-1, 3)
combined['in_color'] = color_data.reshape(-1, 3)
vbo = self.ctx.buffer(combined.tobytes())
vao = self.ctx.vertex_array(
self.line_prog,
[(vbo, '3f 3f', 'in_position', 'in_color')]
)
mvp = proj * view
self.line_prog['mvp'].write(mvp.astype('f4').tobytes())
self.ctx.line_width = 3.0 # Note: May not work on all drivers
vao.render(moderngl.LINES)
vbo.release()
vao.release()
def run_standalone():
"""Run renderer standalone for testing."""
mglw.run_window_config(KAPSModernRenderer)
if __name__ == "__main__":
run_standalone()