119 lines
3.9 KiB
Python
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)
|