convert_obj_threejs_slim.py 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195
  1. """Convert Wavefront OBJ / MTL files into Three.js (slim models version, to be used with web worker based ascii / binary loader)
  2. -------------------------
  3. How to use this converter
  4. -------------------------
  5. python convert_obj_threejs_slim.py -i infile.obj -o outfile.js [-a center|top|bottom] [-s smooth|flat] [-t ascii|binary]
  6. Notes:
  7. - by default, model is centered (middle of bounding box goes to 0,0,0),
  8. uses smooth shading (if there were vertex normals in the original
  9. model) and is in ASCII format.
  10. - binary conversion will create two files:
  11. outfile.js (materials)
  12. outfile.bin (binary buffers)
  13. --------------------------------------------------
  14. How to use generated JS file in your HTML document
  15. --------------------------------------------------
  16. <script type="text/javascript" src="Three.js"></script>
  17. ...
  18. <script type="text/javascript">
  19. ...
  20. var loader = new THREE.Loader();
  21. // load ascii model
  22. loader.loadAscii( "Model_slim.js", function( geometry ) { createScene( geometry) }, path_to_textures );
  23. // load binary model
  24. loader.loadBinary( "Model_bin.js", function( geometry ) { createScene( geometry) }, path_to_textures );
  25. function createScene( geometry ) {
  26. var normalizeUVsFlag = 1; // set to 1 if canvas render has missing materials
  27. var mesh = new THREE.Mesh( geometry, new THREE.MeshFaceMaterial(), normalizeUVsFlag );
  28. }
  29. ...
  30. </script>
  31. -------------------------------------
  32. Parsers based on formats descriptions
  33. -------------------------------------
  34. http://en.wikipedia.org/wiki/Obj
  35. http://en.wikipedia.org/wiki/Material_Template_Library
  36. -------------------
  37. Current limitations
  38. -------------------
  39. - for the moment, only diffuse color and texture are used
  40. (will need to extend shaders / renderers / materials in Three)
  41. - models can have more than 65,536 vertices,
  42. but in most cases it will not work well with browsers,
  43. which currently seem to have troubles with handling
  44. large JS files
  45. - texture coordinates can be wrong in canvas renderer
  46. (there is crude normalization, but it doesn't
  47. work for all cases)
  48. - smoothing can be turned on/off only for the whole mesh
  49. ----------------------------------------------
  50. How to get proper OBJ + MTL files with Blender
  51. ----------------------------------------------
  52. 0. Remove default cube (press DEL and ENTER)
  53. 1. Import / create model
  54. 2. Select all meshes (Select -> Select All by Type -> Mesh)
  55. 3. Export to OBJ (File -> Export -> Wavefront .obj) [*]
  56. - enable following options in exporter
  57. Material Groups
  58. Rotate X90
  59. Apply Modifiers
  60. High Quality Normals
  61. Copy Images
  62. Selection Only
  63. Objects as OBJ Objects
  64. UVs
  65. Normals
  66. Materials
  67. Edges
  68. - select empty folder
  69. - give your exported file name with "obj" extension
  70. - click on "Export OBJ" button
  71. 4. Your model is now all files in this folder (OBJ, MTL, number of images)
  72. - this converter assumes all files staying in the same folder,
  73. (OBJ / MTL files use relative paths)
  74. - for WebGL, textures must be power of 2 sized
  75. [*] If OBJ export fails (Blender 2.54 beta), patch your Blender installation
  76. following instructions here:
  77. http://www.blendernation.com/2010/09/12/blender-2-54-beta-released/
  78. ------
  79. Author
  80. ------
  81. AlteredQualia http://alteredqualia.com
  82. """
  83. import fileinput
  84. import operator
  85. import random
  86. import os.path
  87. import getopt
  88. import sys
  89. import struct
  90. import math
  91. # #####################################################
  92. # Configuration
  93. # #####################################################
  94. ALIGN = "center" # center bottom top none
  95. SHADING = "smooth" # smooth flat
  96. TYPE = "ascii" # ascii binary
  97. # default colors for debugging (each material gets one distinct color):
  98. # white, red, green, blue, yellow, cyan, magenta
  99. COLORS = [0xeeeeee, 0xee0000, 0x00ee00, 0x0000ee, 0xeeee00, 0x00eeee, 0xee00ee]
  100. # #####################################################
  101. # Templates
  102. # #####################################################
  103. TEMPLATE_FILE_ASCII = u"""\
  104. // Converted from: %(fname)s
  105. // vertices: %(nvertex)d
  106. // faces: %(nface)d
  107. // materials: %(nmaterial)d
  108. //
  109. // Generated with OBJ -> Three.js converter
  110. // http://github.com/alteredq/three.js/blob/master/utils/exporters/convert_obj_threejs_slim.py
  111. var model = {
  112. 'materials': [%(materials)s],
  113. 'normals': [%(normals)s],
  114. 'vertices': [%(vertices)s],
  115. 'uvs': [%(uvs)s],
  116. 'triangles': [%(triangles)s],
  117. 'triangles_n': [%(triangles_n)s],
  118. 'triangles_uv': [%(triangles_uv)s],
  119. 'triangles_n_uv': [%(triangles_n_uv)s],
  120. 'quads': [%(quads)s],
  121. 'quads_n': [%(quads_n)s],
  122. 'quads_uv': [%(quads_uv)s],
  123. 'quads_n_uv': [%(quads_n_uv)s],
  124. 'end': (new Date).getTime()
  125. }
  126. postMessage( model );
  127. """
  128. TEMPLATE_FILE_BIN = u"""\
  129. // Converted from: %(fname)s
  130. // vertices: %(nvertex)d
  131. // faces: %(nface)d
  132. // materials: %(nmaterial)d
  133. //
  134. // Generated with OBJ -> Three.js converter
  135. // http://github.com/alteredq/three.js/blob/master/utils/exporters/convert_obj_threejs_slim.py
  136. var model = {
  137. 'materials': [%(materials)s],
  138. 'buffers': '%(buffers)s',
  139. 'end': (new Date).getTime()
  140. }
  141. postMessage( model );
  142. """
  143. TEMPLATE_VERTEX = "%f,%f,%f"
  144. TEMPLATE_UV_TRI = "%f,%f,%f,%f,%f,%f"
  145. TEMPLATE_UV_QUAD = "%f,%f,%f,%f,%f,%f,%f,%f"
  146. TEMPLATE_TRI = "%d,%d,%d,%d"
  147. TEMPLATE_QUAD = "%d,%d,%d,%d,%d"
  148. TEMPLATE_TRI_UV = "%d,%d,%d,%d,%d,%d,%d"
  149. TEMPLATE_QUAD_UV = "%d,%d,%d,%d,%d,%d,%d,%d,%d"
  150. TEMPLATE_TRI_N = "%d,%d,%d,%d,%d,%d,%d"
  151. TEMPLATE_QUAD_N = "%d,%d,%d,%d,%d,%d,%d,%d,%d"
  152. TEMPLATE_TRI_N_UV = "%d,%d,%d,%d,%d,%d,%d,%d,%d,%d"
  153. TEMPLATE_QUAD_N_UV = "%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d"
  154. TEMPLATE_N = "%f,%f,%f"
  155. TEMPLATE_UV = "%f,%f"
  156. # #####################################################
  157. # Utils
  158. # #####################################################
  159. def file_exists(filename):
  160. """Return true if file exists and is accessible for reading.
  161. Should be safer than just testing for existence due to links and
  162. permissions magic on Unix filesystems.
  163. @rtype: boolean
  164. """
  165. try:
  166. f = open(filename, 'r')
  167. f.close()
  168. return True
  169. except IOError:
  170. return False
  171. def get_name(fname):
  172. """Create model name based of filename ("path/fname.js" -> "fname").
  173. """
  174. return os.path.basename(fname).split(".")[0]
  175. def bbox(vertices):
  176. """Compute bounding box of vertex array.
  177. """
  178. if len(vertices)>0:
  179. minx = maxx = vertices[0][0]
  180. miny = maxy = vertices[0][1]
  181. minz = maxz = vertices[0][2]
  182. for v in vertices[1:]:
  183. if v[0]<minx:
  184. minx = v[0]
  185. elif v[0]>maxx:
  186. maxx = v[0]
  187. if v[1]<miny:
  188. miny = v[1]
  189. elif v[1]>maxy:
  190. maxy = v[1]
  191. if v[2]<minz:
  192. minz = v[2]
  193. elif v[2]>maxz:
  194. maxz = v[2]
  195. return { 'x':[minx,maxx], 'y':[miny,maxy], 'z':[minz,maxz] }
  196. else:
  197. return { 'x':[0,0], 'y':[0,0], 'z':[0,0] }
  198. def translate(vertices, t):
  199. """Translate array of vertices by vector t.
  200. """
  201. for i in xrange(len(vertices)):
  202. vertices[i][0] += t[0]
  203. vertices[i][1] += t[1]
  204. vertices[i][2] += t[2]
  205. def center(vertices):
  206. """Center model (middle of bounding box).
  207. """
  208. bb = bbox(vertices)
  209. cx = bb['x'][0] + (bb['x'][1] - bb['x'][0])/2.0
  210. cy = bb['y'][0] + (bb['y'][1] - bb['y'][0])/2.0
  211. cz = bb['z'][0] + (bb['z'][1] - bb['z'][0])/2.0
  212. translate(vertices, [-cx,-cy,-cz])
  213. def top(vertices):
  214. """Align top of the model with the floor (Y-axis) and center it around X and Z.
  215. """
  216. bb = bbox(vertices)
  217. cx = bb['x'][0] + (bb['x'][1] - bb['x'][0])/2.0
  218. cy = bb['y'][1]
  219. cz = bb['z'][0] + (bb['z'][1] - bb['z'][0])/2.0
  220. translate(vertices, [-cx,-cy,-cz])
  221. def bottom(vertices):
  222. """Align bottom of the model with the floor (Y-axis) and center it around X and Z.
  223. """
  224. bb = bbox(vertices)
  225. cx = bb['x'][0] + (bb['x'][1] - bb['x'][0])/2.0
  226. cy = bb['y'][0]
  227. cz = bb['z'][0] + (bb['z'][1] - bb['z'][0])/2.0
  228. translate(vertices, [-cx,-cy,-cz])
  229. def normalize(v):
  230. """Normalize 3d vector"""
  231. l = math.sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2])
  232. v[0] /= l
  233. v[1] /= l
  234. v[2] /= l
  235. # #####################################################
  236. # MTL parser
  237. # #####################################################
  238. def parse_mtl(fname):
  239. """Parse MTL file.
  240. """
  241. materials = {}
  242. for line in fileinput.input(fname):
  243. chunks = line.split()
  244. if len(chunks) > 0:
  245. # Material start
  246. # newmtl identifier
  247. if chunks[0] == "newmtl" and len(chunks) == 2:
  248. identifier = chunks[1]
  249. if not identifier in materials:
  250. materials[identifier] = {}
  251. # Diffuse color
  252. # Kd 1.000 1.000 1.000
  253. if chunks[0] == "Kd" and len(chunks) == 4:
  254. materials[identifier]["col_diffuse"] = [float(chunks[1]), float(chunks[2]), float(chunks[3])]
  255. # Ambient color
  256. # Ka 1.000 1.000 1.000
  257. if chunks[0] == "Ka" and len(chunks) == 4:
  258. materials[identifier]["col_ambient"] = [float(chunks[1]), float(chunks[2]), float(chunks[3])]
  259. # Specular color
  260. # Ks 1.000 1.000 1.000
  261. if chunks[0] == "Ks" and len(chunks) == 4:
  262. materials[identifier]["col_specular"] = [float(chunks[1]), float(chunks[2]), float(chunks[3])]
  263. # Specular coefficient
  264. # Ns 154.000
  265. if chunks[0] == "Ns" and len(chunks) == 2:
  266. materials[identifier]["specular_coef"] = float(chunks[1])
  267. # Transparency
  268. # Tr 0.9 or d 0.9
  269. if (chunks[0] == "Tr" or chunks[0] == "d") and len(chunks) == 2:
  270. materials[identifier]["transparency"] = float(chunks[1])
  271. # Optical density
  272. # Ni 1.0
  273. if chunks[0] == "Ni" and len(chunks) == 2:
  274. materials[identifier]["optical_density"] = float(chunks[1])
  275. # Diffuse texture
  276. # map_Kd texture_diffuse.jpg
  277. if chunks[0] == "map_Kd" and len(chunks) == 2:
  278. materials[identifier]["map_diffuse"] = chunks[1]
  279. # Ambient texture
  280. # map_Ka texture_ambient.jpg
  281. if chunks[0] == "map_Ka" and len(chunks) == 2:
  282. materials[identifier]["map_ambient"] = chunks[1]
  283. # Specular texture
  284. # map_Ks texture_specular.jpg
  285. if chunks[0] == "map_Ks" and len(chunks) == 2:
  286. materials[identifier]["map_specular"] = chunks[1]
  287. # Alpha texture
  288. # map_d texture_alpha.png
  289. if chunks[0] == "map_d" and len(chunks) == 2:
  290. materials[identifier]["map_alpha"] = chunks[1]
  291. # Bump texture
  292. # map_bump texture_bump.jpg or bump texture_bump.jpg
  293. if (chunks[0] == "map_bump" or chunks[0] == "bump") and len(chunks) == 2:
  294. materials[identifier]["map_bump"] = chunks[1]
  295. # Illumination
  296. # illum 2
  297. #
  298. # 0. Color on and Ambient off
  299. # 1. Color on and Ambient on
  300. # 2. Highlight on
  301. # 3. Reflection on and Ray trace on
  302. # 4. Transparency: Glass on, Reflection: Ray trace on
  303. # 5. Reflection: Fresnel on and Ray trace on
  304. # 6. Transparency: Refraction on, Reflection: Fresnel off and Ray trace on
  305. # 7. Transparency: Refraction on, Reflection: Fresnel on and Ray trace on
  306. # 8. Reflection on and Ray trace off
  307. # 9. Transparency: Glass on, Reflection: Ray trace off
  308. # 10. Casts shadows onto invisible surfaces
  309. if chunks[0] == "illum" and len(chunks) == 2:
  310. materials[identifier]["illumination"] = int(chunks[1])
  311. return materials
  312. # #####################################################
  313. # OBJ parser
  314. # #####################################################
  315. def parse_vertex(text):
  316. """Parse text chunk specifying single vertex.
  317. Possible formats:
  318. vertex index
  319. vertex index / texture index
  320. vertex index / texture index / normal index
  321. vertex index / / normal index
  322. """
  323. v = 0
  324. t = 0
  325. n = 0
  326. chunks = text.split("/")
  327. v = int(chunks[0])
  328. if len(chunks) > 1:
  329. if chunks[1]:
  330. t = int(chunks[1])
  331. if len(chunks) > 2:
  332. if chunks[2]:
  333. n = int(chunks[2])
  334. return { 'v':v, 't':t, 'n':n }
  335. def parse_obj(fname):
  336. """Parse OBJ file.
  337. """
  338. vertices = []
  339. normals = []
  340. uvs = []
  341. faces = []
  342. materials = {}
  343. mcounter = 0
  344. mcurrent = 0
  345. mtllib = ""
  346. # current face state
  347. group = 0
  348. object = 0
  349. smooth = 0
  350. for line in fileinput.input(fname):
  351. chunks = line.split()
  352. if len(chunks) > 0:
  353. # Vertices as (x,y,z) coordinates
  354. # v 0.123 0.234 0.345
  355. if chunks[0] == "v" and len(chunks) == 4:
  356. x = float(chunks[1])
  357. y = float(chunks[2])
  358. z = float(chunks[3])
  359. vertices.append([x,y,z])
  360. # Normals in (x,y,z) form; normals might not be unit
  361. # vn 0.707 0.000 0.707
  362. if chunks[0] == "vn" and len(chunks) == 4:
  363. x = float(chunks[1])
  364. y = float(chunks[2])
  365. z = float(chunks[3])
  366. normals.append([x,y,z])
  367. # Texture coordinates in (u,v[,w]) coordinates, w is optional
  368. # vt 0.500 -1.352 [0.234]
  369. if chunks[0] == "vt" and len(chunks) >= 3:
  370. u = float(chunks[1])
  371. v = float(chunks[2])
  372. w = 0
  373. if len(chunks)>3:
  374. w = float(chunks[3])
  375. uvs.append([u,v,w])
  376. # Face
  377. if chunks[0] == "f" and len(chunks) >= 4:
  378. vertex_index = []
  379. uv_index = []
  380. normal_index = []
  381. for v in chunks[1:]:
  382. vertex = parse_vertex(v)
  383. if vertex['v']:
  384. vertex_index.append(vertex['v'])
  385. if vertex['t']:
  386. uv_index.append(vertex['t'])
  387. if vertex['n']:
  388. normal_index.append(vertex['n'])
  389. faces.append({
  390. 'vertex':vertex_index,
  391. 'uv':uv_index,
  392. 'normal':normal_index,
  393. 'material':mcurrent,
  394. 'group':group,
  395. 'object':object,
  396. 'smooth':smooth,
  397. })
  398. # Group
  399. if chunks[0] == "g" and len(chunks) == 2:
  400. group = chunks[1]
  401. # Object
  402. if chunks[0] == "o" and len(chunks) == 2:
  403. object = chunks[1]
  404. # Materials definition
  405. if chunks[0] == "mtllib" and len(chunks) == 2:
  406. mtllib = chunks[1]
  407. # Material
  408. if chunks[0] == "usemtl" and len(chunks) == 2:
  409. material = chunks[1]
  410. if not material in materials:
  411. mcurrent = mcounter
  412. materials[material] = mcounter
  413. mcounter += 1
  414. else:
  415. mcurrent = materials[material]
  416. # Smooth shading
  417. if chunks[0] == "s" and len(chunks) == 2:
  418. smooth = chunks[1]
  419. return faces, vertices, uvs, normals, materials, mtllib
  420. # #####################################################
  421. # Generator
  422. # #####################################################
  423. def generate_vertex(v):
  424. return TEMPLATE_VERTEX % (v[0], v[1], v[2])
  425. def generate_triangle(f):
  426. v = f['vertex']
  427. return TEMPLATE_TRI % (v[0]-1, v[1]-1, v[2]-1,
  428. f['material'])
  429. def generate_triangle_uv(f):
  430. v = f['vertex']
  431. uv = f['uv']
  432. return TEMPLATE_TRI_UV % (v[0]-1, v[1]-1, v[2]-1,
  433. f['material'],
  434. uv[0]-1, uv[1]-1, uv[2]-1)
  435. def generate_triangle_n(f):
  436. v = f['vertex']
  437. n = f['normal']
  438. return TEMPLATE_TRI_N % (v[0]-1, v[1]-1, v[2]-1,
  439. f['material'],
  440. n[0]-1, n[1]-1, n[2]-1)
  441. def generate_triangle_n_uv(f):
  442. v = f['vertex']
  443. n = f['normal']
  444. uv = f['uv']
  445. return TEMPLATE_TRI_N_UV % (v[0]-1, v[1]-1, v[2]-1,
  446. f['material'],
  447. n[0]-1, n[1]-1, n[2]-1,
  448. uv[0]-1, uv[1]-1, uv[2]-1)
  449. def generate_quad(f):
  450. vi = f['vertex']
  451. return TEMPLATE_QUAD % (vi[0]-1, vi[1]-1, vi[2]-1, vi[3]-1,
  452. f['material'])
  453. def generate_quad_uv(f):
  454. v = f['vertex']
  455. uv = f['uv']
  456. return TEMPLATE_QUAD_UV % (v[0]-1, v[1]-1, v[2]-1, v[3]-1,
  457. f['material'],
  458. uv[0]-1, uv[1]-1, uv[2]-1, uv[3]-1)
  459. def generate_quad_n(f):
  460. v = f['vertex']
  461. n = f['normal']
  462. return TEMPLATE_QUAD_N % (v[0]-1, v[1]-1, v[2]-1, v[3]-1,
  463. f['material'],
  464. n[0]-1, n[1]-1, n[2]-1, n[3]-1)
  465. def generate_quad_n_uv(f):
  466. v = f['vertex']
  467. n = f['normal']
  468. uv = f['uv']
  469. return TEMPLATE_QUAD_N_UV % (v[0]-1, v[1]-1, v[2]-1, v[3]-1,
  470. f['material'],
  471. n[0]-1, n[1]-1, n[2]-1, n[3]-1,
  472. uv[0]-1, uv[1]-1, uv[2]-1, uv[3]-1)
  473. def generate_normal(n):
  474. return TEMPLATE_N % (n[0], n[1], n[2])
  475. def generate_uv(uv):
  476. return TEMPLATE_UV % (uv[0], 1.0 - uv[1])
  477. # #####################################################
  478. # Materials
  479. # #####################################################
  480. def generate_color(i):
  481. """Generate hex color corresponding to integer.
  482. Colors should have well defined ordering.
  483. First N colors are hardcoded, then colors are random
  484. (must seed random number generator with deterministic value
  485. before getting colors).
  486. """
  487. if i < len(COLORS):
  488. return "0x%06x" % COLORS[i]
  489. else:
  490. return "0x%06x" % int(0xffffff * random.random())
  491. def value2string(v):
  492. if type(v)==str and v[0] != "0":
  493. return '"%s"' % v
  494. return str(v)
  495. def generate_materials(mtl, materials):
  496. """Generate JS array of materials objects
  497. JS material objects are basically prettified one-to-one
  498. mappings of MTL properties in JSON format.
  499. """
  500. mtl_array = []
  501. for m in mtl:
  502. index = materials[m]
  503. # add debug information
  504. # materials should be sorted according to how
  505. # they appeared in OBJ file (for the first time)
  506. # this index is identifier used in face definitions
  507. mtl[m]['a_dbg_name'] = m
  508. mtl[m]['a_dbg_index'] = index
  509. mtl[m]['a_dbg_color'] = generate_color(index)
  510. mtl_raw = ",\n".join(['\t"%s" : %s' % (n, value2string(v)) for n,v in sorted(mtl[m].items())])
  511. mtl_string = "\t{\n%s\n\t}" % mtl_raw
  512. mtl_array.append([index, mtl_string])
  513. return ",\n\n".join([m for i,m in sorted(mtl_array)])
  514. def generate_mtl(materials):
  515. """Generate dummy materials (if there is no MTL file).
  516. """
  517. mtl = {}
  518. for m in materials:
  519. index = materials[m]
  520. mtl[m] = {
  521. 'a_dbg_name': m,
  522. 'a_dbg_index': index,
  523. 'a_dbg_color': generate_color(index)
  524. }
  525. return mtl
  526. def generate_materials_string(materials, mtllib):
  527. """Generate final materials string.
  528. """
  529. random.seed(42) # to get well defined color order for materials
  530. # default materials with debug colors for when
  531. # there is no specified MTL / MTL loading failed,
  532. # or if there were no materials / null materials
  533. if not materials:
  534. materials = { 'default':0 }
  535. mtl = generate_mtl(materials)
  536. if mtllib:
  537. # create full pathname for MTL (included from OBJ)
  538. path = os.path.dirname(infile)
  539. fname = os.path.join(path, mtllib)
  540. if file_exists(fname):
  541. # override default materials with real ones from MTL
  542. # (where they exist, otherwise keep defaults)
  543. mtl.update(parse_mtl(fname))
  544. else:
  545. print "Couldn't find [%s]" % fname
  546. return generate_materials(mtl, materials)
  547. # #####################################################
  548. # Faces
  549. # #####################################################
  550. def is_triangle_flat(f):
  551. return len(f['vertex'])==3 and not (f["normal"] and SHADING == "smooth") and not f['uv']
  552. def is_triangle_flat_uv(f):
  553. return len(f['vertex'])==3 and not (f["normal"] and SHADING == "smooth") and len(f['uv'])==3
  554. def is_triangle_smooth(f):
  555. return len(f['vertex'])==3 and f["normal"] and SHADING == "smooth" and not f['uv']
  556. def is_triangle_smooth_uv(f):
  557. return len(f['vertex'])==3 and f["normal"] and SHADING == "smooth" and len(f['uv'])==3
  558. def is_quad_flat(f):
  559. return len(f['vertex'])==4 and not (f["normal"] and SHADING == "smooth") and not f['uv']
  560. def is_quad_flat_uv(f):
  561. return len(f['vertex'])==4 and not (f["normal"] and SHADING == "smooth") and len(f['uv'])==4
  562. def is_quad_smooth(f):
  563. return len(f['vertex'])==4 and f["normal"] and SHADING == "smooth" and not f['uv']
  564. def is_quad_smooth_uv(f):
  565. return len(f['vertex'])==4 and f["normal"] and SHADING == "smooth" and len(f['uv'])==4
  566. def sort_faces(faces):
  567. data = {
  568. 'triangles_flat': [],
  569. 'triangles_flat_uv': [],
  570. 'triangles_smooth': [],
  571. 'triangles_smooth_uv': [],
  572. 'quads_flat': [],
  573. 'quads_flat_uv': [],
  574. 'quads_smooth': [],
  575. 'quads_smooth_uv': []
  576. }
  577. for f in faces:
  578. if is_triangle_flat(f):
  579. data['triangles_flat'].append(f)
  580. elif is_triangle_flat_uv(f):
  581. data['triangles_flat_uv'].append(f)
  582. elif is_triangle_smooth(f):
  583. data['triangles_smooth'].append(f)
  584. elif is_triangle_smooth_uv(f):
  585. data['triangles_smooth_uv'].append(f)
  586. elif is_quad_flat(f):
  587. data['quads_flat'].append(f)
  588. elif is_quad_flat_uv(f):
  589. data['quads_flat_uv'].append(f)
  590. elif is_quad_smooth(f):
  591. data['quads_smooth'].append(f)
  592. elif is_quad_smooth_uv(f):
  593. data['quads_smooth_uv'].append(f)
  594. return data
  595. # #####################################################
  596. # API - ASCII converter
  597. # #####################################################
  598. def convert_ascii(infile, outfile):
  599. """Convert infile.obj to outfile.js
  600. Here is where everything happens. If you need to automate conversions,
  601. just import this file as Python module and call this method.
  602. """
  603. if not file_exists(infile):
  604. print "Couldn't find [%s]" % infile
  605. return
  606. faces, vertices, uvs, normals, materials, mtllib = parse_obj(infile)
  607. if ALIGN == "center":
  608. center(vertices)
  609. elif ALIGN == "bottom":
  610. bottom(vertices)
  611. elif ALIGN == "top":
  612. top(vertices)
  613. normals_string = ""
  614. if SHADING == "smooth":
  615. normals_string = ",".join(generate_normal(n) for n in normals)
  616. sfaces = sort_faces(faces)
  617. text = TEMPLATE_FILE_ASCII % {
  618. "name" : get_name(outfile),
  619. "vertices" : ",".join(generate_vertex(v) for v in vertices),
  620. "triangles" : ",".join(generate_triangle(f) for f in sfaces['triangles_flat']),
  621. "triangles_n" : ",".join(generate_triangle_n(f) for f in sfaces['triangles_smooth']),
  622. "triangles_uv" : ",".join(generate_triangle_uv(f) for f in sfaces['triangles_flat_uv']),
  623. "triangles_n_uv": ",".join(generate_triangle_n_uv(f) for f in sfaces['triangles_smooth_uv']),
  624. "quads" : ",".join(generate_quad(f) for f in sfaces['quads_flat']),
  625. "quads_n" : ",".join(generate_quad_n(f) for f in sfaces['quads_smooth']),
  626. "quads_uv" : ",".join(generate_quad_uv(f) for f in sfaces['quads_flat_uv']),
  627. "quads_n_uv" : ",".join(generate_quad_n_uv(f) for f in sfaces['quads_smooth_uv']),
  628. "uvs" : ",".join(generate_uv(uv) for uv in uvs),
  629. "normals" : normals_string,
  630. "materials" : generate_materials_string(materials, mtllib),
  631. "fname" : infile,
  632. "nvertex" : len(vertices),
  633. "nface" : len(faces),
  634. "nmaterial" : len(materials)
  635. }
  636. out = open(outfile, "w")
  637. out.write(text)
  638. out.close()
  639. print "%d vertices, %d faces, %d materials" % (len(vertices), len(faces), len(materials))
  640. # #############################################################################
  641. # API - Binary converter
  642. # #############################################################################
  643. def convert_binary(infile, outfile):
  644. """Convert infile.obj to outfile.js + outfile.bin
  645. """
  646. if not file_exists(infile):
  647. print "Couldn't find [%s]" % infile
  648. return
  649. binfile = get_name(outfile) + ".bin"
  650. faces, vertices, uvs, normals, materials, mtllib = parse_obj(infile)
  651. if ALIGN == "center":
  652. center(vertices)
  653. elif ALIGN == "bottom":
  654. bottom(vertices)
  655. elif ALIGN == "top":
  656. top(vertices)
  657. sfaces = sort_faces(faces)
  658. # ###################
  659. # generate JS file
  660. # ###################
  661. text = TEMPLATE_FILE_BIN % {
  662. "name" : get_name(outfile),
  663. "materials" : generate_materials_string(materials, mtllib),
  664. "buffers" : binfile,
  665. "fname" : infile,
  666. "nvertex" : len(vertices),
  667. "nface" : len(faces),
  668. "nmaterial" : len(materials)
  669. }
  670. out = open(outfile, "w")
  671. out.write(text)
  672. out.close()
  673. # ###################
  674. # generate BIN file
  675. # ###################
  676. if SHADING == "smooth":
  677. nnormals = len(normals)
  678. else:
  679. nnormals = 0
  680. buffer = []
  681. # header
  682. # ------
  683. header_bytes = struct.calcsize('<8s')
  684. header_bytes += struct.calcsize('<BBBBBBBB')
  685. header_bytes += struct.calcsize('<IIIIIIIIIII')
  686. # signature
  687. signature = struct.pack('<8s', 'Three.js')
  688. # metadata (all data is little-endian)
  689. vertex_coordinate_bytes = 4
  690. normal_coordinate_bytes = 1
  691. uv_coordinate_bytes = 4
  692. vertex_index_bytes = 4
  693. normal_index_bytes = 4
  694. uv_index_bytes = 4
  695. material_index_bytes = 2
  696. # header_bytes unsigned char 1
  697. # vertex_coordinate_bytes unsigned char 1
  698. # normal_coordinate_bytes unsigned char 1
  699. # uv_coordinate_bytes unsigned char 1
  700. # vertex_index_bytes unsigned char 1
  701. # normal_index_bytes unsigned char 1
  702. # uv_index_bytes unsigned char 1
  703. # material_index_bytes unsigned char 1
  704. bdata = struct.pack('<BBBBBBBB', header_bytes,
  705. vertex_coordinate_bytes,
  706. normal_coordinate_bytes,
  707. uv_coordinate_bytes,
  708. vertex_index_bytes,
  709. normal_index_bytes,
  710. uv_index_bytes,
  711. material_index_bytes)
  712. # nvertices unsigned int 4
  713. # nnormals unsigned int 4
  714. # nuvs unsigned int 4
  715. # ntri_flat unsigned int 4
  716. # ntri_smooth unsigned int 4
  717. # ntri_flat_uv unsigned int 4
  718. # ntri_smooth_uv unsigned int 4
  719. # nquad_flat unsigned int 4
  720. # nquad_smooth unsigned int 4
  721. # nquad_flat_uv unsigned int 4
  722. # nquad_smooth_uv unsigned int 4
  723. ndata = struct.pack('<IIIIIIIIIII', len(vertices),
  724. nnormals,
  725. len(uvs),
  726. len(sfaces['triangles_flat']),
  727. len(sfaces['triangles_smooth']),
  728. len(sfaces['triangles_flat_uv']),
  729. len(sfaces['triangles_smooth_uv']),
  730. len(sfaces['quads_flat']),
  731. len(sfaces['quads_smooth']),
  732. len(sfaces['quads_flat_uv']),
  733. len(sfaces['quads_smooth_uv']))
  734. buffer.append(signature)
  735. buffer.append(bdata)
  736. buffer.append(ndata)
  737. # 1. vertices
  738. # ------------
  739. # x float 4
  740. # y float 4
  741. # z float 4
  742. for v in vertices:
  743. data = struct.pack('<fff', v[0], v[1], v[2])
  744. buffer.append(data)
  745. # 2. normals
  746. # ---------------
  747. # x signed char 1
  748. # y signed char 1
  749. # z signed char 1
  750. if SHADING == "smooth":
  751. for n in normals:
  752. normalize(n)
  753. data = struct.pack('<bbb', math.floor(n[0]*127+0.5),
  754. math.floor(n[1]*127+0.5),
  755. math.floor(n[2]*127+0.5))
  756. buffer.append(data)
  757. # 3. uvs
  758. # -----------
  759. # u float 4
  760. # v float 4
  761. for uv in uvs:
  762. data = struct.pack('<ff', uv[0], 1.0-uv[1])
  763. buffer.append(data)
  764. # 4. flat triangles
  765. # ------------------
  766. # a unsigned int 4
  767. # b unsigned int 4
  768. # c unsigned int 4
  769. # m unsigned short 2
  770. for f in sfaces['triangles_flat']:
  771. vi = f['vertex']
  772. data = struct.pack('<IIIH',
  773. vi[0]-1, vi[1]-1, vi[2]-1,
  774. f['material'])
  775. buffer.append(data)
  776. # 5. smooth triangles
  777. # -------------------
  778. # a unsigned int 4
  779. # b unsigned int 4
  780. # c unsigned int 4
  781. # m unsigned short 2
  782. # na unsigned int 4
  783. # nb unsigned int 4
  784. # nc unsigned int 4
  785. for f in sfaces['triangles_smooth']:
  786. vi = f['vertex']
  787. ni = f['normal']
  788. data = struct.pack('<IIIHIII',
  789. vi[0]-1, vi[1]-1, vi[2]-1,
  790. f['material'],
  791. ni[0]-1, ni[1]-1, ni[2]-1)
  792. buffer.append(data)
  793. # 6. flat triangles uv
  794. # --------------------
  795. # a unsigned int 4
  796. # b unsigned int 4
  797. # c unsigned int 4
  798. # m unsigned short 2
  799. # ua unsigned int 4
  800. # ub unsigned int 4
  801. # uc unsigned int 4
  802. for f in sfaces['triangles_flat_uv']:
  803. vi = f['vertex']
  804. ui = f['uv']
  805. data = struct.pack('<IIIHIII',
  806. vi[0]-1, vi[1]-1, vi[2]-1,
  807. f['material'],
  808. ui[0]-1, ui[1]-1, ui[2]-1)
  809. buffer.append(data)
  810. # 7. smooth triangles uv
  811. # ----------------------
  812. # a unsigned int 4
  813. # b unsigned int 4
  814. # c unsigned int 4
  815. # m unsigned short 2
  816. # na unsigned int 4
  817. # nb unsigned int 4
  818. # nc unsigned int 4
  819. # ua unsigned int 4
  820. # ub unsigned int 4
  821. # uc unsigned int 4
  822. for f in sfaces['triangles_smooth_uv']:
  823. vi = f['vertex']
  824. ni = f['normal']
  825. ui = f['uv']
  826. data = struct.pack('<IIIHIIIIII',
  827. vi[0]-1, vi[1]-1, vi[2]-1,
  828. f['material'],
  829. ni[0]-1, ni[1]-1, ni[2]-1,
  830. ui[0]-1, ui[1]-1, ui[2]-1)
  831. buffer.append(data)
  832. # 8. flat quads
  833. # ------------------
  834. # a unsigned int 4
  835. # b unsigned int 4
  836. # c unsigned int 4
  837. # d unsigned int 4
  838. # m unsigned short 2
  839. for f in sfaces['quads_flat']:
  840. vi = f['vertex']
  841. data = struct.pack('<IIIIH',
  842. vi[0]-1, vi[1]-1, vi[2]-1, vi[3]-1,
  843. f['material'])
  844. buffer.append(data)
  845. # 9. smooth quads
  846. # -------------------
  847. # a unsigned int 4
  848. # b unsigned int 4
  849. # c unsigned int 4
  850. # d unsigned int 4
  851. # m unsigned short 2
  852. # na unsigned int 4
  853. # nb unsigned int 4
  854. # nc unsigned int 4
  855. # nd unsigned int 4
  856. for f in sfaces['quads_smooth']:
  857. vi = f['vertex']
  858. ni = f['normal']
  859. data = struct.pack('<IIIIHIIII',
  860. vi[0]-1, vi[1]-1, vi[2]-1, vi[3]-1,
  861. f['material'],
  862. ni[0]-1, ni[1]-1, ni[2]-1, ni[3]-1)
  863. buffer.append(data)
  864. # 10. flat quads uv
  865. # ------------------
  866. # a unsigned int 4
  867. # b unsigned int 4
  868. # c unsigned int 4
  869. # d unsigned int 4
  870. # m unsigned short 2
  871. # ua unsigned int 4
  872. # ub unsigned int 4
  873. # uc unsigned int 4
  874. # ud unsigned int 4
  875. for f in sfaces['quads_flat_uv']:
  876. vi = f['vertex']
  877. ui = f['uv']
  878. data = struct.pack('<IIIIHIIII',
  879. vi[0]-1, vi[1]-1, vi[2]-1, vi[3]-1,
  880. f['material'],
  881. ui[0]-1, ui[1]-1, ui[2]-1, ui[3]-1)
  882. buffer.append(data)
  883. # 11. smooth quads uv
  884. # -------------------
  885. # a unsigned int 4
  886. # b unsigned int 4
  887. # c unsigned int 4
  888. # d unsigned int 4
  889. # m unsigned short 2
  890. # na unsigned int 4
  891. # nb unsigned int 4
  892. # nc unsigned int 4
  893. # nd unsigned int 4
  894. # ua unsigned int 4
  895. # ub unsigned int 4
  896. # uc unsigned int 4
  897. # ud unsigned int 4
  898. for f in sfaces['quads_smooth_uv']:
  899. vi = f['vertex']
  900. ni = f['normal']
  901. ui = f['uv']
  902. data = struct.pack('<IIIIHIIIIIIII',
  903. vi[0]-1, vi[1]-1, vi[2]-1, vi[3]-1,
  904. f['material'],
  905. ni[0]-1, ni[1]-1, ni[2]-1, ni[3]-1,
  906. ui[0]-1, ui[1]-1, ui[2]-1, ui[3]-1)
  907. buffer.append(data)
  908. path = os.path.dirname(outfile)
  909. fname = os.path.join(path, binfile)
  910. out = open(fname, "wb")
  911. out.write("".join(buffer))
  912. out.close()
  913. # #############################################################################
  914. # Helpers
  915. # #############################################################################
  916. def usage():
  917. print "Usage: %s -i filename.obj -o filename.js [-a center|top|bottom] [-s flat|smooth] [-t binary|ascii]" % os.path.basename(sys.argv[0])
  918. # #####################################################
  919. # Main
  920. # #####################################################
  921. if __name__ == "__main__":
  922. # get parameters from the command line
  923. try:
  924. opts, args = getopt.getopt(sys.argv[1:], "hi:o:a:s:t:", ["help", "input=", "output=", "align=", "shading=", "type="])
  925. except getopt.GetoptError:
  926. usage()
  927. sys.exit(2)
  928. infile = outfile = ""
  929. for o, a in opts:
  930. if o in ("-h", "--help"):
  931. usage()
  932. sys.exit()
  933. elif o in ("-i", "--input"):
  934. infile = a
  935. elif o in ("-o", "--output"):
  936. outfile = a
  937. elif o in ("-a", "--align"):
  938. if a in ("top", "bottom", "center"):
  939. ALIGN = a
  940. elif o in ("-s", "--shading"):
  941. if a in ("flat", "smooth"):
  942. SHADING = a
  943. elif o in ("-t", "--type"):
  944. if a in ("binary", "ascii"):
  945. TYPE = a
  946. if infile == "" or outfile == "":
  947. usage()
  948. sys.exit(2)
  949. print "Converting [%s] into [%s] ..." % (infile, outfile)
  950. if TYPE == "ascii":
  951. convert_ascii(infile, outfile)
  952. elif TYPE == "binary":
  953. convert_binary(infile, outfile)