#!/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 [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 [output.obj]") sys.exit(1) convert_fbx_to_obj(sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else None)