|
|
""" |
|
|
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), |
|
|
} |
|
|
|