added collision conversion
This commit is contained in:
325
scripts/preprocess/convert_lci_to_ply.py
Normal file
325
scripts/preprocess/convert_lci_to_ply.py
Normal file
@@ -0,0 +1,325 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
XGrids LCI to PLY Converter
|
||||
Converts XGrids .lci collision files to standard .ply format
|
||||
|
||||
Usage:
|
||||
# Convert with default output name (collision_mesh.ply)
|
||||
python convert_lci_to_ply.py path/to/collision.lci
|
||||
|
||||
# Specify custom output file
|
||||
python convert_lci_to_ply.py path/to/collision.lci output/mesh.ply
|
||||
|
||||
# Verbose output
|
||||
python convert_lci_to_ply.py path/to/collision.lci -v
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Constants
|
||||
LCI_MAGIC = 0x6C6C6F63 # 'coll' in little-endian
|
||||
MESH_HEADER_SIZE = 40 # bytes per mesh header
|
||||
|
||||
|
||||
def read_lci_file(filepath, verbose=False):
|
||||
"""
|
||||
Parse XGrids .lci collision file according to official LCI specification
|
||||
|
||||
Args:
|
||||
filepath: Path to input .lci file
|
||||
verbose: Enable verbose output
|
||||
|
||||
Returns:
|
||||
Tuple of (vertices, faces) where:
|
||||
- vertices: List of (x, y, z) tuples
|
||||
- faces: List of (v0, v1, v2) tuples with vertex indices
|
||||
"""
|
||||
filepath = Path(filepath)
|
||||
|
||||
if not filepath.exists():
|
||||
raise FileNotFoundError(f"Input file not found: {filepath}")
|
||||
|
||||
with open(filepath, "rb") as f:
|
||||
lci_data = f.read()
|
||||
|
||||
# Validate minimum file size
|
||||
if len(lci_data) < 48:
|
||||
raise ValueError("File too small to be valid LCI format (minimum 48 bytes required)")
|
||||
|
||||
# Warn about large files that may cause memory issues
|
||||
file_size_mb = len(lci_data) / (1024 * 1024)
|
||||
if file_size_mb > 100 and verbose:
|
||||
print(f"Warning: Large file ({file_size_mb:.1f} MB) loaded into memory")
|
||||
|
||||
# Read main header
|
||||
magic = struct.unpack("<I", lci_data[0:4])[0]
|
||||
if magic != LCI_MAGIC:
|
||||
raise ValueError(
|
||||
f"Invalid file format. Expected magic 0x{LCI_MAGIC:08X}, got 0x{magic:08X}"
|
||||
)
|
||||
|
||||
version = struct.unpack("<I", lci_data[4:8])[0]
|
||||
header_len = struct.unpack("<I", lci_data[8:12])[0]
|
||||
|
||||
min_x, min_y, min_z = struct.unpack("<fff", lci_data[12:24])
|
||||
max_x, max_y, max_z = struct.unpack("<fff", lci_data[24:36])
|
||||
|
||||
cell_length_x = struct.unpack("<f", lci_data[36:40])[0]
|
||||
cell_length_y = struct.unpack("<f", lci_data[40:44])[0]
|
||||
mesh_num = struct.unpack("<I", lci_data[44:48])[0]
|
||||
|
||||
# Validate mesh count
|
||||
if mesh_num == 0:
|
||||
raise ValueError("LCI file contains no mesh data")
|
||||
|
||||
# Validate header length
|
||||
expected_header = 48 + mesh_num * MESH_HEADER_SIZE
|
||||
if header_len != expected_header:
|
||||
raise ValueError(
|
||||
f"Header length mismatch: expected {expected_header}, got {header_len}"
|
||||
)
|
||||
|
||||
if verbose:
|
||||
print("LCI File Information:")
|
||||
print(f" Version: {version}")
|
||||
print(f" Header length: {header_len} bytes")
|
||||
print(
|
||||
f" Bounding box: min({min_x:.2f}, {min_y:.2f}, {min_z:.2f}) "
|
||||
f"max({max_x:.2f}, {max_y:.2f}, {max_z:.2f})"
|
||||
)
|
||||
print(f" Cell length: X={cell_length_x:.2f}, Y={cell_length_y:.2f}")
|
||||
print(f" Mesh count: {mesh_num}\n")
|
||||
|
||||
# Read mesh headers (40 bytes each, starting at offset 48)
|
||||
meshes = []
|
||||
mesh_header_offset = 48
|
||||
|
||||
for i in range(mesh_num):
|
||||
offset = mesh_header_offset + i * MESH_HEADER_SIZE
|
||||
|
||||
# Validate buffer bounds for mesh header
|
||||
if offset + MESH_HEADER_SIZE > len(lci_data):
|
||||
raise ValueError(
|
||||
f"Mesh header {i} exceeds file size (offset {offset}, file size {len(lci_data)})"
|
||||
)
|
||||
|
||||
index_x = struct.unpack("<I", lci_data[offset : offset + 4])[0]
|
||||
index_y = struct.unpack("<I", lci_data[offset + 4 : offset + 8])[0]
|
||||
data_offset = struct.unpack("<Q", lci_data[offset + 8 : offset + 16])[0]
|
||||
bytes_size = struct.unpack("<Q", lci_data[offset + 16 : offset + 24])[0]
|
||||
vertex_num = struct.unpack("<I", lci_data[offset + 24 : offset + 28])[0]
|
||||
face_num = struct.unpack("<I", lci_data[offset + 28 : offset + 32])[0]
|
||||
bvh_size = struct.unpack("<I", lci_data[offset + 32 : offset + 36])[0]
|
||||
|
||||
meshes.append(
|
||||
{
|
||||
"index_x": index_x,
|
||||
"index_y": index_y,
|
||||
"offset": data_offset,
|
||||
"bytes_size": bytes_size,
|
||||
"vertex_num": vertex_num,
|
||||
"face_num": face_num,
|
||||
"bvh_size": bvh_size,
|
||||
}
|
||||
)
|
||||
|
||||
if verbose:
|
||||
print(
|
||||
f"Mesh {i}: grid({index_x},{index_y}), "
|
||||
f"{vertex_num:,} verts, {face_num:,} faces"
|
||||
)
|
||||
|
||||
if verbose:
|
||||
print()
|
||||
|
||||
# Read all mesh data
|
||||
all_vertices = []
|
||||
all_faces = []
|
||||
global_vertex_offset = 0
|
||||
|
||||
for i, mesh in enumerate(meshes):
|
||||
mesh_offset = mesh["offset"]
|
||||
vertex_num = mesh["vertex_num"]
|
||||
face_num = mesh["face_num"]
|
||||
bytes_size = mesh["bytes_size"]
|
||||
bvh_size = mesh["bvh_size"]
|
||||
|
||||
# Validate mesh data bounds
|
||||
expected_data_size = (vertex_num * 12) + (face_num * 12) + bvh_size
|
||||
if expected_data_size != bytes_size:
|
||||
raise ValueError(
|
||||
f"Mesh {i} data size mismatch: expected {expected_data_size} bytes, "
|
||||
f"header specifies {bytes_size} bytes"
|
||||
)
|
||||
|
||||
if mesh_offset + expected_data_size > len(lci_data):
|
||||
raise ValueError(
|
||||
f"Mesh {i} data exceeds file size (offset {mesh_offset}, "
|
||||
f"data size {expected_data_size}, file size {len(lci_data)})"
|
||||
)
|
||||
|
||||
# Read vertices
|
||||
vertices = []
|
||||
pos = mesh_offset
|
||||
|
||||
for j in range(vertex_num):
|
||||
x, y, z = struct.unpack("<fff", lci_data[pos : pos + 12])
|
||||
vertices.append((x, y, z))
|
||||
pos += 12
|
||||
|
||||
# Read faces
|
||||
faces = []
|
||||
for j in range(face_num):
|
||||
v0, v1, v2 = struct.unpack("<III", lci_data[pos : pos + 12])
|
||||
|
||||
# Validate face indices
|
||||
if v0 >= vertex_num or v1 >= vertex_num or v2 >= vertex_num:
|
||||
raise ValueError(
|
||||
f"Invalid face indices in mesh {i}: ({v0}, {v1}, {v2}), "
|
||||
f"vertex_num={vertex_num}"
|
||||
)
|
||||
|
||||
# Adjust indices to global vertex offset
|
||||
faces.append(
|
||||
(
|
||||
v0 + global_vertex_offset,
|
||||
v1 + global_vertex_offset,
|
||||
v2 + global_vertex_offset,
|
||||
)
|
||||
)
|
||||
pos += 12
|
||||
|
||||
all_vertices.extend(vertices)
|
||||
all_faces.extend(faces)
|
||||
global_vertex_offset += len(vertices)
|
||||
|
||||
if verbose:
|
||||
print(f"Total: {len(all_vertices):,} vertices, {len(all_faces):,} faces")
|
||||
|
||||
return all_vertices, all_faces
|
||||
|
||||
|
||||
def write_ply_ascii(filepath, vertices, faces, verbose=False):
|
||||
"""
|
||||
Write mesh data to ASCII PLY file
|
||||
|
||||
Args:
|
||||
filepath: Path to output .ply file
|
||||
vertices: List of (x, y, z) vertex tuples
|
||||
faces: List of (v0, v1, v2) face tuples
|
||||
verbose: Enable verbose output
|
||||
"""
|
||||
filepath = Path(filepath)
|
||||
|
||||
# Create output directory if needed
|
||||
filepath.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if verbose:
|
||||
print(f"\nWriting ASCII PLY to: {filepath}")
|
||||
|
||||
with open(filepath, "w") as f:
|
||||
# Write header
|
||||
f.write("ply\n")
|
||||
f.write("format ascii 1.0\n")
|
||||
f.write(f"element vertex {len(vertices)}\n")
|
||||
f.write("property float x\n")
|
||||
f.write("property float y\n")
|
||||
f.write("property float z\n")
|
||||
f.write(f"element face {len(faces)}\n")
|
||||
f.write("property list uchar int vertex_indices\n")
|
||||
f.write("end_header\n")
|
||||
|
||||
# Write vertices with full precision
|
||||
for v in vertices:
|
||||
f.write(f"{v[0]:.9g} {v[1]:.9g} {v[2]:.9g}\n")
|
||||
|
||||
# Write faces
|
||||
for face in faces:
|
||||
f.write(f"3 {face[0]} {face[1]} {face[2]}\n")
|
||||
|
||||
if verbose:
|
||||
file_size_kb = filepath.stat().st_size / 1024
|
||||
print(f" File size: {file_size_kb:.1f} KB")
|
||||
|
||||
|
||||
def parse_args():
|
||||
"""Parse command line arguments"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Convert XGrids .lci collision files to .ply format",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
# Convert with default output name (collision_mesh.ply in same directory)
|
||||
python convert_lci_to_ply.py collision.lci
|
||||
|
||||
# Specify custom output file
|
||||
python convert_lci_to_ply.py collision.lci output/mesh.ply
|
||||
|
||||
# Verbose output
|
||||
python convert_lci_to_ply.py collision.lci -v
|
||||
""",
|
||||
)
|
||||
|
||||
parser.add_argument("input", type=Path, help="Input .lci collision file")
|
||||
|
||||
parser.add_argument(
|
||||
"output",
|
||||
type=Path,
|
||||
nargs="?",
|
||||
help="Output .ply file (default: collision_mesh.ply in same directory as input)",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-v", "--verbose", action="store_true", help="Enable verbose output"
|
||||
)
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point"""
|
||||
args = parse_args()
|
||||
|
||||
# Determine output path
|
||||
output_path = (
|
||||
args.input.parent / "collision_mesh.ply" if args.output is None else args.output
|
||||
)
|
||||
|
||||
if args.verbose:
|
||||
print("=" * 60)
|
||||
print(f"Converting: {args.input}")
|
||||
print(f"Output: {output_path}")
|
||||
print("=" * 60 + "\n")
|
||||
|
||||
try:
|
||||
# Read LCI file
|
||||
vertices, faces = read_lci_file(args.input, verbose=args.verbose)
|
||||
|
||||
if not vertices:
|
||||
print("Error: No mesh data extracted from file", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Write PLY file
|
||||
write_ply_ascii(output_path, vertices, faces, verbose=args.verbose)
|
||||
|
||||
if args.verbose:
|
||||
print("\n" + "=" * 60)
|
||||
print("✓ Conversion successful!")
|
||||
else:
|
||||
print(f"✓ Converted {args.input} -> {output_path}")
|
||||
print(f" {len(vertices):,} vertices, {len(faces):,} faces")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n✗ Conversion failed: {e}", file=sys.stderr)
|
||||
if args.verbose:
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user