DoorNroll/scripts/convert_blockbench_fbx.py

119 lines
3.9 KiB
Python

#!/usr/bin/env python3
"""
Convert Blockbench ASCII FBX to standard OBJ with normals, UVs, and triangulation for Raylib.
Usage: python scripts/convert_blockbench_fbx.py <input.fbx> [output.obj]
"""
import sys
import os
import re
def convert_fbx_to_obj(fbx_path, obj_path=None):
if obj_path is None:
obj_path = os.path.splitext(fbx_path)[0] + ".obj"
mtl_path = os.path.splitext(obj_path)[0] + ".mtl"
obj_name = os.path.splitext(os.path.basename(obj_path))[0]
tex_name = obj_name + "_textures.png"
with open(fbx_path, "r", encoding="utf-8", errors="ignore") as f:
text = f.read()
# Vertices
v_match = re.search(r"Vertices:\s*\*\d+\s*\{\s*a:\s*([^}]+)\}", text)
if not v_match:
print(f"Error: No vertices found in {fbx_path}")
return False
raw_v = [float(x.strip()) for x in v_match.group(1).split(",")]
min_y = min(raw_v[1::3])
# Ground the model so bottom Y is 0.0
vertices = [(raw_v[i], raw_v[i+1] - min_y, raw_v[i+2]) for i in range(0, len(raw_v), 3)]
# Indices
p_match = re.search(r"PolygonVertexIndex:\s*\*\d+\s*\{\s*a:\s*([^}]+)\}", text)
if not p_match:
print(f"Error: No indices found in {fbx_path}")
return False
raw_p = [int(x.strip()) for x in p_match.group(1).split(",")]
# UVs
uv_match = re.search(r"UV:\s*\*\d+\s*\{\s*a:\s*([^}]+)\}", text)
raw_uv = [float(x.strip()) for x in uv_match.group(1).split(",")] if uv_match else []
uvs = [(raw_uv[i], raw_uv[i+1]) for i in range(0, len(raw_uv), 2)]
# Polygons
polygons_v = []
polygons_uv = []
cur_v = []
cur_uv = []
for i, idx in enumerate(raw_p):
uv_idx = i
if idx < 0:
cur_v.append(~idx)
cur_uv.append(uv_idx)
polygons_v.append(cur_v)
polygons_uv.append(cur_uv)
cur_v = []
cur_uv = []
else:
cur_v.append(idx)
cur_uv.append(uv_idx)
# Compute face normals
def calc_normal(p0, p1, p2):
ax, ay, az = p1[0]-p0[0], p1[1]-p0[1], p1[2]-p0[2]
bx, by, bz = p2[0]-p0[0], p2[1]-p0[1], p2[2]-p0[2]
nx = ay*bz - az*by
ny = az*bx - ax*bz
nz = ax*by - ay*bx
l = (nx*nx + ny*ny + nz*nz)**0.5
if l < 1e-6:
return (0.0, 1.0, 0.0)
return (nx/l, ny/l, nz/l)
normals = []
triangles = []
for pv, puv in zip(polygons_v, polygons_uv):
p0 = vertices[pv[0]]
p1 = vertices[pv[1]]
p2 = vertices[pv[2]]
norm = calc_normal(p0, p1, p2)
normals.append(norm)
norm_idx = len(normals)
for k in range(1, len(pv) - 1):
tri = [
(pv[0], puv[0], norm_idx),
(pv[k], puv[k], norm_idx),
(pv[k+1], puv[k+1], norm_idx)
]
triangles.append(tri)
with open(obj_path, "w", encoding="utf-8") as out:
out.write(f"mtllib {os.path.basename(mtl_path)}\n")
out.write(f"o {obj_name}\n")
for v in vertices:
out.write(f"v {v[0]:.6f} {v[1]:.6f} {v[2]:.6f}\n")
for uv in uvs:
out.write(f"vt {uv[0]:.6f} {uv[1]:.6f}\n")
for n in normals:
out.write(f"vn {n[0]:.6f} {n[1]:.6f} {n[2]:.6f}\n")
out.write(f"usemtl {obj_name}Material\ns 1\n")
for tri in triangles:
face_str = " ".join(f"{vi+1}/{uvi+1}/{ni}" for vi, uvi, ni in tri)
out.write(f"f {face_str}\n")
with open(mtl_path, "w", encoding="utf-8") as out:
out.write(f"newmtl {obj_name}Material\n")
out.write("Kd 1.0 1.0 1.0\n")
out.write(f"map_Kd {tex_name}\n")
print(f"Successfully converted {fbx_path} -> {obj_path} & {mtl_path}")
return True
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: convert_blockbench_fbx.py <file.fbx> [output.obj]")
sys.exit(1)
convert_fbx_to_obj(sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else None)