|
@@ -0,0 +1,504 @@
|
|
|
+"""Split single OBJ model into mutliple OBJ files by materials
|
|
|
+
|
|
|
+-------------------------
|
|
|
+How to use this converter
|
|
|
+-------------------------
|
|
|
+
|
|
|
+python split_obj.py -i infile.obj -o outfile
|
|
|
+
|
|
|
+Will generate:
|
|
|
+
|
|
|
+outfile_000.obj
|
|
|
+outfile_001.obj
|
|
|
+
|
|
|
+...
|
|
|
+
|
|
|
+outfile_XXX.obj
|
|
|
+
|
|
|
+-------------------------------------
|
|
|
+Parser based on format description
|
|
|
+-------------------------------------
|
|
|
+
|
|
|
+ http://en.wikipedia.org/wiki/Obj
|
|
|
+
|
|
|
+------
|
|
|
+Author
|
|
|
+------
|
|
|
+AlteredQualia http://alteredqualia.com
|
|
|
+
|
|
|
+"""
|
|
|
+
|
|
|
+import fileinput
|
|
|
+import operator
|
|
|
+import random
|
|
|
+import os.path
|
|
|
+import getopt
|
|
|
+import sys
|
|
|
+import struct
|
|
|
+import math
|
|
|
+import glob
|
|
|
+
|
|
|
+# #####################################################
|
|
|
+# Configuration
|
|
|
+# #####################################################
|
|
|
+TRUNCATE = False
|
|
|
+SCALE = 1.0
|
|
|
+
|
|
|
+
|
|
|
+# #####################################################
|
|
|
+# Templates
|
|
|
+# #####################################################
|
|
|
+TEMPLATE_OBJ = u"""\
|
|
|
+################################
|
|
|
+# OBJ generated by split_obj.py
|
|
|
+################################
|
|
|
+# Faces: %(nfaces)d
|
|
|
+# Vertices: %(nvertices)d
|
|
|
+# Normals: %(nnormals)d
|
|
|
+# UVs: %(nuvs)d
|
|
|
+################################
|
|
|
+
|
|
|
+# vertices
|
|
|
+
|
|
|
+%(vertices)s
|
|
|
+
|
|
|
+# normals
|
|
|
+
|
|
|
+%(normals)s
|
|
|
+
|
|
|
+# uvs
|
|
|
+
|
|
|
+%(uvs)s
|
|
|
+
|
|
|
+# faces
|
|
|
+
|
|
|
+%(faces)s
|
|
|
+"""
|
|
|
+
|
|
|
+TEMPLATE_VERTEX = "v %f %f %f"
|
|
|
+TEMPLATE_VERTEX_TRUNCATE = "v %d %d %d"
|
|
|
+
|
|
|
+TEMPLATE_NORMAL = "vn %.5g %.5g %.5g"
|
|
|
+TEMPLATE_UV = "vt %.5g %.5g"
|
|
|
+
|
|
|
+TEMPLATE_FACE3_V = "f %d %d %d"
|
|
|
+TEMPLATE_FACE4_V = "f %d %d %d %d"
|
|
|
+
|
|
|
+TEMPLATE_FACE3_VT = "f %d/%d %d/%d %d/%d"
|
|
|
+TEMPLATE_FACE4_VT = "f %d/%d %d/%d %d/%d %d/%d"
|
|
|
+
|
|
|
+TEMPLATE_FACE3_VN = "f %d//%d %d//%d %d//%d"
|
|
|
+TEMPLATE_FACE4_VN = "f %d//%d %d//%d %d//%d %d//%d"
|
|
|
+
|
|
|
+TEMPLATE_FACE3_VTN = "f %d/%d/%d %d/%d/%d %d/%d/%d"
|
|
|
+TEMPLATE_FACE4_VTN = "f %d/%d/%d %d/%d/%d %d/%d/%d %d/%d/%d"
|
|
|
+
|
|
|
+
|
|
|
+# #####################################################
|
|
|
+# Utils
|
|
|
+# #####################################################
|
|
|
+def file_exists(filename):
|
|
|
+ """Return true if file exists and is accessible for reading.
|
|
|
+
|
|
|
+ Should be safer than just testing for existence due to links and
|
|
|
+ permissions magic on Unix filesystems.
|
|
|
+
|
|
|
+ @rtype: boolean
|
|
|
+ """
|
|
|
+
|
|
|
+ try:
|
|
|
+ f = open(filename, 'r')
|
|
|
+ f.close()
|
|
|
+ return True
|
|
|
+ except IOError:
|
|
|
+ return False
|
|
|
+
|
|
|
+# #####################################################
|
|
|
+# OBJ parser
|
|
|
+# #####################################################
|
|
|
+def parse_vertex(text):
|
|
|
+ """Parse text chunk specifying single vertex.
|
|
|
+
|
|
|
+ Possible formats:
|
|
|
+ vertex index
|
|
|
+ vertex index / texture index
|
|
|
+ vertex index / texture index / normal index
|
|
|
+ vertex index / / normal index
|
|
|
+ """
|
|
|
+
|
|
|
+ v = 0
|
|
|
+ t = 0
|
|
|
+ n = 0
|
|
|
+
|
|
|
+ chunks = text.split("/")
|
|
|
+
|
|
|
+ v = int(chunks[0])
|
|
|
+ if len(chunks) > 1:
|
|
|
+ if chunks[1]:
|
|
|
+ t = int(chunks[1])
|
|
|
+ if len(chunks) > 2:
|
|
|
+ if chunks[2]:
|
|
|
+ n = int(chunks[2])
|
|
|
+
|
|
|
+ return { 'v': v, 't': t, 'n': n }
|
|
|
+
|
|
|
+def parse_obj(fname):
|
|
|
+ """Parse OBJ file.
|
|
|
+ """
|
|
|
+
|
|
|
+ vertices = []
|
|
|
+ normals = []
|
|
|
+ uvs = []
|
|
|
+
|
|
|
+ faces = []
|
|
|
+
|
|
|
+ materials = {}
|
|
|
+ mcounter = 0
|
|
|
+ mcurrent = 0
|
|
|
+
|
|
|
+ mtllib = ""
|
|
|
+
|
|
|
+ # current face state
|
|
|
+ group = 0
|
|
|
+ object = 0
|
|
|
+ smooth = 0
|
|
|
+
|
|
|
+ for line in fileinput.input(fname):
|
|
|
+ chunks = line.split()
|
|
|
+ if len(chunks) > 0:
|
|
|
+
|
|
|
+ # Vertices as (x,y,z) coordinates
|
|
|
+ # v 0.123 0.234 0.345
|
|
|
+ if chunks[0] == "v" and len(chunks) == 4:
|
|
|
+ x = float(chunks[1])
|
|
|
+ y = float(chunks[2])
|
|
|
+ z = float(chunks[3])
|
|
|
+ vertices.append([x,y,z])
|
|
|
+
|
|
|
+ # Normals in (x,y,z) form; normals might not be unit
|
|
|
+ # vn 0.707 0.000 0.707
|
|
|
+ if chunks[0] == "vn" and len(chunks) == 4:
|
|
|
+ x = float(chunks[1])
|
|
|
+ y = float(chunks[2])
|
|
|
+ z = float(chunks[3])
|
|
|
+ normals.append([x,y,z])
|
|
|
+
|
|
|
+ # Texture coordinates in (u,v[,w]) coordinates, w is optional
|
|
|
+ # vt 0.500 -1.352 [0.234]
|
|
|
+ if chunks[0] == "vt" and len(chunks) >= 3:
|
|
|
+ u = float(chunks[1])
|
|
|
+ v = float(chunks[2])
|
|
|
+ w = 0
|
|
|
+ if len(chunks)>3:
|
|
|
+ w = float(chunks[3])
|
|
|
+ uvs.append([u,v,w])
|
|
|
+
|
|
|
+ # Face
|
|
|
+ if chunks[0] == "f" and len(chunks) >= 4:
|
|
|
+ vertex_index = []
|
|
|
+ uv_index = []
|
|
|
+ normal_index = []
|
|
|
+
|
|
|
+ for v in chunks[1:]:
|
|
|
+ vertex = parse_vertex(v)
|
|
|
+ if vertex['v']:
|
|
|
+ vertex_index.append(vertex['v'])
|
|
|
+ if vertex['t']:
|
|
|
+ uv_index.append(vertex['t'])
|
|
|
+ if vertex['n']:
|
|
|
+ normal_index.append(vertex['n'])
|
|
|
+
|
|
|
+ faces.append({
|
|
|
+ 'vertex':vertex_index,
|
|
|
+ 'uv':uv_index,
|
|
|
+ 'normal':normal_index,
|
|
|
+
|
|
|
+ 'material':mcurrent,
|
|
|
+ 'group':group,
|
|
|
+ 'object':object,
|
|
|
+ 'smooth':smooth,
|
|
|
+ })
|
|
|
+
|
|
|
+ # Group
|
|
|
+ if chunks[0] == "g" and len(chunks) == 2:
|
|
|
+ group = chunks[1]
|
|
|
+
|
|
|
+ # Object
|
|
|
+ if chunks[0] == "o" and len(chunks) == 2:
|
|
|
+ object = chunks[1]
|
|
|
+
|
|
|
+ # Materials definition
|
|
|
+ if chunks[0] == "mtllib" and len(chunks) == 2:
|
|
|
+ mtllib = chunks[1]
|
|
|
+
|
|
|
+ # Material
|
|
|
+ if chunks[0] == "usemtl" and len(chunks) == 2:
|
|
|
+ material = chunks[1]
|
|
|
+ if not material in materials:
|
|
|
+ mcurrent = mcounter
|
|
|
+ materials[material] = mcounter
|
|
|
+ mcounter += 1
|
|
|
+ else:
|
|
|
+ mcurrent = materials[material]
|
|
|
+
|
|
|
+ # Smooth shading
|
|
|
+ if chunks[0] == "s" and len(chunks) == 2:
|
|
|
+ smooth = chunks[1]
|
|
|
+
|
|
|
+ return faces, vertices, uvs, normals, materials, mtllib
|
|
|
+
|
|
|
+# #############################################################################
|
|
|
+# API - Breaker
|
|
|
+# #############################################################################
|
|
|
+def break_obj(infile, outfile):
|
|
|
+ """Break infile.obj to outfile.obj
|
|
|
+ """
|
|
|
+
|
|
|
+ if not file_exists(infile):
|
|
|
+ print "Couldn't find [%s]" % infile
|
|
|
+ return
|
|
|
+
|
|
|
+ faces, vertices, uvs, normals, materials, mtllib = parse_obj(infile)
|
|
|
+
|
|
|
+ # sort faces by materials
|
|
|
+
|
|
|
+ chunks = {}
|
|
|
+
|
|
|
+ for face in faces:
|
|
|
+ material = face["material"]
|
|
|
+ if not material in chunks:
|
|
|
+ chunks[material] = {"faces": [], "vertices": set(), "normals": set(), "uvs": set()}
|
|
|
+
|
|
|
+ chunks[material]["faces"].append(face)
|
|
|
+
|
|
|
+ # extract unique vertex / normal / uv indices used per chunk
|
|
|
+
|
|
|
+ for material in chunks:
|
|
|
+ chunk = chunks[material]
|
|
|
+ for face in chunk["faces"]:
|
|
|
+ for i in face["vertex"]:
|
|
|
+ chunk["vertices"].add(i)
|
|
|
+
|
|
|
+ for i in face["normal"]:
|
|
|
+ chunk["normals"].add(i)
|
|
|
+
|
|
|
+ for i in face["uv"]:
|
|
|
+ chunk["uvs"].add(i)
|
|
|
+
|
|
|
+ # generate new OBJs
|
|
|
+
|
|
|
+ for mi, material in enumerate(chunks):
|
|
|
+ chunk = chunks[material]
|
|
|
+
|
|
|
+ # generate separate vertex / normal / uv index lists for each chunk
|
|
|
+ # (including mapping from original to new indices)
|
|
|
+
|
|
|
+ # get well defined order
|
|
|
+
|
|
|
+ new_vertices = list(chunk["vertices"])
|
|
|
+ new_normals = list(chunk["normals"])
|
|
|
+ new_uvs = list(chunk["uvs"])
|
|
|
+
|
|
|
+ # map original => new indices
|
|
|
+
|
|
|
+ vmap = {}
|
|
|
+ for i, v in enumerate(new_vertices):
|
|
|
+ vmap[v] = i + 1
|
|
|
+
|
|
|
+ nmap = {}
|
|
|
+ for i, n in enumerate(new_normals):
|
|
|
+ nmap[n] = i + 1
|
|
|
+
|
|
|
+ tmap = {}
|
|
|
+ for i, t in enumerate(new_uvs):
|
|
|
+ tmap[t] = i + 1
|
|
|
+
|
|
|
+
|
|
|
+ # vertices
|
|
|
+
|
|
|
+ pieces = []
|
|
|
+ for i in new_vertices:
|
|
|
+ vertex = vertices[i-1]
|
|
|
+ txt = TEMPLATE_VERTEX % (vertex[0], vertex[1], vertex[2])
|
|
|
+ pieces.append(txt)
|
|
|
+
|
|
|
+ str_vertices = "\n".join(pieces)
|
|
|
+
|
|
|
+ # normals
|
|
|
+
|
|
|
+ pieces = []
|
|
|
+ for i in new_normals:
|
|
|
+ normal = normals[i-1]
|
|
|
+ txt = TEMPLATE_NORMAL % (normal[0], normal[1], normal[2])
|
|
|
+ pieces.append(txt)
|
|
|
+
|
|
|
+ str_normals = "\n".join(pieces)
|
|
|
+
|
|
|
+ # uvs
|
|
|
+
|
|
|
+ pieces = []
|
|
|
+ for i in new_uvs:
|
|
|
+ uv = uvs[i-1]
|
|
|
+ txt = TEMPLATE_UV % (uv[0], uv[1])
|
|
|
+ pieces.append(txt)
|
|
|
+
|
|
|
+ str_uvs = "\n".join(pieces)
|
|
|
+
|
|
|
+ # faces
|
|
|
+
|
|
|
+ pieces = []
|
|
|
+
|
|
|
+ for face in chunk["faces"]:
|
|
|
+
|
|
|
+ txt = ""
|
|
|
+
|
|
|
+ fv = face["vertex"]
|
|
|
+ fn = face["normal"]
|
|
|
+ ft = face["uv"]
|
|
|
+
|
|
|
+ if len(fv) == 3:
|
|
|
+
|
|
|
+ va = vmap[fv[0]]
|
|
|
+ vb = vmap[fv[1]]
|
|
|
+ vc = vmap[fv[2]]
|
|
|
+
|
|
|
+ if len(fn) == 3 and len(ft) == 3:
|
|
|
+ na = nmap[fn[0]]
|
|
|
+ nb = nmap[fn[1]]
|
|
|
+ nc = nmap[fn[2]]
|
|
|
+
|
|
|
+ ta = tmap[ft[0]]
|
|
|
+ tb = tmap[ft[1]]
|
|
|
+ tc = tmap[ft[2]]
|
|
|
+
|
|
|
+ txt = TEMPLATE_FACE3_VTN % (va, ta, na, vb, tb, nb, vc, tc, nc)
|
|
|
+
|
|
|
+ elif len(fn) == 3:
|
|
|
+ na = nmap[fn[0]]
|
|
|
+ nb = nmap[fn[1]]
|
|
|
+ nc = nmap[fn[2]]
|
|
|
+
|
|
|
+ txt = TEMPLATE_FACE3_VN % (va, na, vb, nb, vc, nc)
|
|
|
+
|
|
|
+ elif len(ft) == 3:
|
|
|
+ ta = tmap[ft[0]]
|
|
|
+ tb = tmap[ft[1]]
|
|
|
+ tc = tmap[ft[2]]
|
|
|
+
|
|
|
+ txt = TEMPLATE_FACE3_VT % (va, ta, vb, tb, vc, tc)
|
|
|
+
|
|
|
+ else:
|
|
|
+ txt = TEMPLATE_FACE3_V % (va, vb, vc)
|
|
|
+
|
|
|
+ elif len(fv) == 4:
|
|
|
+
|
|
|
+ va = vmap[fv[0]]
|
|
|
+ vb = vmap[fv[1]]
|
|
|
+ vc = vmap[fv[2]]
|
|
|
+ vd = vmap[fv[3]]
|
|
|
+
|
|
|
+ if len(fn) == 4 and len(ft) == 4:
|
|
|
+ na = nmap[fn[0]]
|
|
|
+ nb = nmap[fn[1]]
|
|
|
+ nc = nmap[fn[2]]
|
|
|
+ nd = nmap[fn[3]]
|
|
|
+
|
|
|
+ ta = tmap[ft[0]]
|
|
|
+ tb = tmap[ft[1]]
|
|
|
+ tc = tmap[ft[2]]
|
|
|
+ td = tmap[ft[3]]
|
|
|
+
|
|
|
+ txt = TEMPLATE_FACE4_VTN % (va, ta, na, vb, tb, nb, vc, tc, nc, vd, td, nd)
|
|
|
+
|
|
|
+ elif len(fn) == 4:
|
|
|
+ na = nmap[fn[0]]
|
|
|
+ nb = nmap[fn[1]]
|
|
|
+ nc = nmap[fn[2]]
|
|
|
+ nd = nmap[fn[3]]
|
|
|
+
|
|
|
+ txt = TEMPLATE_FACE4_VN % (va, na, vb, nb, vc, nc, vd, nd)
|
|
|
+
|
|
|
+ elif len(ft) == 4:
|
|
|
+ ta = tmap[ft[0]]
|
|
|
+ tb = tmap[ft[1]]
|
|
|
+ tc = tmap[ft[2]]
|
|
|
+ td = tmap[ft[3]]
|
|
|
+
|
|
|
+ txt = TEMPLATE_FACE4_VT % (va, ta, vb, tb, vc, tc, vd, td)
|
|
|
+
|
|
|
+ else:
|
|
|
+ txt = TEMPLATE_FACE4_V % (va, vb, vc, vd)
|
|
|
+
|
|
|
+ pieces.append(txt)
|
|
|
+
|
|
|
+
|
|
|
+ str_faces = "\n".join(pieces)
|
|
|
+
|
|
|
+ # generate OBJ string
|
|
|
+
|
|
|
+ content = TEMPLATE_OBJ % {
|
|
|
+ "nfaces" : len(chunk["faces"]),
|
|
|
+ "nvertices" : len(new_vertices),
|
|
|
+ "nnormals" : len(new_normals),
|
|
|
+ "nuvs" : len(new_uvs),
|
|
|
+
|
|
|
+ "vertices" : str_vertices,
|
|
|
+ "normals" : str_normals,
|
|
|
+ "uvs" : str_uvs,
|
|
|
+ "faces" : str_faces
|
|
|
+ }
|
|
|
+
|
|
|
+ # write OBJ file
|
|
|
+
|
|
|
+ outname = "%s_%03d.obj" % (outfile, mi)
|
|
|
+
|
|
|
+ f = open(outname, "w")
|
|
|
+ f.write(content)
|
|
|
+ f.close()
|
|
|
+
|
|
|
+
|
|
|
+# #############################################################################
|
|
|
+# Helpers
|
|
|
+# #############################################################################
|
|
|
+def usage():
|
|
|
+ print "Usage: %s -i filename.obj -o prefix" % os.path.basename(sys.argv[0])
|
|
|
+
|
|
|
+# #####################################################
|
|
|
+# Main
|
|
|
+# #####################################################
|
|
|
+if __name__ == "__main__":
|
|
|
+
|
|
|
+ # get parameters from the command line
|
|
|
+
|
|
|
+ try:
|
|
|
+ opts, args = getopt.getopt(sys.argv[1:], "hi:o:x:", ["help", "input=", "output=", "truncatescale="])
|
|
|
+
|
|
|
+ except getopt.GetoptError:
|
|
|
+ usage()
|
|
|
+ sys.exit(2)
|
|
|
+
|
|
|
+ infile = outfile = ""
|
|
|
+
|
|
|
+ for o, a in opts:
|
|
|
+ if o in ("-h", "--help"):
|
|
|
+ usage()
|
|
|
+ sys.exit()
|
|
|
+
|
|
|
+ elif o in ("-i", "--input"):
|
|
|
+ infile = a
|
|
|
+
|
|
|
+ elif o in ("-o", "--output"):
|
|
|
+ outfile = a
|
|
|
+
|
|
|
+ elif o in ("-x", "--truncatescale"):
|
|
|
+ TRUNCATE = True
|
|
|
+ SCALE = float(a)
|
|
|
+
|
|
|
+ if infile == "" or outfile == "":
|
|
|
+ usage()
|
|
|
+ sys.exit(2)
|
|
|
+
|
|
|
+ print "Splitting [%s] into [%s_XXX.obj] ..." % (infile, outfile)
|
|
|
+
|
|
|
+ break_obj(infile, outfile)
|
|
|
+
|