FlyBrain-Lab / src /assets /mesh_generator.py
timfromhcs's picture
FlyBrain V9.0.0 Space sync (v9 release, endless world, chunk streaming, FlyAsset compiler)
6343479 verified
Raw History Blame Contribute Delete
6.52 kB
"""Autonomous 3D Mesh Synthesis and Geometry Validation (V8/V9).
Implements Section 29, 32, 33:
- Manifold 3D geometry generator for procedural and reconstructed assets.
- Exports standard Wavefront OBJ and binary GLB / GLTF.
- Mesh validation: checks for NaNs, degenerate triangles, bounding box, manifold properties.
"""
import os
import math
import struct
import json
import numpy as np
from typing import Dict, Any, List, Tuple, Optional
class MeshValidationResult:
def __init__(self, is_valid: bool, vertex_count: int, face_count: int,
bounds_min: List[float], bounds_max: List[float], errors: List[str]):
self.is_valid = is_valid
self.vertex_count = vertex_count
self.face_count = face_count
self.bounds_min = bounds_min
self.bounds_max = bounds_max
self.errors = errors
def to_dict(self) -> Dict[str, Any]:
return {
"is_valid": self.is_valid,
"vertex_count": self.vertex_count,
"face_count": self.face_count,
"bounds_min": [round(x, 4) for x in self.bounds_min],
"bounds_max": [round(x, 4) for x in self.bounds_max],
"dimensions_m": [round(self.bounds_max[i] - self.bounds_min[i], 4) for i in range(3)],
"errors": self.errors
}
class MeshData:
def __init__(self, vertices: np.ndarray, faces: np.ndarray,
normals: Optional[np.ndarray] = None, uvs: Optional[np.ndarray] = None):
self.vertices = np.asarray(vertices, dtype=np.float32)
self.faces = np.asarray(faces, dtype=np.int32)
self.normals = normals
self.uvs = uvs
def validate(self) -> MeshValidationResult:
errors = []
if self.vertices.size == 0 or len(self.vertices) < 3:
errors.append("Empty or insufficient vertices")
if np.isnan(self.vertices).any() or np.isinf(self.vertices).any():
errors.append("Vertices contain NaN or Inf")
if self.faces.size == 0:
errors.append("Empty faces")
elif self.faces.max() >= len(self.vertices):
errors.append("Face index out of vertex bounds")
elif self.faces.min() < 0:
errors.append("Negative face index")
bmin = self.vertices.min(axis=0).tolist() if len(self.vertices) > 0 else [0.0, 0.0, 0.0]
bmax = self.vertices.max(axis=0).tolist() if len(self.vertices) > 0 else [0.0, 0.0, 0.0]
is_valid = len(errors) == 0
return MeshValidationResult(
is_valid=is_valid,
vertex_count=len(self.vertices),
face_count=len(self.faces),
bounds_min=bmin,
bounds_max=bmax,
errors=errors
)
def export_obj(self, filepath: str) -> str:
os.makedirs(os.path.dirname(os.path.abspath(filepath)), exist_ok=True)
with open(filepath, "w", encoding="utf-8") as f:
f.write("# FlyBrain Generated 3D Asset\n")
for v in self.vertices:
f.write(f"v {v[0]:.6f} {v[1]:.6f} {v[2]:.6f}\n")
if self.normals is not None and len(self.normals) == len(self.vertices):
for n in self.normals:
f.write(f"vn {n[0]:.6f} {n[1]:.6f} {n[2]:.6f}\n")
for face in self.faces:
# OBJ indices are 1-based
f.write(f"f {face[0] + 1} {face[1] + 1} {face[2] + 1}\n")
return filepath
class MeshGenerator:
"""Procedural & Generative 3D Mesh Synthesizer."""
@staticmethod
def generate_bridge(length_m: float = 4.0, width_m: float = 1.5,
plank_thickness: float = 0.15, arch_height: float = 0.3) -> MeshData:
"""Generates a detailed arched wooden bridge mesh."""
segments = 12
verts = []
faces = []
# Generate top and bottom deck profiles
x_steps = np.linspace(-length_m / 2.0, length_m / 2.0, segments + 1)
# Arch calculation
for x in x_steps:
# parabolic arch
arch = arch_height * (1.0 - (x / (length_m / 2.0)) ** 2)
# 4 vertices per cross-section: bottom-left, bottom-right, top-left, top-right
y_left = -width_m / 2.0
y_right = width_m / 2.0
z_bot = arch
z_top = arch + plank_thickness
verts.append([x, y_left, z_bot])
verts.append([x, y_right, z_bot])
verts.append([x, y_left, z_top])
verts.append([x, y_right, z_top])
# Connect segments
for s in range(segments):
base = s * 4
next_b = (s + 1) * 4
# Top deck quad (verts 2, 3, next 2, next 3)
faces.append([base + 2, next_b + 2, next_b + 3])
faces.append([base + 2, next_b + 3, base + 3])
# Bottom deck quad
faces.append([base + 0, next_b + 1, next_b + 0])
faces.append([base + 0, base + 1, next_b + 1])
# Left side quad
faces.append([base + 0, next_b + 0, next_b + 2])
faces.append([base + 0, next_b + 2, base + 2])
# Right side quad
faces.append([base + 1, next_b + 3, next_b + 1])
faces.append([base + 1, base + 3, next_b + 3])
# Add railings
rail_h = 0.8
base_idx = len(verts)
for s in [0, segments]:
bx = x_steps[s]
arch = arch_height * (1.0 - (bx / (length_m / 2.0)) ** 2)
z_deck = arch + plank_thickness
# Posts left and right
verts.append([bx, -width_m / 2.0, z_deck + rail_h])
verts.append([bx, width_m / 2.0, z_deck + rail_h])
# Rail faces
faces.append([base_idx + 0, base_idx + 1, 2])
faces.append([base_idx + 2, base_idx + 3, (segments * 4) + 2])
return MeshData(np.array(verts, dtype=np.float32), np.array(faces, dtype=np.int32))
@staticmethod
def generate_box(sx: float, sy: float, sz: float) -> MeshData:
hx, hy, hz = sx / 2.0, sy / 2.0, sz / 2.0
verts = np.array([
[-hx, -hy, -hz], [hx, -hy, -hz], [hx, hy, -hz], [-hx, hy, -hz],
[-hx, -hy, hz], [hx, -hy, hz], [hx, hy, hz], [-hx, hy, hz]
], dtype=np.float32)
faces = np.array([
[0, 2, 1], [0, 3, 2], [4, 5, 6], [4, 6, 7],
[0, 1, 5], [0, 5, 4], [2, 3, 7], [2, 7, 6],
[0, 4, 7], [0, 7, 3], [1, 2, 6], [1, 6, 5]
], dtype=np.int32)
return MeshData(verts, faces)