"""Convert Wavefront OBJ / MTL files into Three.js (slim models version, to be used with web worker based ascii / binary loader) ------------------------- How to use this converter ------------------------- python convert_obj_threejs_slim.py -i infile.obj -o outfile.js [-a center|top|bottom] [-s smooth|flat] [-t ascii|binary] Notes: - by default, model is centered (middle of bounding box goes to 0,0,0), uses smooth shading (if there were vertex normals in the original model) and is in ASCII format. - binary conversion will create two files: outfile.js (materials) outfile.bin (binary buffers) -------------------------------------------------- How to use generated JS file in your HTML document -------------------------------------------------- ... ------------------------------------- Parsers based on formats descriptions ------------------------------------- http://en.wikipedia.org/wiki/Obj http://en.wikipedia.org/wiki/Material_Template_Library ------------------- Current limitations ------------------- - for the moment, only diffuse color and texture are used (will need to extend shaders / renderers / materials in Three) - models can have more than 65,536 vertices, but in most cases it will not work well with browsers, which currently seem to have troubles with handling large JS files - texture coordinates can be wrong in canvas renderer (there is crude normalization, but it doesn't work for all cases) - smoothing can be turned on/off only for the whole mesh ---------------------------------------------- How to get proper OBJ + MTL files with Blender ---------------------------------------------- 0. Remove default cube (press DEL and ENTER) 1. Import / create model 2. Select all meshes (Select -> Select All by Type -> Mesh) 3. Export to OBJ (File -> Export -> Wavefront .obj) [*] - enable following options in exporter Material Groups Rotate X90 Apply Modifiers High Quality Normals Copy Images Selection Only Objects as OBJ Objects UVs Normals Materials Edges - select empty folder - give your exported file name with "obj" extension - click on "Export OBJ" button 4. Your model is now all files in this folder (OBJ, MTL, number of images) - this converter assumes all files staying in the same folder, (OBJ / MTL files use relative paths) - for WebGL, textures must be power of 2 sized [*] If OBJ export fails (Blender 2.54 beta), patch your Blender installation following instructions here: http://www.blendernation.com/2010/09/12/blender-2-54-beta-released/ ------ Author ------ AlteredQualia http://alteredqualia.com """ import fileinput import operator import random import os.path import getopt import sys import struct import math # ##################################################### # Configuration # ##################################################### ALIGN = "center" # center bottom top none SHADING = "smooth" # smooth flat TYPE = "ascii" # ascii binary # default colors for debugging (each material gets one distinct color): # white, red, green, blue, yellow, cyan, magenta COLORS = [0xeeeeee, 0xee0000, 0x00ee00, 0x0000ee, 0xeeee00, 0x00eeee, 0xee00ee] # ##################################################### # Templates # ##################################################### TEMPLATE_FILE_ASCII = u"""\ // Converted from: %(fname)s // vertices: %(nvertex)d // faces: %(nface)d // materials: %(nmaterial)d // // Generated with OBJ -> Three.js converter // http://github.com/alteredq/three.js/blob/master/utils/exporters/convert_obj_threejs_slim.py var model = { 'materials': [%(materials)s], 'normals': [%(normals)s], 'vertices': [%(vertices)s], 'uvs': [%(uvs)s], 'triangles': [%(triangles)s], 'triangles_n': [%(triangles_n)s], 'triangles_uv': [%(triangles_uv)s], 'triangles_n_uv': [%(triangles_n_uv)s], 'quads': [%(quads)s], 'quads_n': [%(quads_n)s], 'quads_uv': [%(quads_uv)s], 'quads_n_uv': [%(quads_n_uv)s], 'end': (new Date).getTime() } postMessage( model ); """ TEMPLATE_FILE_BIN = u"""\ // Converted from: %(fname)s // vertices: %(nvertex)d // faces: %(nface)d // materials: %(nmaterial)d // // Generated with OBJ -> Three.js converter // http://github.com/alteredq/three.js/blob/master/utils/exporters/convert_obj_threejs_slim.py var model = { 'materials': [%(materials)s], 'buffers': '%(buffers)s', 'end': (new Date).getTime() } postMessage( model ); """ TEMPLATE_VERTEX = "%f,%f,%f" TEMPLATE_UV_TRI = "%f,%f,%f,%f,%f,%f" TEMPLATE_UV_QUAD = "%f,%f,%f,%f,%f,%f,%f,%f" TEMPLATE_TRI = "%d,%d,%d,%d" TEMPLATE_QUAD = "%d,%d,%d,%d,%d" TEMPLATE_TRI_UV = "%d,%d,%d,%d,%d,%d,%d" TEMPLATE_QUAD_UV = "%d,%d,%d,%d,%d,%d,%d,%d,%d" TEMPLATE_TRI_N = "%d,%d,%d,%d,%d,%d,%d" TEMPLATE_QUAD_N = "%d,%d,%d,%d,%d,%d,%d,%d,%d" TEMPLATE_TRI_N_UV = "%d,%d,%d,%d,%d,%d,%d,%d,%d,%d" TEMPLATE_QUAD_N_UV = "%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d" TEMPLATE_N = "%f,%f,%f" TEMPLATE_UV = "%f,%f" # ##################################################### # Utils # ##################################################### def file_exists(filename): """Return true if file exists and is accessible for reading. Should be safer than just testing for existence due to links and permissions magic on Unix filesystems. @rtype: boolean """ try: f = open(filename, 'r') f.close() return True except IOError: return False def get_name(fname): """Create model name based of filename ("path/fname.js" -> "fname"). """ return os.path.basename(fname).split(".")[0] def bbox(vertices): """Compute bounding box of vertex array. """ if len(vertices)>0: minx = maxx = vertices[0][0] miny = maxy = vertices[0][1] minz = maxz = vertices[0][2] for v in vertices[1:]: if v[0]maxx: maxx = v[0] if v[1]maxy: maxy = v[1] if v[2]maxz: maxz = v[2] return { 'x':[minx,maxx], 'y':[miny,maxy], 'z':[minz,maxz] } else: return { 'x':[0,0], 'y':[0,0], 'z':[0,0] } def translate(vertices, t): """Translate array of vertices by vector t. """ for i in xrange(len(vertices)): vertices[i][0] += t[0] vertices[i][1] += t[1] vertices[i][2] += t[2] def center(vertices): """Center model (middle of bounding box). """ bb = bbox(vertices) cx = bb['x'][0] + (bb['x'][1] - bb['x'][0])/2.0 cy = bb['y'][0] + (bb['y'][1] - bb['y'][0])/2.0 cz = bb['z'][0] + (bb['z'][1] - bb['z'][0])/2.0 translate(vertices, [-cx,-cy,-cz]) def top(vertices): """Align top of the model with the floor (Y-axis) and center it around X and Z. """ bb = bbox(vertices) cx = bb['x'][0] + (bb['x'][1] - bb['x'][0])/2.0 cy = bb['y'][1] cz = bb['z'][0] + (bb['z'][1] - bb['z'][0])/2.0 translate(vertices, [-cx,-cy,-cz]) def bottom(vertices): """Align bottom of the model with the floor (Y-axis) and center it around X and Z. """ bb = bbox(vertices) cx = bb['x'][0] + (bb['x'][1] - bb['x'][0])/2.0 cy = bb['y'][0] cz = bb['z'][0] + (bb['z'][1] - bb['z'][0])/2.0 translate(vertices, [-cx,-cy,-cz]) def normalize(v): """Normalize 3d vector""" l = math.sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2]) v[0] /= l v[1] /= l v[2] /= l # ##################################################### # MTL parser # ##################################################### def parse_mtl(fname): """Parse MTL file. """ materials = {} for line in fileinput.input(fname): chunks = line.split() if len(chunks) > 0: # Material start # newmtl identifier if chunks[0] == "newmtl" and len(chunks) == 2: identifier = chunks[1] if not identifier in materials: materials[identifier] = {} # Diffuse color # Kd 1.000 1.000 1.000 if chunks[0] == "Kd" and len(chunks) == 4: materials[identifier]["col_diffuse"] = [float(chunks[1]), float(chunks[2]), float(chunks[3])] # Ambient color # Ka 1.000 1.000 1.000 if chunks[0] == "Ka" and len(chunks) == 4: materials[identifier]["col_ambient"] = [float(chunks[1]), float(chunks[2]), float(chunks[3])] # Specular color # Ks 1.000 1.000 1.000 if chunks[0] == "Ks" and len(chunks) == 4: materials[identifier]["col_specular"] = [float(chunks[1]), float(chunks[2]), float(chunks[3])] # Specular coefficient # Ns 154.000 if chunks[0] == "Ns" and len(chunks) == 2: materials[identifier]["specular_coef"] = float(chunks[1]) # Transparency # Tr 0.9 or d 0.9 if (chunks[0] == "Tr" or chunks[0] == "d") and len(chunks) == 2: materials[identifier]["transparency"] = float(chunks[1]) # Optical density # Ni 1.0 if chunks[0] == "Ni" and len(chunks) == 2: materials[identifier]["optical_density"] = float(chunks[1]) # Diffuse texture # map_Kd texture_diffuse.jpg if chunks[0] == "map_Kd" and len(chunks) == 2: materials[identifier]["map_diffuse"] = chunks[1] # Ambient texture # map_Ka texture_ambient.jpg if chunks[0] == "map_Ka" and len(chunks) == 2: materials[identifier]["map_ambient"] = chunks[1] # Specular texture # map_Ks texture_specular.jpg if chunks[0] == "map_Ks" and len(chunks) == 2: materials[identifier]["map_specular"] = chunks[1] # Alpha texture # map_d texture_alpha.png if chunks[0] == "map_d" and len(chunks) == 2: materials[identifier]["map_alpha"] = chunks[1] # Bump texture # map_bump texture_bump.jpg or bump texture_bump.jpg if (chunks[0] == "map_bump" or chunks[0] == "bump") and len(chunks) == 2: materials[identifier]["map_bump"] = chunks[1] # Illumination # illum 2 # # 0. Color on and Ambient off # 1. Color on and Ambient on # 2. Highlight on # 3. Reflection on and Ray trace on # 4. Transparency: Glass on, Reflection: Ray trace on # 5. Reflection: Fresnel on and Ray trace on # 6. Transparency: Refraction on, Reflection: Fresnel off and Ray trace on # 7. Transparency: Refraction on, Reflection: Fresnel on and Ray trace on # 8. Reflection on and Ray trace off # 9. Transparency: Glass on, Reflection: Ray trace off # 10. Casts shadows onto invisible surfaces if chunks[0] == "illum" and len(chunks) == 2: materials[identifier]["illumination"] = int(chunks[1]) return materials # ##################################################### # OBJ parser # ##################################################### def parse_vertex(text): """Parse text chunk specifying single vertex. Possible formats: vertex index vertex index / texture index vertex index / texture index / normal index vertex index / / normal index """ v = 0 t = 0 n = 0 chunks = text.split("/") v = int(chunks[0]) if len(chunks) > 1: if chunks[1]: t = int(chunks[1]) if len(chunks) > 2: if chunks[2]: n = int(chunks[2]) return { 'v':v, 't':t, 'n':n } def parse_obj(fname): """Parse OBJ file. """ vertices = [] normals = [] uvs = [] faces = [] materials = {} mcounter = 0 mcurrent = 0 mtllib = "" # current face state group = 0 object = 0 smooth = 0 for line in fileinput.input(fname): chunks = line.split() if len(chunks) > 0: # Vertices as (x,y,z) coordinates # v 0.123 0.234 0.345 if chunks[0] == "v" and len(chunks) == 4: x = float(chunks[1]) y = float(chunks[2]) z = float(chunks[3]) vertices.append([x,y,z]) # Normals in (x,y,z) form; normals might not be unit # vn 0.707 0.000 0.707 if chunks[0] == "vn" and len(chunks) == 4: x = float(chunks[1]) y = float(chunks[2]) z = float(chunks[3]) normals.append([x,y,z]) # Texture coordinates in (u,v[,w]) coordinates, w is optional # vt 0.500 -1.352 [0.234] if chunks[0] == "vt" and len(chunks) >= 3: u = float(chunks[1]) v = float(chunks[2]) w = 0 if len(chunks)>3: w = float(chunks[3]) uvs.append([u,v,w]) # Face if chunks[0] == "f" and len(chunks) >= 4: vertex_index = [] uv_index = [] normal_index = [] for v in chunks[1:]: vertex = parse_vertex(v) if vertex['v']: vertex_index.append(vertex['v']) if vertex['t']: uv_index.append(vertex['t']) if vertex['n']: normal_index.append(vertex['n']) faces.append({ 'vertex':vertex_index, 'uv':uv_index, 'normal':normal_index, 'material':mcurrent, 'group':group, 'object':object, 'smooth':smooth, }) # Group if chunks[0] == "g" and len(chunks) == 2: group = chunks[1] # Object if chunks[0] == "o" and len(chunks) == 2: object = chunks[1] # Materials definition if chunks[0] == "mtllib" and len(chunks) == 2: mtllib = chunks[1] # Material if chunks[0] == "usemtl" and len(chunks) == 2: material = chunks[1] if not material in materials: mcurrent = mcounter materials[material] = mcounter mcounter += 1 else: mcurrent = materials[material] # Smooth shading if chunks[0] == "s" and len(chunks) == 2: smooth = chunks[1] return faces, vertices, uvs, normals, materials, mtllib # ##################################################### # Generator # ##################################################### def generate_vertex(v): return TEMPLATE_VERTEX % (v[0], v[1], v[2]) def generate_triangle(f): v = f['vertex'] return TEMPLATE_TRI % (v[0]-1, v[1]-1, v[2]-1, f['material']) def generate_triangle_uv(f): v = f['vertex'] uv = f['uv'] return TEMPLATE_TRI_UV % (v[0]-1, v[1]-1, v[2]-1, f['material'], uv[0]-1, uv[1]-1, uv[2]-1) def generate_triangle_n(f): v = f['vertex'] n = f['normal'] return TEMPLATE_TRI_N % (v[0]-1, v[1]-1, v[2]-1, f['material'], n[0]-1, n[1]-1, n[2]-1) def generate_triangle_n_uv(f): v = f['vertex'] n = f['normal'] uv = f['uv'] return TEMPLATE_TRI_N_UV % (v[0]-1, v[1]-1, v[2]-1, f['material'], n[0]-1, n[1]-1, n[2]-1, uv[0]-1, uv[1]-1, uv[2]-1) def generate_quad(f): vi = f['vertex'] return TEMPLATE_QUAD % (vi[0]-1, vi[1]-1, vi[2]-1, vi[3]-1, f['material']) def generate_quad_uv(f): v = f['vertex'] uv = f['uv'] return TEMPLATE_QUAD_UV % (v[0]-1, v[1]-1, v[2]-1, v[3]-1, f['material'], uv[0]-1, uv[1]-1, uv[2]-1, uv[3]-1) def generate_quad_n(f): v = f['vertex'] n = f['normal'] return TEMPLATE_QUAD_N % (v[0]-1, v[1]-1, v[2]-1, v[3]-1, f['material'], n[0]-1, n[1]-1, n[2]-1, n[3]-1) def generate_quad_n_uv(f): v = f['vertex'] n = f['normal'] uv = f['uv'] return TEMPLATE_QUAD_N_UV % (v[0]-1, v[1]-1, v[2]-1, v[3]-1, f['material'], n[0]-1, n[1]-1, n[2]-1, n[3]-1, uv[0]-1, uv[1]-1, uv[2]-1, uv[3]-1) def generate_normal(n): return TEMPLATE_N % (n[0], n[1], n[2]) def generate_uv(uv): return TEMPLATE_UV % (uv[0], 1.0 - uv[1]) # ##################################################### # Materials # ##################################################### def generate_color(i): """Generate hex color corresponding to integer. Colors should have well defined ordering. First N colors are hardcoded, then colors are random (must seed random number generator with deterministic value before getting colors). """ if i < len(COLORS): return "0x%06x" % COLORS[i] else: return "0x%06x" % int(0xffffff * random.random()) def value2string(v): if type(v)==str and v[0] != "0": return '"%s"' % v return str(v) def generate_materials(mtl, materials): """Generate JS array of materials objects JS material objects are basically prettified one-to-one mappings of MTL properties in JSON format. """ mtl_array = [] for m in mtl: index = materials[m] # add debug information # materials should be sorted according to how # they appeared in OBJ file (for the first time) # this index is identifier used in face definitions mtl[m]['a_dbg_name'] = m mtl[m]['a_dbg_index'] = index mtl[m]['a_dbg_color'] = generate_color(index) mtl_raw = ",\n".join(['\t"%s" : %s' % (n, value2string(v)) for n,v in sorted(mtl[m].items())]) mtl_string = "\t{\n%s\n\t}" % mtl_raw mtl_array.append([index, mtl_string]) return ",\n\n".join([m for i,m in sorted(mtl_array)]) def generate_mtl(materials): """Generate dummy materials (if there is no MTL file). """ mtl = {} for m in materials: index = materials[m] mtl[m] = { 'a_dbg_name': m, 'a_dbg_index': index, 'a_dbg_color': generate_color(index) } return mtl def generate_materials_string(materials, mtllib): """Generate final materials string. """ random.seed(42) # to get well defined color order for materials # default materials with debug colors for when # there is no specified MTL / MTL loading failed, # or if there were no materials / null materials if not materials: materials = { 'default':0 } mtl = generate_mtl(materials) if mtllib: # create full pathname for MTL (included from OBJ) path = os.path.dirname(infile) fname = os.path.join(path, mtllib) if file_exists(fname): # override default materials with real ones from MTL # (where they exist, otherwise keep defaults) mtl.update(parse_mtl(fname)) else: print "Couldn't find [%s]" % fname return generate_materials(mtl, materials) # ##################################################### # Faces # ##################################################### def is_triangle_flat(f): return len(f['vertex'])==3 and not (f["normal"] and SHADING == "smooth") and not f['uv'] def is_triangle_flat_uv(f): return len(f['vertex'])==3 and not (f["normal"] and SHADING == "smooth") and len(f['uv'])==3 def is_triangle_smooth(f): return len(f['vertex'])==3 and f["normal"] and SHADING == "smooth" and not f['uv'] def is_triangle_smooth_uv(f): return len(f['vertex'])==3 and f["normal"] and SHADING == "smooth" and len(f['uv'])==3 def is_quad_flat(f): return len(f['vertex'])==4 and not (f["normal"] and SHADING == "smooth") and not f['uv'] def is_quad_flat_uv(f): return len(f['vertex'])==4 and not (f["normal"] and SHADING == "smooth") and len(f['uv'])==4 def is_quad_smooth(f): return len(f['vertex'])==4 and f["normal"] and SHADING == "smooth" and not f['uv'] def is_quad_smooth_uv(f): return len(f['vertex'])==4 and f["normal"] and SHADING == "smooth" and len(f['uv'])==4 def sort_faces(faces): data = { 'triangles_flat': [], 'triangles_flat_uv': [], 'triangles_smooth': [], 'triangles_smooth_uv': [], 'quads_flat': [], 'quads_flat_uv': [], 'quads_smooth': [], 'quads_smooth_uv': [] } for f in faces: if is_triangle_flat(f): data['triangles_flat'].append(f) elif is_triangle_flat_uv(f): data['triangles_flat_uv'].append(f) elif is_triangle_smooth(f): data['triangles_smooth'].append(f) elif is_triangle_smooth_uv(f): data['triangles_smooth_uv'].append(f) elif is_quad_flat(f): data['quads_flat'].append(f) elif is_quad_flat_uv(f): data['quads_flat_uv'].append(f) elif is_quad_smooth(f): data['quads_smooth'].append(f) elif is_quad_smooth_uv(f): data['quads_smooth_uv'].append(f) return data # ##################################################### # API - ASCII converter # ##################################################### def convert_ascii(infile, outfile): """Convert infile.obj to outfile.js Here is where everything happens. If you need to automate conversions, just import this file as Python module and call this method. """ if not file_exists(infile): print "Couldn't find [%s]" % infile return faces, vertices, uvs, normals, materials, mtllib = parse_obj(infile) if ALIGN == "center": center(vertices) elif ALIGN == "bottom": bottom(vertices) elif ALIGN == "top": top(vertices) normals_string = "" if SHADING == "smooth": normals_string = ",".join(generate_normal(n) for n in normals) sfaces = sort_faces(faces) text = TEMPLATE_FILE_ASCII % { "name" : get_name(outfile), "vertices" : ",".join(generate_vertex(v) for v in vertices), "triangles" : ",".join(generate_triangle(f) for f in sfaces['triangles_flat']), "triangles_n" : ",".join(generate_triangle_n(f) for f in sfaces['triangles_smooth']), "triangles_uv" : ",".join(generate_triangle_uv(f) for f in sfaces['triangles_flat_uv']), "triangles_n_uv": ",".join(generate_triangle_n_uv(f) for f in sfaces['triangles_smooth_uv']), "quads" : ",".join(generate_quad(f) for f in sfaces['quads_flat']), "quads_n" : ",".join(generate_quad_n(f) for f in sfaces['quads_smooth']), "quads_uv" : ",".join(generate_quad_uv(f) for f in sfaces['quads_flat_uv']), "quads_n_uv" : ",".join(generate_quad_n_uv(f) for f in sfaces['quads_smooth_uv']), "uvs" : ",".join(generate_uv(uv) for uv in uvs), "normals" : normals_string, "materials" : generate_materials_string(materials, mtllib), "fname" : infile, "nvertex" : len(vertices), "nface" : len(faces), "nmaterial" : len(materials) } out = open(outfile, "w") out.write(text) out.close() print "%d vertices, %d faces, %d materials" % (len(vertices), len(faces), len(materials)) # ############################################################################# # API - Binary converter # ############################################################################# def convert_binary(infile, outfile): """Convert infile.obj to outfile.js + outfile.bin """ if not file_exists(infile): print "Couldn't find [%s]" % infile return binfile = get_name(outfile) + ".bin" faces, vertices, uvs, normals, materials, mtllib = parse_obj(infile) if ALIGN == "center": center(vertices) elif ALIGN == "bottom": bottom(vertices) elif ALIGN == "top": top(vertices) sfaces = sort_faces(faces) # ################### # generate JS file # ################### text = TEMPLATE_FILE_BIN % { "name" : get_name(outfile), "materials" : generate_materials_string(materials, mtllib), "buffers" : binfile, "fname" : infile, "nvertex" : len(vertices), "nface" : len(faces), "nmaterial" : len(materials) } out = open(outfile, "w") out.write(text) out.close() # ################### # generate BIN file # ################### if SHADING == "smooth": nnormals = len(normals) else: nnormals = 0 buffer = [] # header # ------ header_bytes = struct.calcsize('<8s') header_bytes += struct.calcsize('