convert_obj_threejs.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840
  1. """Convert Wavefront OBJ / MTL files into Three.js
  2. -------------------------
  3. How to use this converter
  4. -------------------------
  5. python convert_obj_threejs.py -i filename.obj -o filename.js [-a center|top|bottom]
  6. Note: by default, model is centered (middle of bounding box goes to 0,0,0).
  7. --------------------------------------------------
  8. How to use generated JS file in your HTML document
  9. --------------------------------------------------
  10. <script type="text/javascript" src="Three.js"></script>
  11. <script type="text/javascript" src="ModelName.js"></script>
  12. ...
  13. <script type="text/javascript">
  14. ...
  15. var normalizeUVsFlag = 1; // set to 1 if canvas render has missing materials
  16. var geometry = new ModelName( path_to_textures );
  17. var mesh = new THREE.Mesh( geometry, geometry.materials, normalizeUVsFlag );
  18. ...
  19. </script>
  20. -------------------------------------
  21. Parsers based on formats descriptions
  22. -------------------------------------
  23. http://en.wikipedia.org/wiki/Obj
  24. http://en.wikipedia.org/wiki/Material_Template_Library
  25. -------------------
  26. Current limitations
  27. -------------------
  28. - for the moment, only diffuse color and texture are used
  29. (will need to extend shaders / renderers / materials in Three)
  30. - models cannot have more than 65,536 vertices
  31. (this comes from WebGL using just 16-bit indices,
  32. could be worked around by expanding indexed
  33. faces into full vertex definitions)
  34. - texture coordinates can be wrong in canvas renderer
  35. (there is crude normalization, but it doesn't
  36. work for all cases)
  37. - everything is using smoothing
  38. (if you want flat shading for whole mesh,
  39. don't export normals, then Three will
  40. compute own normals)
  41. ----------------------------------------------
  42. How to get proper OBJ + MTL files with Blender
  43. ----------------------------------------------
  44. 0. Remove default cube (press DEL and ENTER)
  45. 1. Import / create model
  46. 2. Select all meshes (Select -> Select All by Type -> Mesh)
  47. 3. Export to OBJ (File -> Export -> Wavefront .obj) [*]
  48. - enable following options in exporter
  49. Material Groups
  50. Rotate X90
  51. Apply Modifiers
  52. High Quality Normals
  53. Copy Images
  54. Selection Only
  55. Objects as OBJ Objects
  56. UVs
  57. Normals
  58. Materials
  59. Edges
  60. - select empty folder
  61. - give your exported file name with "obj" extension
  62. - click on "Export OBJ" button
  63. 4. Your model is now all files in this folder (OBJ, MTL, number of images)
  64. - this converter assumes all files staying in the same folder,
  65. (OBJ / MTL files use relative paths)
  66. - for WebGL, textures must be power of 2 sized
  67. [*] If OBJ export fails (Blender 2.54 beta), patch your Blender installation
  68. following instructions here:
  69. http://www.blendernation.com/2010/09/12/blender-2-54-beta-released/
  70. ------
  71. Author
  72. ------
  73. AlteredQualia http://alteredqualia.com
  74. """
  75. import fileinput
  76. import operator
  77. import random
  78. import os.path
  79. import getopt
  80. import sys
  81. # #####################################################
  82. # Configuration
  83. # #####################################################
  84. ALIGN = "center" # center bottom top none
  85. # default colors for debugging (each material gets one distinct color):
  86. # white, red, green, blue, yellow, cyan, magenta
  87. COLORS = [0xffeeeeee, 0xffee0000, 0xff00ee00, 0xff0000ee, 0xffeeee00, 0xff00eeee, 0xffee00ee]
  88. # #####################################################
  89. # Templates
  90. # #####################################################
  91. TEMPLATE_FILE = u"""\
  92. // Converted from: %(fname)s
  93. // vertices: %(nvertex)d
  94. // faces: %(nface)d
  95. // materials: %(nmaterial)d
  96. //
  97. // Generated with OBJ -> Three.js converter
  98. // http://github.com/alteredq/three.js/blob/master/utils/exporters/convert_obj_threejs.py
  99. var %(name)s = function ( urlbase ) {
  100. var scope = this;
  101. THREE.Geometry.call(this);
  102. var materials = [%(materials)s];
  103. init_materials();
  104. var normals = [%(normals)s];
  105. %(vertices)s
  106. %(uvs)s
  107. %(faces)s
  108. this.computeCentroids();
  109. this.computeNormals();
  110. function material_color( mi ) {
  111. var m = materials[mi];
  112. if( m.col_diffuse )
  113. return (m.col_diffuse[0]*255 << 16) + (m.col_diffuse[1]*255 << 8) + m.col_diffuse[2]*255;
  114. else if ( m.a_dbg_color )
  115. return m.a_dbg_color;
  116. else
  117. return 0xffeeeeee;
  118. }
  119. function v( x, y, z ) {
  120. scope.vertices.push( new THREE.Vertex( new THREE.Vector3( x, y, z ) ) );
  121. }
  122. function f3( a, b, c, mi ) {
  123. var material = scope.materials[ mi ];
  124. scope.faces.push( new THREE.Face3( a, b, c, null, material ) );
  125. }
  126. function f4( a, b, c, d, mi ) {
  127. var material = scope.materials[ mi ];
  128. scope.faces.push( new THREE.Face4( a, b, c, d, null, material ) );
  129. }
  130. function f3n( a, b, c, mi, n1, n2, n3 ) {
  131. var material = scope.materials[ mi ];
  132. var n1x = normals[n1][0];
  133. var n1y = normals[n1][1];
  134. var n1z = normals[n1][2];
  135. var n2x = normals[n2][0];
  136. var n2y = normals[n2][1];
  137. var n2z = normals[n2][2];
  138. var n3x = normals[n3][0];
  139. var n3y = normals[n3][1];
  140. var n3z = normals[n3][2];
  141. scope.faces.push( new THREE.Face3( a, b, c,
  142. [new THREE.Vector3( n1x, n1y, n1z ), new THREE.Vector3( n2x, n2y, n2z ), new THREE.Vector3( n3x, n3y, n3z )],
  143. material ) );
  144. }
  145. function f4n( a, b, c, d, mi, n1, n2, n3, n4 ) {
  146. var material = scope.materials[ mi ];
  147. var n1x = normals[n1][0];
  148. var n1y = normals[n1][1];
  149. var n1z = normals[n1][2];
  150. var n2x = normals[n2][0];
  151. var n2y = normals[n2][1];
  152. var n2z = normals[n2][2];
  153. var n3x = normals[n3][0];
  154. var n3y = normals[n3][1];
  155. var n3z = normals[n3][2];
  156. var n4x = normals[n4][0];
  157. var n4y = normals[n4][1];
  158. var n4z = normals[n4][2];
  159. scope.faces.push( new THREE.Face4( a, b, c, d,
  160. [new THREE.Vector3( n1x, n1y, n1z ), new THREE.Vector3( n2x, n2y, n2z ), new THREE.Vector3( n3x, n3y, n3z ), new THREE.Vector3( n4x, n4y, n4z )],
  161. material ) );
  162. }
  163. function uv( u1, v1, u2, v2, u3, v3, u4, v4 ) {
  164. var uv = [];
  165. uv.push( new THREE.UV( u1, v1 ) );
  166. uv.push( new THREE.UV( u2, v2 ) );
  167. uv.push( new THREE.UV( u3, v3 ) );
  168. if ( u4 && v4 ) uv.push( new THREE.UV( u4, v4 ) );
  169. scope.uvs.push( uv );
  170. }
  171. function init_materials() {
  172. scope.materials = [];
  173. for(var i=0; i<materials.length; ++i) {
  174. scope.materials[i] = [ create_material( materials[i], urlbase ) ];
  175. }
  176. }
  177. function is_pow2( n ) {
  178. var l = Math.log(n) / Math.LN2;
  179. return Math.floor(l) == l;
  180. }
  181. function nearest_pow2(n) {
  182. var l = Math.log(n) / Math.LN2;
  183. return Math.pow( 2, Math.round(l) );
  184. }
  185. function create_material( m ) {
  186. var material;
  187. if( m.map_diffuse && urlbase ) {
  188. var texture = document.createElement( 'canvas' );
  189. material = new THREE.MeshBitmapMaterial( texture );
  190. var image = new Image();
  191. image.onload = function () {
  192. if ( !is_pow2(this.width) || !is_pow2(this.height) ) {
  193. var w = nearest_pow2( this.width );
  194. var h = nearest_pow2( this.height );
  195. material.bitmap.width = w;
  196. material.bitmap.height = h;
  197. material.bitmap.getContext("2d").drawImage( this, 0, 0, w, h );
  198. }
  199. else {
  200. material.bitmap = this;
  201. }
  202. material.loaded = 1;
  203. };
  204. image.src = urlbase + "/" + m.map_diffuse;
  205. }
  206. else if( m.col_diffuse ) {
  207. var color = (m.col_diffuse[0]*255 << 16) + (m.col_diffuse[1]*255 << 8) + m.col_diffuse[2]*255;
  208. material = new THREE.MeshColorFillMaterial( color, m.transparency );
  209. }
  210. else if( m.a_dbg_color ) {
  211. material = new THREE.MeshColorFillMaterial( m.a_dbg_color );
  212. }
  213. else {
  214. material = new THREE.MeshColorFillMaterial( 0xffeeeeee );
  215. }
  216. return material;
  217. }
  218. }
  219. %(name)s.prototype = new THREE.Geometry();
  220. %(name)s.prototype.constructor = %(name)s;
  221. """
  222. TEMPLATE_VERTEX = "\tv(%f,%f,%f);"
  223. TEMPLATE_UV3 = "\tuv(%f,%f,%f,%f,%f,%f);"
  224. TEMPLATE_UV4 = "\tuv(%f,%f,%f,%f,%f,%f,%f,%f);"
  225. TEMPLATE_FACE3 = "\tf3(%d,%d,%d,%d);"
  226. TEMPLATE_FACE4 = "\tf4(%d,%d,%d,%d,%d);"
  227. TEMPLATE_FACE3N = "\tf3n(%d,%d,%d, %d, %d,%d,%d);"
  228. TEMPLATE_FACE4N = "\tf4n(%d,%d,%d,%d, %d, %d,%d,%d,%d);"
  229. TEMPLATE_N = "[%f,%f,%f]"
  230. # #####################################################
  231. # Utils
  232. # #####################################################
  233. def file_exists(filename):
  234. """Return true if file exists and is accessible for reading.
  235. Should be safer than just testing for existence due to links and
  236. permissions magic on Unix filesystems.
  237. @rtype: boolean
  238. """
  239. try:
  240. f = open(filename, 'r')
  241. f.close()
  242. return True
  243. except IOError:
  244. return False
  245. def get_name(fname):
  246. """Create model name based of filename ("path/fname.js" -> "fname").
  247. """
  248. return os.path.basename(fname).split(".")[0]
  249. def bbox(vertices):
  250. """Compute bounding box of vertex array.
  251. """
  252. if len(vertices)>0:
  253. minx = maxx = vertices[0][0]
  254. miny = maxy = vertices[0][1]
  255. minz = maxz = vertices[0][2]
  256. for v in vertices[1:]:
  257. if v[0]<minx:
  258. minx = v[0]
  259. elif v[0]>maxx:
  260. maxx = v[0]
  261. if v[1]<miny:
  262. miny = v[1]
  263. elif v[1]>maxy:
  264. maxy = v[1]
  265. if v[2]<minz:
  266. minz = v[2]
  267. elif v[2]>maxz:
  268. maxz = v[2]
  269. return { 'x':[minx,maxx], 'y':[miny,maxy], 'z':[minz,maxz] }
  270. else:
  271. return { 'x':[0,0], 'y':[0,0], 'z':[0,0] }
  272. def translate(vertices, t):
  273. """Translate array of vertices by vector t.
  274. """
  275. for i in xrange(len(vertices)):
  276. vertices[i][0] += t[0]
  277. vertices[i][1] += t[1]
  278. vertices[i][2] += t[2]
  279. def center(vertices):
  280. """Center model (middle of bounding box).
  281. """
  282. bb = bbox(vertices)
  283. cx = bb['x'][0] + (bb['x'][1] - bb['x'][0])/2.0
  284. cy = bb['y'][0] + (bb['y'][1] - bb['y'][0])/2.0
  285. cz = bb['z'][0] + (bb['z'][1] - bb['z'][0])/2.0
  286. translate(vertices, [-cx,-cy,-cz])
  287. def top(vertices):
  288. """Align top of the model with the floor (Y-axis) and center it around X and Z.
  289. """
  290. bb = bbox(vertices)
  291. cx = bb['x'][0] + (bb['x'][1] - bb['x'][0])/2.0
  292. cy = bb['y'][1]
  293. cz = bb['z'][0] + (bb['z'][1] - bb['z'][0])/2.0
  294. translate(vertices, [-cx,-cy,-cz])
  295. def bottom(vertices):
  296. """Align bottom of the model with the floor (Y-axis) and center it around X and Z.
  297. """
  298. bb = bbox(vertices)
  299. cx = bb['x'][0] + (bb['x'][1] - bb['x'][0])/2.0
  300. cy = bb['y'][0]
  301. cz = bb['z'][0] + (bb['z'][1] - bb['z'][0])/2.0
  302. translate(vertices, [-cx,cy,-cz])
  303. def normalize(v):
  304. """Normalize 3d vector"""
  305. l = math.sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2])
  306. v[0] /= l
  307. v[1] /= l
  308. v[2] /= l
  309. # #####################################################
  310. # MTL parser
  311. # #####################################################
  312. def parse_mtl(fname):
  313. """Parse MTL file.
  314. """
  315. materials = {}
  316. for line in fileinput.input(fname):
  317. chunks = line.split()
  318. if len(chunks) > 0:
  319. # Material start
  320. # newmtl identifier
  321. if chunks[0] == "newmtl" and len(chunks) == 2:
  322. identifier = chunks[1]
  323. if not identifier in materials:
  324. materials[identifier] = {}
  325. # Diffuse color
  326. # Kd 1.000 1.000 1.000
  327. if chunks[0] == "Kd" and len(chunks) == 4:
  328. materials[identifier]["col_diffuse"] = [float(chunks[1]), float(chunks[2]), float(chunks[3])]
  329. # Ambient color
  330. # Ka 1.000 1.000 1.000
  331. if chunks[0] == "Ka" and len(chunks) == 4:
  332. materials[identifier]["col_ambient"] = [float(chunks[1]), float(chunks[2]), float(chunks[3])]
  333. # Specular color
  334. # Ks 1.000 1.000 1.000
  335. if chunks[0] == "Ks" and len(chunks) == 4:
  336. materials[identifier]["col_specular"] = [float(chunks[1]), float(chunks[2]), float(chunks[3])]
  337. # Specular coefficient
  338. # Ns 154.000
  339. if chunks[0] == "Ns" and len(chunks) == 2:
  340. materials[identifier]["specular_coef"] = float(chunks[1])
  341. # Transparency
  342. # Tr 0.9 or d 0.9
  343. if (chunks[0] == "Tr" or chunks[0] == "d") and len(chunks) == 2:
  344. materials[identifier]["transparency"] = float(chunks[1])
  345. # Optical density
  346. # Ni 1.0
  347. if chunks[0] == "Ni" and len(chunks) == 2:
  348. materials[identifier]["optical_density"] = float(chunks[1])
  349. # Diffuse texture
  350. # map_Kd texture_diffuse.jpg
  351. if chunks[0] == "map_Kd" and len(chunks) == 2:
  352. materials[identifier]["map_diffuse"] = chunks[1]
  353. # Ambient texture
  354. # map_Ka texture_ambient.jpg
  355. if chunks[0] == "map_Ka" and len(chunks) == 2:
  356. materials[identifier]["map_ambient"] = chunks[1]
  357. # Specular texture
  358. # map_Ks texture_specular.jpg
  359. if chunks[0] == "map_Ks" and len(chunks) == 2:
  360. materials[identifier]["map_specular"] = chunks[1]
  361. # Alpha texture
  362. # map_d texture_alpha.png
  363. if chunks[0] == "map_d" and len(chunks) == 2:
  364. materials[identifier]["map_alpha"] = chunks[1]
  365. # Bump texture
  366. # map_bump texture_bump.jpg or bump texture_bump.jpg
  367. if (chunks[0] == "map_bump" or chunks[0] == "bump") and len(chunks) == 2:
  368. materials[identifier]["map_bump"] = chunks[1]
  369. # Illumination
  370. # illum 2
  371. #
  372. # 0. Color on and Ambient off
  373. # 1. Color on and Ambient on
  374. # 2. Highlight on
  375. # 3. Reflection on and Ray trace on
  376. # 4. Transparency: Glass on, Reflection: Ray trace on
  377. # 5. Reflection: Fresnel on and Ray trace on
  378. # 6. Transparency: Refraction on, Reflection: Fresnel off and Ray trace on
  379. # 7. Transparency: Refraction on, Reflection: Fresnel on and Ray trace on
  380. # 8. Reflection on and Ray trace off
  381. # 9. Transparency: Glass on, Reflection: Ray trace off
  382. # 10. Casts shadows onto invisible surfaces
  383. if chunks[0] == "illum" and len(chunks) == 2:
  384. materials[identifier]["illumination"] = int(chunks[1])
  385. return materials
  386. # #####################################################
  387. # OBJ parser
  388. # #####################################################
  389. def parse_vertex(text):
  390. """Parse text chunk specifying single vertex.
  391. Possible formats:
  392. vertex index
  393. vertex index / texture index
  394. vertex index / texture index / normal index
  395. vertex index / / normal index
  396. """
  397. v = 0
  398. t = 0
  399. n = 0
  400. chunks = text.split("/")
  401. v = int(chunks[0])
  402. if len(chunks) > 1:
  403. if chunks[1]:
  404. t = int(chunks[1])
  405. if len(chunks) > 2:
  406. if chunks[2]:
  407. n = int(chunks[2])
  408. return { 'v':v, 't':t, 'n':n }
  409. def parse_obj(fname):
  410. """Parse OBJ file.
  411. """
  412. vertices = []
  413. normals = []
  414. uvs = []
  415. faces = []
  416. materials = {}
  417. mcounter = 0
  418. mcurrent = 0
  419. mtllib = ""
  420. # current face state
  421. group = 0
  422. object = 0
  423. smooth = 0
  424. for line in fileinput.input(fname):
  425. chunks = line.split()
  426. if len(chunks) > 0:
  427. # Vertices as (x,y,z) coordinates
  428. # v 0.123 0.234 0.345
  429. if chunks[0] == "v" and len(chunks) == 4:
  430. x = float(chunks[1])
  431. y = float(chunks[2])
  432. z = float(chunks[3])
  433. vertices.append([x,y,z])
  434. # Normals in (x,y,z) form; normals might not be unit
  435. # vn 0.707 0.000 0.707
  436. if chunks[0] == "vn" and len(chunks) == 4:
  437. x = float(chunks[1])
  438. y = float(chunks[2])
  439. z = float(chunks[3])
  440. normals.append([x,y,z])
  441. # Texture coordinates in (u,v[,w]) coordinates, w is optional
  442. # vt 0.500 -1.352 [0.234]
  443. if chunks[0] == "vt" and len(chunks) >= 3:
  444. u = float(chunks[1])
  445. v = float(chunks[2])
  446. w = 0
  447. if len(chunks)>3:
  448. w = float(chunks[3])
  449. uvs.append([u,v,w])
  450. # Face
  451. if chunks[0] == "f" and len(chunks) >= 4:
  452. vertex_index = []
  453. uv_index = []
  454. normal_index = []
  455. for v in chunks[1:]:
  456. vertex = parse_vertex(v)
  457. if vertex['v']:
  458. vertex_index.append(vertex['v'])
  459. if vertex['t']:
  460. uv_index.append(vertex['t'])
  461. if vertex['n']:
  462. normal_index.append(vertex['n'])
  463. faces.append({
  464. 'vertex':vertex_index,
  465. 'uv':uv_index,
  466. 'normal':normal_index,
  467. 'material':mcurrent,
  468. 'group':group,
  469. 'object':object,
  470. 'smooth':smooth,
  471. })
  472. # Group
  473. if chunks[0] == "g" and len(chunks) == 2:
  474. group = chunks[1]
  475. # Object
  476. if chunks[0] == "o" and len(chunks) == 2:
  477. object = chunks[1]
  478. # Materials definition
  479. if chunks[0] == "mtllib" and len(chunks) == 2:
  480. mtllib = chunks[1]
  481. # Material
  482. if chunks[0] == "usemtl" and len(chunks) == 2:
  483. material = chunks[1]
  484. if not material in materials:
  485. mcurrent = mcounter
  486. materials[material] = mcounter
  487. mcounter += 1
  488. else:
  489. mcurrent = materials[material]
  490. # Smooth shading
  491. if chunks[0] == "s" and len(chunks) == 2:
  492. smooth = chunks[1]
  493. return faces, vertices, uvs, normals, materials, mtllib
  494. # #####################################################
  495. # Generator
  496. # #####################################################
  497. def generate_vertex(v):
  498. return TEMPLATE_VERTEX % (v[0], v[1], v[2])
  499. def generate_uv(f, uvs):
  500. ui = f['uv']
  501. if len(ui) == 3:
  502. return TEMPLATE_UV3 % (uvs[ui[0]-1][0], 1.0 - uvs[ui[0]-1][1],
  503. uvs[ui[1]-1][0], 1.0 - uvs[ui[1]-1][1],
  504. uvs[ui[2]-1][0], 1.0 - uvs[ui[2]-1][1])
  505. elif len(ui) == 4:
  506. return TEMPLATE_UV4 % (uvs[ui[0]-1][0], 1.0 - uvs[ui[0]-1][1],
  507. uvs[ui[1]-1][0], 1.0 - uvs[ui[1]-1][1],
  508. uvs[ui[2]-1][0], 1.0 - uvs[ui[2]-1][1],
  509. uvs[ui[3]-1][0], 1.0 - uvs[ui[3]-1][1])
  510. return ""
  511. def generate_face(f):
  512. vi = f['vertex']
  513. if f["normal"]:
  514. ni = f['normal']
  515. if len(vi) == 3:
  516. return TEMPLATE_FACE3N % (vi[0]-1, vi[1]-1, vi[2]-1, f['material'], ni[0]-1, ni[1]-1, ni[2]-1)
  517. elif len(vi) == 4:
  518. return TEMPLATE_FACE4N % (vi[0]-1, vi[1]-1, vi[2]-1, vi[3]-1, f['material'], ni[0]-1, ni[1]-1, ni[2]-1, ni[3]-1)
  519. else:
  520. if len(vi) == 3:
  521. return TEMPLATE_FACE3 % (vi[0]-1, vi[1]-1, vi[2]-1, f['material'])
  522. elif len(vi) == 4:
  523. return TEMPLATE_FACE4 % (vi[0]-1, vi[1]-1, vi[2]-1, vi[3]-1, f['material'])
  524. return ""
  525. def generate_normal(n):
  526. return TEMPLATE_N % (n[0], n[1], n[2])
  527. def generate_color(i):
  528. """Generate hex color corresponding to integer.
  529. Colors should have well defined ordering.
  530. First N colors are hardcoded, then colors are random
  531. (must seed random number generator with deterministic value
  532. before getting colors).
  533. """
  534. if i < len(COLORS):
  535. return "0x%x" % COLORS[i]
  536. else:
  537. return "0x%x" % (int(0xffffff * random.random()) + 0xff000000)
  538. def value2string(v):
  539. if type(v)==str and v[0] != "0":
  540. return '"%s"' % v
  541. return str(v)
  542. def generate_materials(mtl, materials):
  543. """Generate JS array of materials objects
  544. JS material objects are basically prettified one-to-one
  545. mappings of MTL properties in JSON format.
  546. """
  547. mtl_array = []
  548. for m in mtl:
  549. index = materials[m]
  550. # add debug information
  551. # materials should be sorted according to how
  552. # they appeared in OBJ file (for the first time)
  553. # this index is identifier used in face definitions
  554. mtl[m]['a_dbg_name'] = m
  555. mtl[m]['a_dbg_index'] = index
  556. mtl[m]['a_dbg_color'] = generate_color(index)
  557. mtl_raw = ",\n".join(['\t"%s" : %s' % (n, value2string(v)) for n,v in sorted(mtl[m].items())])
  558. mtl_string = "\t{\n%s\n\t}" % mtl_raw
  559. mtl_array.append([index, mtl_string])
  560. return ",\n\n".join([m for i,m in sorted(mtl_array)])
  561. def generate_mtl(materials):
  562. """Generate dummy materials (if there is no MTL file).
  563. """
  564. mtl = {}
  565. for m in materials:
  566. index = materials[m]
  567. mtl[m] = {
  568. 'a_dbg_name': m,
  569. 'a_dbg_index': index,
  570. 'a_dbg_color': generate_color(index)
  571. }
  572. return mtl
  573. # #####################################################
  574. # API
  575. # #####################################################
  576. def convert(infile, outfile):
  577. """Convert infile.obj to outfile.js
  578. Here is where everything happens. If you need to automate conversions,
  579. just import this file as Python module and call this method.
  580. """
  581. if not file_exists(infile):
  582. print "Couldn't find [%s]" % infile
  583. return
  584. faces, vertices, uvs, normals, materials, mtllib = parse_obj(infile)
  585. if ALIGN == "center":
  586. center(vertices)
  587. elif ALIGN == "bottom":
  588. bottom(vertices)
  589. elif ALIGN == "top":
  590. top(vertices)
  591. random.seed(42) # to get well defined color order for materials
  592. uv_string = ""
  593. if len(uvs)>0:
  594. uv_string = "\n".join([generate_uv(f, uvs) for f in faces])
  595. # default materials with debug colors for when
  596. # there is no specified MTL, if loading failed
  597. # or there were null materials
  598. mtl = generate_mtl(materials)
  599. if mtllib:
  600. # create full pathname for MTL (included from OBJ)
  601. path = os.path.dirname(infile)
  602. fname = os.path.join(path, mtllib)
  603. if file_exists(fname):
  604. # override default materials with real ones from MTL
  605. # (where they exist, otherwise keep defaults)
  606. mtl.update(parse_mtl(fname))
  607. else:
  608. print "Couldn't find [%s]" % fname
  609. text = TEMPLATE_FILE % {
  610. "name" : get_name(outfile),
  611. "vertices" : "\n".join([generate_vertex(v) for v in vertices]),
  612. "faces" : "\n".join([generate_face(f) for f in faces]),
  613. "uvs" : uv_string,
  614. "normals" : ",".join(generate_normal(n) for n in normals),
  615. "materials" : generate_materials(mtl, materials),
  616. "fname" : infile,
  617. "nvertex" : len(vertices),
  618. "nface" : len(faces),
  619. "nmaterial" : len(materials)
  620. }
  621. out = open(outfile, "w")
  622. out.write(text)
  623. out.close()
  624. print "%d vertices, %d faces, %d materials" % (len(vertices), len(faces), len(materials))
  625. # #############################################################################
  626. # Helpers
  627. # #############################################################################
  628. def usage():
  629. print "Usage: %s -i filename.obj -o filename.js [-a center|top|bottom]" % os.path.basename(sys.argv[0])
  630. # #####################################################
  631. # Main
  632. # #####################################################
  633. if __name__ == "__main__":
  634. # get parameters from the command line
  635. try:
  636. opts, args = getopt.getopt(sys.argv[1:], "hi:o:a:", ["help", "input=", "output=", "align="])
  637. except getopt.GetoptError:
  638. usage()
  639. sys.exit(2)
  640. infile = outfile = ""
  641. for o, a in opts:
  642. if o in ("-h", "--help"):
  643. usage()
  644. sys.exit()
  645. elif o in ("-i", "--input"):
  646. infile = a
  647. elif o in ("-o", "--output"):
  648. outfile = a
  649. elif o in ("-a", "--align"):
  650. if a in ("top", "bottom", "center"):
  651. ALIGN = a
  652. if infile == "" or outfile == "":
  653. usage()
  654. sys.exit(2)
  655. print "Converting [%s] into [%s] ..." % (infile, outfile)
  656. convert(infile, outfile)