split_obj.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  1. """Split single OBJ model into mutliple OBJ files by materials
  2. -------------------------------------
  3. How to use
  4. -------------------------------------
  5. python split_obj.py -i infile.obj -o outfile
  6. Will generate:
  7. outfile_000.obj
  8. outfile_001.obj
  9. ...
  10. outfile_XXX.obj
  11. -------------------------------------
  12. Parser based on format description
  13. -------------------------------------
  14. http://en.wikipedia.org/wiki/Obj
  15. ------
  16. Author
  17. ------
  18. AlteredQualia http://alteredqualia.com
  19. """
  20. import fileinput
  21. import operator
  22. import random
  23. import os.path
  24. import getopt
  25. import sys
  26. import struct
  27. import math
  28. import glob
  29. # #####################################################
  30. # Configuration
  31. # #####################################################
  32. TRUNCATE = False
  33. SCALE = 1.0
  34. # #####################################################
  35. # Templates
  36. # #####################################################
  37. TEMPLATE_OBJ = u"""\
  38. ################################
  39. # OBJ generated by split_obj.py
  40. ################################
  41. # Faces: %(nfaces)d
  42. # Vertices: %(nvertices)d
  43. # Normals: %(nnormals)d
  44. # UVs: %(nuvs)d
  45. ################################
  46. # vertices
  47. %(vertices)s
  48. # normals
  49. %(normals)s
  50. # uvs
  51. %(uvs)s
  52. # faces
  53. %(faces)s
  54. """
  55. TEMPLATE_VERTEX = "v %f %f %f"
  56. TEMPLATE_VERTEX_TRUNCATE = "v %d %d %d"
  57. TEMPLATE_NORMAL = "vn %.5g %.5g %.5g"
  58. TEMPLATE_UV = "vt %.5g %.5g"
  59. TEMPLATE_FACE3_V = "f %d %d %d"
  60. TEMPLATE_FACE4_V = "f %d %d %d %d"
  61. TEMPLATE_FACE3_VT = "f %d/%d %d/%d %d/%d"
  62. TEMPLATE_FACE4_VT = "f %d/%d %d/%d %d/%d %d/%d"
  63. TEMPLATE_FACE3_VN = "f %d//%d %d//%d %d//%d"
  64. TEMPLATE_FACE4_VN = "f %d//%d %d//%d %d//%d %d//%d"
  65. TEMPLATE_FACE3_VTN = "f %d/%d/%d %d/%d/%d %d/%d/%d"
  66. TEMPLATE_FACE4_VTN = "f %d/%d/%d %d/%d/%d %d/%d/%d %d/%d/%d"
  67. # #####################################################
  68. # Utils
  69. # #####################################################
  70. def file_exists(filename):
  71. """Return true if file exists and is accessible for reading.
  72. Should be safer than just testing for existence due to links and
  73. permissions magic on Unix filesystems.
  74. @rtype: boolean
  75. """
  76. try:
  77. f = open(filename, 'r')
  78. f.close()
  79. return True
  80. except IOError:
  81. return False
  82. # #####################################################
  83. # OBJ parser
  84. # #####################################################
  85. def parse_vertex(text):
  86. """Parse text chunk specifying single vertex.
  87. Possible formats:
  88. vertex index
  89. vertex index / texture index
  90. vertex index / texture index / normal index
  91. vertex index / / normal index
  92. """
  93. v = 0
  94. t = 0
  95. n = 0
  96. chunks = text.split("/")
  97. v = int(chunks[0])
  98. if len(chunks) > 1:
  99. if chunks[1]:
  100. t = int(chunks[1])
  101. if len(chunks) > 2:
  102. if chunks[2]:
  103. n = int(chunks[2])
  104. return { 'v': v, 't': t, 'n': n }
  105. def parse_obj(fname):
  106. """Parse OBJ file.
  107. """
  108. vertices = []
  109. normals = []
  110. uvs = []
  111. faces = []
  112. materials = {}
  113. mcounter = 0
  114. mcurrent = 0
  115. mtllib = ""
  116. # current face state
  117. group = 0
  118. object = 0
  119. smooth = 0
  120. for line in fileinput.input(fname):
  121. chunks = line.split()
  122. if len(chunks) > 0:
  123. # Vertices as (x,y,z) coordinates
  124. # v 0.123 0.234 0.345
  125. if chunks[0] == "v" and len(chunks) == 4:
  126. x = float(chunks[1])
  127. y = float(chunks[2])
  128. z = float(chunks[3])
  129. vertices.append([x,y,z])
  130. # Normals in (x,y,z) form; normals might not be unit
  131. # vn 0.707 0.000 0.707
  132. if chunks[0] == "vn" and len(chunks) == 4:
  133. x = float(chunks[1])
  134. y = float(chunks[2])
  135. z = float(chunks[3])
  136. normals.append([x,y,z])
  137. # Texture coordinates in (u,v[,w]) coordinates, w is optional
  138. # vt 0.500 -1.352 [0.234]
  139. if chunks[0] == "vt" and len(chunks) >= 3:
  140. u = float(chunks[1])
  141. v = float(chunks[2])
  142. w = 0
  143. if len(chunks)>3:
  144. w = float(chunks[3])
  145. uvs.append([u,v,w])
  146. # Face
  147. if chunks[0] == "f" and len(chunks) >= 4:
  148. vertex_index = []
  149. uv_index = []
  150. normal_index = []
  151. for v in chunks[1:]:
  152. vertex = parse_vertex(v)
  153. if vertex['v']:
  154. vertex_index.append(vertex['v'])
  155. if vertex['t']:
  156. uv_index.append(vertex['t'])
  157. if vertex['n']:
  158. normal_index.append(vertex['n'])
  159. faces.append({
  160. 'vertex':vertex_index,
  161. 'uv':uv_index,
  162. 'normal':normal_index,
  163. 'material':mcurrent,
  164. 'group':group,
  165. 'object':object,
  166. 'smooth':smooth,
  167. })
  168. # Group
  169. if chunks[0] == "g" and len(chunks) == 2:
  170. group = chunks[1]
  171. # Object
  172. if chunks[0] == "o" and len(chunks) == 2:
  173. object = chunks[1]
  174. # Materials definition
  175. if chunks[0] == "mtllib" and len(chunks) == 2:
  176. mtllib = chunks[1]
  177. # Material
  178. if chunks[0] == "usemtl" and len(chunks) == 2:
  179. material = chunks[1]
  180. if not material in materials:
  181. mcurrent = mcounter
  182. materials[material] = mcounter
  183. mcounter += 1
  184. else:
  185. mcurrent = materials[material]
  186. # Smooth shading
  187. if chunks[0] == "s" and len(chunks) == 2:
  188. smooth = chunks[1]
  189. return faces, vertices, uvs, normals, materials, mtllib
  190. # #############################################################################
  191. # API - Breaker
  192. # #############################################################################
  193. def break_obj(infile, outfile):
  194. """Break infile.obj to outfile.obj
  195. """
  196. if not file_exists(infile):
  197. print "Couldn't find [%s]" % infile
  198. return
  199. faces, vertices, uvs, normals, materials, mtllib = parse_obj(infile)
  200. # sort faces by materials
  201. chunks = {}
  202. for face in faces:
  203. material = face["material"]
  204. if not material in chunks:
  205. chunks[material] = {"faces": [], "vertices": set(), "normals": set(), "uvs": set()}
  206. chunks[material]["faces"].append(face)
  207. # extract unique vertex / normal / uv indices used per chunk
  208. for material in chunks:
  209. chunk = chunks[material]
  210. for face in chunk["faces"]:
  211. for i in face["vertex"]:
  212. chunk["vertices"].add(i)
  213. for i in face["normal"]:
  214. chunk["normals"].add(i)
  215. for i in face["uv"]:
  216. chunk["uvs"].add(i)
  217. # generate new OBJs
  218. for mi, material in enumerate(chunks):
  219. chunk = chunks[material]
  220. # generate separate vertex / normal / uv index lists for each chunk
  221. # (including mapping from original to new indices)
  222. # get well defined order
  223. new_vertices = list(chunk["vertices"])
  224. new_normals = list(chunk["normals"])
  225. new_uvs = list(chunk["uvs"])
  226. # map original => new indices
  227. vmap = {}
  228. for i, v in enumerate(new_vertices):
  229. vmap[v] = i + 1
  230. nmap = {}
  231. for i, n in enumerate(new_normals):
  232. nmap[n] = i + 1
  233. tmap = {}
  234. for i, t in enumerate(new_uvs):
  235. tmap[t] = i + 1
  236. # vertices
  237. pieces = []
  238. for i in new_vertices:
  239. vertex = vertices[i-1]
  240. txt = TEMPLATE_VERTEX % (vertex[0], vertex[1], vertex[2])
  241. pieces.append(txt)
  242. str_vertices = "\n".join(pieces)
  243. # normals
  244. pieces = []
  245. for i in new_normals:
  246. normal = normals[i-1]
  247. txt = TEMPLATE_NORMAL % (normal[0], normal[1], normal[2])
  248. pieces.append(txt)
  249. str_normals = "\n".join(pieces)
  250. # uvs
  251. pieces = []
  252. for i in new_uvs:
  253. uv = uvs[i-1]
  254. txt = TEMPLATE_UV % (uv[0], uv[1])
  255. pieces.append(txt)
  256. str_uvs = "\n".join(pieces)
  257. # faces
  258. pieces = []
  259. for face in chunk["faces"]:
  260. txt = ""
  261. fv = face["vertex"]
  262. fn = face["normal"]
  263. ft = face["uv"]
  264. if len(fv) == 3:
  265. va = vmap[fv[0]]
  266. vb = vmap[fv[1]]
  267. vc = vmap[fv[2]]
  268. if len(fn) == 3 and len(ft) == 3:
  269. na = nmap[fn[0]]
  270. nb = nmap[fn[1]]
  271. nc = nmap[fn[2]]
  272. ta = tmap[ft[0]]
  273. tb = tmap[ft[1]]
  274. tc = tmap[ft[2]]
  275. txt = TEMPLATE_FACE3_VTN % (va, ta, na, vb, tb, nb, vc, tc, nc)
  276. elif len(fn) == 3:
  277. na = nmap[fn[0]]
  278. nb = nmap[fn[1]]
  279. nc = nmap[fn[2]]
  280. txt = TEMPLATE_FACE3_VN % (va, na, vb, nb, vc, nc)
  281. elif len(ft) == 3:
  282. ta = tmap[ft[0]]
  283. tb = tmap[ft[1]]
  284. tc = tmap[ft[2]]
  285. txt = TEMPLATE_FACE3_VT % (va, ta, vb, tb, vc, tc)
  286. else:
  287. txt = TEMPLATE_FACE3_V % (va, vb, vc)
  288. elif len(fv) == 4:
  289. va = vmap[fv[0]]
  290. vb = vmap[fv[1]]
  291. vc = vmap[fv[2]]
  292. vd = vmap[fv[3]]
  293. if len(fn) == 4 and len(ft) == 4:
  294. na = nmap[fn[0]]
  295. nb = nmap[fn[1]]
  296. nc = nmap[fn[2]]
  297. nd = nmap[fn[3]]
  298. ta = tmap[ft[0]]
  299. tb = tmap[ft[1]]
  300. tc = tmap[ft[2]]
  301. td = tmap[ft[3]]
  302. txt = TEMPLATE_FACE4_VTN % (va, ta, na, vb, tb, nb, vc, tc, nc, vd, td, nd)
  303. elif len(fn) == 4:
  304. na = nmap[fn[0]]
  305. nb = nmap[fn[1]]
  306. nc = nmap[fn[2]]
  307. nd = nmap[fn[3]]
  308. txt = TEMPLATE_FACE4_VN % (va, na, vb, nb, vc, nc, vd, nd)
  309. elif len(ft) == 4:
  310. ta = tmap[ft[0]]
  311. tb = tmap[ft[1]]
  312. tc = tmap[ft[2]]
  313. td = tmap[ft[3]]
  314. txt = TEMPLATE_FACE4_VT % (va, ta, vb, tb, vc, tc, vd, td)
  315. else:
  316. txt = TEMPLATE_FACE4_V % (va, vb, vc, vd)
  317. pieces.append(txt)
  318. str_faces = "\n".join(pieces)
  319. # generate OBJ string
  320. content = TEMPLATE_OBJ % {
  321. "nfaces" : len(chunk["faces"]),
  322. "nvertices" : len(new_vertices),
  323. "nnormals" : len(new_normals),
  324. "nuvs" : len(new_uvs),
  325. "vertices" : str_vertices,
  326. "normals" : str_normals,
  327. "uvs" : str_uvs,
  328. "faces" : str_faces
  329. }
  330. # write OBJ file
  331. outname = "%s_%03d.obj" % (outfile, mi)
  332. f = open(outname, "w")
  333. f.write(content)
  334. f.close()
  335. # #############################################################################
  336. # Helpers
  337. # #############################################################################
  338. def usage():
  339. print "Usage: %s -i filename.obj -o prefix" % os.path.basename(sys.argv[0])
  340. # #####################################################
  341. # Main
  342. # #####################################################
  343. if __name__ == "__main__":
  344. # get parameters from the command line
  345. try:
  346. opts, args = getopt.getopt(sys.argv[1:], "hi:o:x:", ["help", "input=", "output=", "truncatescale="])
  347. except getopt.GetoptError:
  348. usage()
  349. sys.exit(2)
  350. infile = outfile = ""
  351. for o, a in opts:
  352. if o in ("-h", "--help"):
  353. usage()
  354. sys.exit()
  355. elif o in ("-i", "--input"):
  356. infile = a
  357. elif o in ("-o", "--output"):
  358. outfile = a
  359. elif o in ("-x", "--truncatescale"):
  360. TRUNCATE = True
  361. SCALE = float(a)
  362. if infile == "" or outfile == "":
  363. usage()
  364. sys.exit(2)
  365. print "Splitting [%s] into [%s_XXX.obj] ..." % (infile, outfile)
  366. break_obj(infile, outfile)