convert_obj_threejs.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851
  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] [-s smooth|flat]
  6. Note: by default, model is centered (middle of bounding box goes to 0,0,0)
  7. and uses smooth shading (if there were vertex normals in the original
  8. model).
  9. --------------------------------------------------
  10. How to use generated JS file in your HTML document
  11. --------------------------------------------------
  12. <script type="text/javascript" src="Three.js"></script>
  13. <script type="text/javascript" src="ModelName.js"></script>
  14. ...
  15. <script type="text/javascript">
  16. ...
  17. var normalizeUVsFlag = 1; // set to 1 if canvas render has missing materials
  18. var geometry = new ModelName( path_to_textures );
  19. var mesh = new THREE.Mesh( geometry, geometry.materials, normalizeUVsFlag );
  20. ...
  21. </script>
  22. -------------------------------------
  23. Parsers based on formats descriptions
  24. -------------------------------------
  25. http://en.wikipedia.org/wiki/Obj
  26. http://en.wikipedia.org/wiki/Material_Template_Library
  27. -------------------
  28. Current limitations
  29. -------------------
  30. - for the moment, only diffuse color and texture are used
  31. (will need to extend shaders / renderers / materials in Three)
  32. - models can have more than 65,536 vertices,
  33. but in most cases it will not work well with browsers,
  34. which currently seem to have troubles with handling
  35. large JS files
  36. - texture coordinates can be wrong in canvas renderer
  37. (there is crude normalization, but it doesn't
  38. work for all cases)
  39. - smoothing can be turned on/off only for the whole mesh
  40. ----------------------------------------------
  41. How to get proper OBJ + MTL files with Blender
  42. ----------------------------------------------
  43. 0. Remove default cube (press DEL and ENTER)
  44. 1. Import / create model
  45. 2. Select all meshes (Select -> Select All by Type -> Mesh)
  46. 3. Export to OBJ (File -> Export -> Wavefront .obj) [*]
  47. - enable following options in exporter
  48. Material Groups
  49. Rotate X90
  50. Apply Modifiers
  51. High Quality Normals
  52. Copy Images
  53. Selection Only
  54. Objects as OBJ Objects
  55. UVs
  56. Normals
  57. Materials
  58. Edges
  59. - select empty folder
  60. - give your exported file name with "obj" extension
  61. - click on "Export OBJ" button
  62. 4. Your model is now all files in this folder (OBJ, MTL, number of images)
  63. - this converter assumes all files staying in the same folder,
  64. (OBJ / MTL files use relative paths)
  65. - for WebGL, textures must be power of 2 sized
  66. [*] If OBJ export fails (Blender 2.54 beta), patch your Blender installation
  67. following instructions here:
  68. http://www.blendernation.com/2010/09/12/blender-2-54-beta-released/
  69. ------
  70. Author
  71. ------
  72. AlteredQualia http://alteredqualia.com
  73. """
  74. import fileinput
  75. import operator
  76. import random
  77. import os.path
  78. import getopt
  79. import sys
  80. # #####################################################
  81. # Configuration
  82. # #####################################################
  83. ALIGN = "center" # center bottom top none
  84. SHADING = "smooth" # flat smooth
  85. # default colors for debugging (each material gets one distinct color):
  86. # white, red, green, blue, yellow, cyan, magenta
  87. COLORS = [0xeeeeee, 0xee0000, 0x00ee00, 0x0000ee, 0xeeee00, 0x00eeee, 0xee00ee]
  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"] and SHADING == "smooth":
  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%06x" % COLORS[i]
  536. else:
  537. return "0x%06x" % int(0xffffff * random.random())
  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 / MTL loading failed,
  597. # or if there were no materials / null materials
  598. if not materials:
  599. materials = { 'default':0 }
  600. mtl = generate_mtl(materials)
  601. if mtllib:
  602. # create full pathname for MTL (included from OBJ)
  603. path = os.path.dirname(infile)
  604. fname = os.path.join(path, mtllib)
  605. if file_exists(fname):
  606. # override default materials with real ones from MTL
  607. # (where they exist, otherwise keep defaults)
  608. mtl.update(parse_mtl(fname))
  609. else:
  610. print "Couldn't find [%s]" % fname
  611. normals_string = ""
  612. if SHADING == "smooth":
  613. normals_string = ",".join(generate_normal(n) for n in normals)
  614. text = TEMPLATE_FILE % {
  615. "name" : get_name(outfile),
  616. "vertices" : "\n".join([generate_vertex(v) for v in vertices]),
  617. "faces" : "\n".join([generate_face(f) for f in faces]),
  618. "uvs" : uv_string,
  619. "normals" : normals_string,
  620. "materials" : generate_materials(mtl, materials),
  621. "fname" : infile,
  622. "nvertex" : len(vertices),
  623. "nface" : len(faces),
  624. "nmaterial" : len(materials)
  625. }
  626. out = open(outfile, "w")
  627. out.write(text)
  628. out.close()
  629. print "%d vertices, %d faces, %d materials" % (len(vertices), len(faces), len(materials))
  630. # #############################################################################
  631. # Helpers
  632. # #############################################################################
  633. def usage():
  634. print "Usage: %s -i filename.obj -o filename.js [-a center|top|bottom] [-s flat|smooth]" % os.path.basename(sys.argv[0])
  635. # #####################################################
  636. # Main
  637. # #####################################################
  638. if __name__ == "__main__":
  639. # get parameters from the command line
  640. try:
  641. opts, args = getopt.getopt(sys.argv[1:], "hi:o:a:s:", ["help", "input=", "output=", "align=", "shading="])
  642. except getopt.GetoptError:
  643. usage()
  644. sys.exit(2)
  645. infile = outfile = ""
  646. for o, a in opts:
  647. if o in ("-h", "--help"):
  648. usage()
  649. sys.exit()
  650. elif o in ("-i", "--input"):
  651. infile = a
  652. elif o in ("-o", "--output"):
  653. outfile = a
  654. elif o in ("-a", "--align"):
  655. if a in ("top", "bottom", "center"):
  656. ALIGN = a
  657. elif o in ("-s", "--shading"):
  658. if a in ("flat", "smooth"):
  659. SHADING = a
  660. if infile == "" or outfile == "":
  661. usage()
  662. sys.exit(2)
  663. print "Converting [%s] into [%s] ..." % (infile, outfile)
  664. convert(infile, outfile)