| """ |
| Enhanced Visual Geometry for KAPS |
| ================================== |
| |
| Proper 3D geometry for: |
| - CABLES: Thick tubes with tension coloring, not thin lines |
| - AIRFOILS: Visible delta wings with thickness and control surfaces |
| - BUZZARD: Detailed mother drone with features |
| - THREATS: Distinctive shapes per threat type |
| |
| All geometry is procedural Panda3D compatible. |
| """ |
|
|
| import numpy as np |
| from typing import Tuple, List |
|
|
| try: |
| from panda3d.core import ( |
| GeomVertexFormat, GeomVertexData, GeomVertexWriter, |
| Geom, GeomTriangles, GeomTristrips, GeomNode, |
| Vec3, Vec4, Point3, |
| LineSegs |
| ) |
| PANDA3D_AVAILABLE = True |
| except ImportError: |
| PANDA3D_AVAILABLE = False |
|
|
|
|
| def create_cable_geometry( |
| start: np.ndarray, |
| end: np.ndarray, |
| radius: float = 0.5, |
| segments: int = 8, |
| color: Tuple[float, float, float, float] = (0.4, 0.4, 0.5, 1.0), |
| tension_color: Tuple[float, float, float, float] = None |
| ) -> GeomNode: |
| """ |
| Create a 3D tube/cable geometry between two points. |
| |
| This creates a visible CYLINDER, not a thin line. |
| |
| Args: |
| start: Start position |
| end: End position |
| radius: Cable thickness (default 0.5m) |
| segments: Number of sides (8 = octagon cross-section) |
| color: Base color |
| tension_color: Color at end if cable is under tension |
| """ |
| if not PANDA3D_AVAILABLE: |
| return None |
| |
| format = GeomVertexFormat.getV3n3c4() |
| vdata = GeomVertexData("cable", format, Geom.UHStatic) |
| |
| vertex = GeomVertexWriter(vdata, "vertex") |
| normal = GeomVertexWriter(vdata, "normal") |
| col = GeomVertexWriter(vdata, "color") |
| |
| |
| direction = end - start |
| length = np.linalg.norm(direction) |
| if length < 0.01: |
| length = 0.01 |
| direction = direction / length |
| |
| |
| if abs(direction[2]) < 0.9: |
| perp1 = np.cross(direction, np.array([0, 0, 1])) |
| else: |
| perp1 = np.cross(direction, np.array([1, 0, 0])) |
| perp1 = perp1 / np.linalg.norm(perp1) |
| perp2 = np.cross(direction, perp1) |
| |
| |
| for t, pos, c in [(0, start, color), (1, end, tension_color or color)]: |
| for i in range(segments + 1): |
| angle = 2 * np.pi * i / segments |
| offset = perp1 * np.cos(angle) * radius + perp2 * np.sin(angle) * radius |
| p = pos + offset |
| n = offset / radius |
| |
| vertex.addData3f(p[0], p[1], p[2]) |
| normal.addData3f(n[0], n[1], n[2]) |
| col.addData4f(*c) |
| |
| |
| prim = GeomTriangles(Geom.UHStatic) |
| for i in range(segments): |
| |
| s0 = i |
| s1 = i + 1 |
| |
| e0 = segments + 1 + i |
| e1 = segments + 1 + i + 1 |
| |
| |
| prim.addVertices(s0, e0, s1) |
| prim.addVertices(s1, e0, e1) |
| |
| geom = Geom(vdata) |
| geom.addPrimitive(prim) |
| node = GeomNode("cable") |
| node.addGeom(geom) |
| return node |
|
|
|
|
| def create_airfoil_geometry( |
| wingspan: float = 4.0, |
| chord: float = 1.5, |
| thickness: float = 0.3, |
| color: Tuple[float, float, float, float] = (0.8, 0.8, 0.8, 1.0), |
| highlight_color: Tuple[float, float, float, float] = None |
| ) -> GeomNode: |
| """ |
| Create a detailed flying wing / delta airfoil geometry. |
| |
| This creates a VISIBLE 3D wing shape with: |
| - Proper delta planform |
| - Visible thickness |
| - Leading and trailing edges |
| - Control surface hints |
| |
| Args: |
| wingspan: Total wing span (tip to tip) |
| chord: Root chord length |
| thickness: Maximum thickness |
| color: Main body color |
| highlight_color: Leading edge accent color |
| """ |
| if not PANDA3D_AVAILABLE: |
| return None |
| |
| format = GeomVertexFormat.getV3n3c4() |
| vdata = GeomVertexData("airfoil", format, Geom.UHStatic) |
| |
| vertex = GeomVertexWriter(vdata, "vertex") |
| normal = GeomVertexWriter(vdata, "normal") |
| col = GeomVertexWriter(vdata, "color") |
| |
| half_span = wingspan / 2 |
| tip_chord = chord * 0.3 |
| |
| |
| le_color = highlight_color or ( |
| min(1.0, color[0] + 0.2), |
| min(1.0, color[1] + 0.2), |
| min(1.0, color[2] + 0.2), |
| color[3] |
| ) |
| |
| |
| |
| airfoil_top = [ |
| (0.0, 0.0), |
| (0.1, 0.04), |
| (0.3, 0.06), |
| (0.6, 0.04), |
| (1.0, 0.01), |
| ] |
| airfoil_bottom = [ |
| (0.0, 0.0), |
| (0.1, -0.02), |
| (0.3, -0.03), |
| (0.6, -0.02), |
| (1.0, -0.01), |
| ] |
| |
| |
| t_scale = thickness * 10 |
| |
| |
| |
| sections = [ |
| (0, chord, color), |
| (half_span * 0.3, chord * 0.8, color), |
| (half_span * 0.6, chord * 0.5, color), |
| (half_span, tip_chord, le_color), |
| ] |
| |
| verts = [] |
| norms = [] |
| colors = [] |
| |
| for y_pos, local_chord, c in sections: |
| for side in [1, -1]: |
| y = y_pos * side |
| |
| |
| for x_frac, z_frac in airfoil_top: |
| x = -local_chord * x_frac + local_chord * 0.3 |
| z = z_frac * t_scale |
| verts.append((x, y, z)) |
| norms.append((0, 0, 1)) |
| colors.append(c) |
| |
| |
| for x_frac, z_frac in airfoil_bottom: |
| x = -local_chord * x_frac + local_chord * 0.3 |
| z = z_frac * t_scale |
| verts.append((x, y, z)) |
| norms.append((0, 0, -1)) |
| colors.append(c) |
| |
| |
| for v, n, c in zip(verts, norms, colors): |
| vertex.addData3f(*v) |
| normal.addData3f(*n) |
| col.addData4f(*c) |
| |
| |
| |
| vdata2 = GeomVertexData("airfoil2", format, Geom.UHStatic) |
| vertex2 = GeomVertexWriter(vdata2, "vertex") |
| normal2 = GeomVertexWriter(vdata2, "normal") |
| col2 = GeomVertexWriter(vdata2, "color") |
| |
| |
| simple_verts = [ |
| |
| (chord * 0.5, 0, thickness), |
| (-chord * 0.3, half_span, thickness/2), |
| (-chord * 0.3, -half_span, thickness/2), |
| (-chord * 0.5, 0, thickness/2), |
| |
| |
| (chord * 0.5, 0, -thickness/2), |
| (-chord * 0.3, half_span, -thickness/2), |
| (-chord * 0.3, -half_span, -thickness/2), |
| (-chord * 0.5, 0, -thickness/2), |
| |
| |
| (chord * 0.4, half_span * 0.3, 0), |
| (chord * 0.4, -half_span * 0.3, 0), |
| |
| |
| (0, half_span * 0.5, thickness), |
| (0, -half_span * 0.5, thickness), |
| (0, half_span * 0.5, -thickness/3), |
| (0, -half_span * 0.5, -thickness/3), |
| ] |
| |
| simple_norms = [ |
| (0, 0, 1), (0.2, 0.5, 0.8), (0.2, -0.5, 0.8), (0, 0, 1), |
| (0, 0, -1), (0.2, 0.5, -0.8), (0.2, -0.5, -0.8), (0, 0, -1), |
| (0.7, 0.7, 0), (0.7, -0.7, 0), |
| (0, 0.3, 0.95), (0, -0.3, 0.95), |
| (0, 0.3, -0.95), (0, -0.3, -0.95), |
| ] |
| |
| for v, n in zip(simple_verts, simple_norms): |
| vertex2.addData3f(*v) |
| normal2.addData3f(*n) |
| col2.addData4f(*color) |
| |
| |
| prim2 = GeomTriangles(Geom.UHStatic) |
| |
| |
| prim2.addVertices(0, 10, 11) |
| prim2.addVertices(0, 11, 2) |
| prim2.addVertices(0, 1, 10) |
| prim2.addVertices(10, 3, 11) |
| prim2.addVertices(10, 1, 3) |
| prim2.addVertices(11, 3, 2) |
| |
| |
| prim2.addVertices(4, 13, 12) |
| prim2.addVertices(4, 6, 13) |
| prim2.addVertices(4, 12, 5) |
| prim2.addVertices(12, 13, 7) |
| prim2.addVertices(12, 7, 5) |
| prim2.addVertices(13, 6, 7) |
| |
| |
| prim2.addVertices(0, 8, 4) |
| prim2.addVertices(0, 4, 9) |
| prim2.addVertices(0, 1, 8) |
| prim2.addVertices(8, 1, 5) |
| prim2.addVertices(8, 5, 4) |
| prim2.addVertices(0, 9, 2) |
| prim2.addVertices(9, 6, 2) |
| prim2.addVertices(9, 4, 6) |
| |
| |
| prim2.addVertices(3, 1, 5) |
| prim2.addVertices(3, 5, 7) |
| prim2.addVertices(3, 2, 6) |
| prim2.addVertices(3, 6, 7) |
| |
| geom2 = Geom(vdata2) |
| geom2.addPrimitive(prim2) |
| node = GeomNode("airfoil") |
| node.addGeom(geom2) |
| return node |
|
|
|
|
| def create_buzzard_geometry( |
| body_length: float = 8.0, |
| body_radius: float = 2.0, |
| color: Tuple[float, float, float, float] = (0.2, 0.3, 0.8, 1.0) |
| ) -> GeomNode: |
| """ |
| Create detailed Buzzard (mother drone) geometry. |
| |
| The Buzzard is the PROTECTED ASSET - it should be visually prominent. |
| |
| Features: |
| - Elongated fuselage |
| - Corkscrew propulsion housing (rear) |
| - Sensor dome (front) |
| - TAB attachment points |
| """ |
| if not PANDA3D_AVAILABLE: |
| return None |
| |
| format = GeomVertexFormat.getV3n3c4() |
| vdata = GeomVertexData("buzzard", format, Geom.UHStatic) |
| |
| vertex = GeomVertexWriter(vdata, "vertex") |
| normal = GeomVertexWriter(vdata, "normal") |
| col = GeomVertexWriter(vdata, "color") |
| |
| segments = 16 |
| length_segments = 8 |
| |
| |
| profile = [ |
| (0.0, 0.3), |
| (0.1, 0.6), |
| (0.2, 0.85), |
| (0.4, 1.0), |
| (0.6, 1.0), |
| (0.8, 0.85), |
| (0.9, 0.6), |
| (1.0, 0.4), |
| ] |
| |
| |
| nose_color = (0.5, 0.6, 0.9, 1.0) |
| engine_color = (0.3, 0.3, 0.4, 1.0) |
| |
| |
| for i, (t, r_factor) in enumerate(profile): |
| x = body_length * (t - 0.5) |
| r = body_radius * r_factor |
| |
| |
| if t < 0.2: |
| c = nose_color |
| elif t > 0.8: |
| c = engine_color |
| else: |
| c = color |
| |
| for j in range(segments + 1): |
| angle = 2 * np.pi * j / segments |
| y = r * np.cos(angle) |
| z = r * np.sin(angle) |
| |
| |
| n = np.array([0, np.cos(angle), np.sin(angle)]) |
| |
| vertex.addData3f(x, y, z) |
| normal.addData3f(n[0], n[1], n[2]) |
| col.addData4f(*c) |
| |
| |
| prim = GeomTriangles(Geom.UHStatic) |
| |
| for i in range(len(profile) - 1): |
| for j in range(segments): |
| |
| v0 = i * (segments + 1) + j |
| v1 = i * (segments + 1) + j + 1 |
| |
| v2 = (i + 1) * (segments + 1) + j |
| v3 = (i + 1) * (segments + 1) + j + 1 |
| |
| prim.addVertices(v0, v2, v1) |
| prim.addVertices(v1, v2, v3) |
| |
| geom = Geom(vdata) |
| geom.addPrimitive(prim) |
| node = GeomNode("buzzard") |
| node.addGeom(geom) |
| return node |
|
|
|
|
| def create_threat_geometry( |
| threat_type: str = "missile", |
| size: float = 2.0, |
| color: Tuple[float, float, float, float] = (1.0, 0.2, 0.1, 1.0) |
| ) -> GeomNode: |
| """ |
| Create threat-specific geometry. |
| |
| Different shapes for different threat types: |
| - missile: Elongated cylinder with fins |
| - drone: Quad-copter shape |
| - swarm: Small sphere |
| """ |
| if not PANDA3D_AVAILABLE: |
| return None |
| |
| format = GeomVertexFormat.getV3n3c4() |
| vdata = GeomVertexData(threat_type, format, Geom.UHStatic) |
| |
| vertex = GeomVertexWriter(vdata, "vertex") |
| normal = GeomVertexWriter(vdata, "normal") |
| col = GeomVertexWriter(vdata, "color") |
| |
| if threat_type in ["missile", "IR_MISSILE", "RADAR_MISSILE"]: |
| |
| length = size * 2 |
| radius = size * 0.3 |
| segments = 8 |
| |
| |
| vertex.addData3f(length/2, 0, 0) |
| normal.addData3f(1, 0, 0) |
| col.addData4f(*color) |
| |
| |
| for i in range(segments + 1): |
| angle = 2 * np.pi * i / segments |
| y = radius * np.cos(angle) |
| z = radius * np.sin(angle) |
| |
| |
| vertex.addData3f(length/4, y * 0.5, z * 0.5) |
| normal.addData3f(0.5, np.cos(angle) * 0.5, np.sin(angle) * 0.5) |
| col.addData4f(*color) |
| |
| |
| vertex.addData3f(-length/4, y, z) |
| normal.addData3f(0, np.cos(angle), np.sin(angle)) |
| col.addData4f(*color) |
| |
| |
| vertex.addData3f(-length/2, y * 0.7, z * 0.7) |
| normal.addData3f(-0.3, np.cos(angle) * 0.7, np.sin(angle) * 0.7) |
| col.addData4f(color[0] * 0.5, color[1] * 0.5, color[2] * 0.5, 1) |
| |
| prim = GeomTriangles(Geom.UHStatic) |
| |
| |
| for i in range(segments): |
| prim.addVertices(0, 1 + i * 3, 1 + (i + 1) * 3) |
| |
| |
| for i in range(segments): |
| n = 1 + i * 3 |
| nn = 1 + ((i + 1) % (segments + 1)) * 3 |
| |
| prim.addVertices(n, n + 1, nn) |
| prim.addVertices(nn, n + 1, nn + 1) |
| |
| prim.addVertices(n + 1, n + 2, nn + 1) |
| prim.addVertices(nn + 1, n + 2, nn + 2) |
| |
| else: |
| |
| segments = 8 |
| for i in range(segments + 1): |
| lat = np.pi * (-0.5 + float(i) / segments) |
| for j in range(segments + 1): |
| lon = 2 * np.pi * float(j) / segments |
| |
| x = size * np.cos(lat) * np.cos(lon) |
| y = size * np.cos(lat) * np.sin(lon) |
| z = size * np.sin(lat) |
| |
| vertex.addData3f(x, y, z) |
| normal.addData3f(x/size, y/size, z/size) |
| col.addData4f(*color) |
| |
| prim = GeomTriangles(Geom.UHStatic) |
| for i in range(segments): |
| for j in range(segments): |
| v0 = i * (segments + 1) + j |
| v1 = v0 + 1 |
| v2 = v0 + segments + 1 |
| v3 = v2 + 1 |
| prim.addVertices(v0, v2, v1) |
| prim.addVertices(v1, v2, v3) |
| |
| geom = Geom(vdata) |
| geom.addPrimitive(prim) |
| node = GeomNode(threat_type) |
| node.addGeom(geom) |
| return node |
|
|
|
|
| |
| |
| |
|
|
| TAB_COLORS = { |
| "UP": (0.1, 0.9, 0.2, 1.0), |
| "DOWN": (0.9, 0.1, 0.1, 1.0), |
| "LEFT": (0.9, 0.9, 0.1, 1.0), |
| "RIGHT": (0.9, 0.1, 0.9, 1.0), |
| } |
|
|
| CABLE_COLORS = { |
| "normal": (0.5, 0.5, 0.6, 1.0), |
| "tension_low": (0.4, 0.6, 0.4, 1.0), |
| "tension_high": (0.8, 0.4, 0.2, 1.0), |
| "near_limit": (1.0, 0.2, 0.1, 1.0), |
| } |
|
|