"""Convert Wavefront OBJ / MTL files into Three.js (JSON model version, to be used with ascii / binary loader) ------------------------- How to use this converter ------------------------- python convert_obj_three.py -i infile.obj -o outfile.js [-m "morphfiles*.obj"] [-c "morphcolors*.obj"] [-a center|centerxz|top|bottom|none] [-s smooth|flat] [-t ascii|binary] [-d invert|normal] [-b] [-e] Notes: - flags -i infile.obj input OBJ file -o outfile.js output JS file -m "morphfiles*.obj" morph OBJ files (can use wildcards, enclosed in quotes multiple patterns separate by space) -c "morphcolors*.obj" morph colors OBJ files (can use wildcards, enclosed in quotes multiple patterns separate by space) -a center|centerxz|top|bottom|none model alignment -s smooth|flat smooth = export vertex normals, flat = no normals (face normals computed in loader) -t ascii|binary export ascii or binary format (ascii has more features, binary just supports vertices, faces, normals, uvs and materials) -b bake material colors into face colors -x 10.0 scale and truncate -f 2 morph frame sampling step - by default: use smooth shading (if there were vertex normals in the original model) will be in ASCII format no face colors baking no scale and truncate morph frame step = 1 (all files will be processed) - 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) - 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 - 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 ------ Author ------ AlteredQualia http://alteredqualia.com """ import fileinput import operator import random import os.path import getopt import sys import struct import math import glob # ##################################################### # Configuration # ##################################################### ALIGN = "none" # center centerxz bottom top none SHADING = "smooth" # smooth flat TYPE = "ascii" # ascii binary TRUNCATE = False SCALE = 1.0 FRAMESTEP = 1 BAKE_COLORS = False # 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"""\ { "metadata" : { "formatVersion" : 3.1, "sourceFile" : "%(fname)s", "generatedBy" : "OBJConverter", "vertices" : %(nvertex)d, "faces" : %(nface)d, "normals" : %(nnormal)d, "colors" : %(ncolor)d, "uvs" : %(nuv)d, "materials" : %(nmaterial)d }, "scale" : %(scale)f, "materials": [%(materials)s], "vertices": [%(vertices)s], "morphTargets": [%(morphTargets)s], "morphColors": [%(morphColors)s], "normals": [%(normals)s], "colors": [%(colors)s], "uvs": [[%(uvs)s]], "faces": [%(faces)s] } """ TEMPLATE_FILE_BIN = u"""\ { "metadata" : { "formatVersion" : 3.1, "sourceFile" : "%(fname)s", "generatedBy" : "OBJConverter", "vertices" : %(nvertex)d, "faces" : %(nface)d, "normals" : %(nnormal)d, "uvs" : %(nuv)d, "materials" : %(nmaterial)d }, "materials": [%(materials)s], "buffers": "%(buffers)s" } """ TEMPLATE_VERTEX = "%f,%f,%f" TEMPLATE_VERTEX_TRUNCATE = "%d,%d,%d" TEMPLATE_N = "%.5g,%.5g,%.5g" TEMPLATE_UV = "%.5g,%.5g" TEMPLATE_COLOR = "%.3g,%.3g,%.3g" TEMPLATE_COLOR_DEC = "%d" TEMPLATE_MORPH_VERTICES = '\t{ "name": "%s", "vertices": [%s] }' TEMPLATE_MORPH_COLORS = '\t{ "name": "%s", "colors": [%s] }' # ##################################################### # 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.splitext(os.path.basename(fname))[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 centerxz(vertices): """Center model around X and Z. """ bb = bbox(vertices) cx = bb['x'][0] + (bb['x'][1] - bb['x'][0])/2.0 cy = 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]) if l: v[0] /= l v[1] /= l v[2] /= l def veckey3(v): return round(v[0], 6), round(v[1], 6), round(v[2], 6) # ##################################################### # MTL parser # ##################################################### def texture_relative_path(fullpath): texture_file = os.path.basename(fullpath.replace("\\", "/")) return texture_file def parse_mtl(fname): """Parse MTL file. """ materials = {} previous_line = "" for line in fileinput.input(fname): line = previous_line + line if line[-2:-1] == '\\': previous_line = line[:-2] continue previous_line = "" # Only split once initially for single-parameter tags that might have additional spaces in # their values (i.e. "newmtl Material with spaces"). chunks = line.split(None, 1) if len(chunks) > 0: if len(chunks) > 1: chunks[1] = chunks[1].strip() # Material start # newmtl identifier if chunks[0] == "newmtl": if len(chunks) > 1: identifier = chunks[1] else: identifier = "" if not identifier in materials: materials[identifier] = {} # Diffuse texture # map_Kd texture_diffuse.jpg if chunks[0] == "map_Kd" and len(chunks) == 2: materials[identifier]["mapDiffuse"] = texture_relative_path(chunks[1]) # Specular texture # map_Ks texture_specular.jpg if chunks[0] == "map_Ks" and len(chunks) == 2: materials[identifier]["mapSpecular"] = texture_relative_path(chunks[1]) # Alpha texture # map_d texture_alpha.png if chunks[0] == "map_d" and len(chunks) == 2: materials[identifier]["transparent"] = True materials[identifier]["mapAlpha"] = texture_relative_path(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]["mapBump"] = texture_relative_path(chunks[1]) # Split the remaining parameters. if len(chunks) > 1: chunks = [chunks[0]] + chunks[1].split() # Diffuse color # Kd 1.000 1.000 1.000 if chunks[0] == "Kd" and len(chunks) == 4: materials[identifier]["colorDiffuse"] = [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]["colorSpecular"] = [float(chunks[1]), float(chunks[2]), float(chunks[3])] # Specular coefficient # Ns 154.000 if chunks[0] == "Ns" and len(chunks) == 2: materials[identifier]["specularCoef"] = float(chunks[1]) # Dissolves # d 0.9 if chunks[0] == "d" and len(chunks) == 2: materials[identifier]["opacity"] = float(chunks[1]) if materials[identifier]["opacity"] < 1.0: materials[identifier]["transparent"] = True # Transparency # Tr 0.1 if chunks[0] == "Tr" and len(chunks) == 2: materials[identifier]["opacity"] = 1.0 - float(chunks[1]) if materials[identifier]["opacity"] < 1.0: materials[identifier]["transparent"] = True # Optical density # Ni 1.0 if chunks[0] == "Ni" and len(chunks) == 2: materials[identifier]["opticalDensity"] = float(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 = {} material = "" mcounter = 0 mcurrent = 0 mtllib = "" # current face state group = 0 object = 0 smooth = 0 previous_line = "" for line in fileinput.input(fname): line = previous_line + line if line[-2:-1] == '\\': previous_line = line[:-2] continue previous_line = "" # Only split once initially for single-parameter tags that might have additional spaces in # their values (i.e. "usemtl Material with spaces"). chunks = line.split(None, 1) if len(chunks) > 0: if len(chunks) > 1: chunks[1] = chunks[1].strip() # 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": if len(chunks) > 1: material = chunks[1] else: material = "" if not material in materials: mcurrent = mcounter materials[material] = mcounter mcounter += 1 else: mcurrent = materials[material] # Split the remaining parameters. if len(chunks) > 1: chunks = [chunks[0]] + chunks[1].split() # 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 = [] # Precompute vert / normal / uv lists # for negative index lookup vertlen = len(vertices) + 1 normlen = len(normals) + 1 uvlen = len(uvs) + 1 for v in chunks[1:]: vertex = parse_vertex(v) if vertex['v']: if vertex['v'] < 0: vertex['v'] += vertlen vertex_index.append(vertex['v']) if vertex['t']: if vertex['t'] < 0: vertex['t'] += uvlen uv_index.append(vertex['t']) if vertex['n']: if vertex['n'] < 0: vertex['n'] += normlen normal_index.append(vertex['n']) faces.append({ 'vertex':vertex_index, 'uv':uv_index, 'normal':normal_index, 'material':mcurrent, 'group':group, 'object':object, 'smooth':smooth, }) # Smooth shading if chunks[0] == "s" and len(chunks) == 2: smooth = chunks[1] return faces, vertices, uvs, normals, materials, mtllib # ##################################################### # Generator - faces # ##################################################### def setBit(value, position, on): if on: mask = 1 << position return (value | mask) else: mask = ~(1 << position) return (value & mask) def generate_face(f, fc): isTriangle = ( len(f['vertex']) == 3 ) if isTriangle: nVertices = 3 else: nVertices = 4 hasMaterial = True # for the moment OBJs without materials get default material hasFaceUvs = False # not supported in OBJ hasFaceVertexUvs = ( len(f['uv']) >= nVertices ) hasFaceNormals = False # don't export any face normals (as they are computed in engine) hasFaceVertexNormals = ( len(f["normal"]) >= nVertices and SHADING == "smooth" ) hasFaceColors = BAKE_COLORS hasFaceVertexColors = False # not supported in OBJ faceType = 0 faceType = setBit(faceType, 0, not isTriangle) faceType = setBit(faceType, 1, hasMaterial) faceType = setBit(faceType, 2, hasFaceUvs) faceType = setBit(faceType, 3, hasFaceVertexUvs) faceType = setBit(faceType, 4, hasFaceNormals) faceType = setBit(faceType, 5, hasFaceVertexNormals) faceType = setBit(faceType, 6, hasFaceColors) faceType = setBit(faceType, 7, hasFaceVertexColors) faceData = [] # order is important, must match order in JSONLoader # face type # vertex indices # material index # face uvs index # face vertex uvs indices # face normal index # face vertex normals indices # face color index # face vertex colors indices faceData.append(faceType) # must clamp in case on polygons bigger than quads for i in xrange(nVertices): index = f['vertex'][i] - 1 faceData.append(index) faceData.append( f['material'] ) if hasFaceVertexUvs: for i in xrange(nVertices): index = f['uv'][i] - 1 faceData.append(index) if hasFaceVertexNormals: for i in xrange(nVertices): index = f['normal'][i] - 1 faceData.append(index) if hasFaceColors: index = fc['material'] faceData.append(index) return ",".join( map(str, faceData) ) # ##################################################### # Generator - chunks # ##################################################### def hexcolor(c): return ( int(c[0] * 255) << 16 ) + ( int(c[1] * 255) << 8 ) + int(c[2] * 255) def generate_vertex(v, option_vertices_truncate, scale): if not option_vertices_truncate: return TEMPLATE_VERTEX % (v[0], v[1], v[2]) else: return TEMPLATE_VERTEX_TRUNCATE % (scale * v[0], scale * v[1], scale * v[2]) def generate_normal(n): return TEMPLATE_N % (n[0], n[1], n[2]) def generate_uv(uv): return TEMPLATE_UV % (uv[0], uv[1]) def generate_color_rgb(c): return TEMPLATE_COLOR % (c[0], c[1], c[2]) def generate_color_decimal(c): return TEMPLATE_COLOR_DEC % hexcolor(c) # ##################################################### # Morphs # ##################################################### def generate_morph_vertex(name, vertices): vertex_string = ",".join(generate_vertex(v, TRUNCATE, SCALE) for v in vertices) return TEMPLATE_MORPH_VERTICES % (name, vertex_string) def generate_morph_color(name, colors): color_string = ",".join(generate_color_rgb(c) for c in colors) return TEMPLATE_MORPH_COLORS % (name, color_string) def extract_material_colors(materials, mtlfilename, basename): """Extract diffuse colors from MTL materials """ if not materials: materials = { 'default': 0 } mtl = create_materials(materials, mtlfilename, basename) mtlColorArraySrt = [] for m in mtl: if m in materials: index = materials[m] color = mtl[m].get("colorDiffuse", [1,0,0]) mtlColorArraySrt.append([index, color]) mtlColorArraySrt.sort() mtlColorArray = [x[1] for x in mtlColorArraySrt] return mtlColorArray def extract_face_colors(faces, material_colors): """Extract colors from materials and assign them to faces """ faceColors = [] for face in faces: material_index = face['material'] faceColors.append(material_colors[material_index]) return faceColors def generate_morph_targets(morphfiles, n_vertices, infile): skipOriginalMorph = False norminfile = os.path.normpath(infile) morphVertexData = [] for mfilepattern in morphfiles.split(): matches = glob.glob(mfilepattern) matches.sort() indices = range(0, len(matches), FRAMESTEP) for i in indices: path = matches[i] normpath = os.path.normpath(path) if normpath != norminfile or not skipOriginalMorph: name = os.path.basename(normpath) morphFaces, morphVertices, morphUvs, morphNormals, morphMaterials, morphMtllib = parse_obj(normpath) n_morph_vertices = len(morphVertices) if n_vertices != n_morph_vertices: print "WARNING: skipping morph [%s] with different number of vertices [%d] than the original model [%d]" % (name, n_morph_vertices, n_vertices) else: if ALIGN == "center": center(morphVertices) elif ALIGN == "centerxz": centerxz(morphVertices) elif ALIGN == "bottom": bottom(morphVertices) elif ALIGN == "top": top(morphVertices) morphVertexData.append((get_name(name), morphVertices)) print "adding [%s] with %d vertices" % (name, n_morph_vertices) morphTargets = "" if len(morphVertexData): morphTargets = "\n%s\n\t" % ",\n".join(generate_morph_vertex(name, vertices) for name, vertices in morphVertexData) return morphTargets def generate_morph_colors(colorfiles, n_vertices, n_faces): morphColorData = [] colorFaces = [] materialColors = [] for mfilepattern in colorfiles.split(): matches = glob.glob(mfilepattern) matches.sort() for path in matches: normpath = os.path.normpath(path) name = os.path.basename(normpath) morphFaces, morphVertices, morphUvs, morphNormals, morphMaterials, morphMtllib = parse_obj(normpath) n_morph_vertices = len(morphVertices) n_morph_faces = len(morphFaces) if n_vertices != n_morph_vertices: print "WARNING: skipping morph color map [%s] with different number of vertices [%d] than the original model [%d]" % (name, n_morph_vertices, n_vertices) elif n_faces != n_morph_faces: print "WARNING: skipping morph color map [%s] with different number of faces [%d] than the original model [%d]" % (name, n_morph_faces, n_faces) else: morphMaterialColors = extract_material_colors(morphMaterials, morphMtllib, normpath) morphFaceColors = extract_face_colors(morphFaces, morphMaterialColors) morphColorData.append((get_name(name), morphFaceColors)) # take first color map for baking into face colors if len(colorFaces) == 0: colorFaces = morphFaces materialColors = morphMaterialColors print "adding [%s] with %d face colors" % (name, len(morphFaceColors)) morphColors = "" if len(morphColorData): morphColors = "\n%s\n\t" % ",\n".join(generate_morph_color(name, colors) for name, colors in morphColorData) return morphColors, colorFaces, materialColors # ##################################################### # 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] return COLORS[i] else: #return "0x%06x" % int(0xffffff * random.random()) return int(0xffffff * random.random()) def value2string(v): if type(v)==str and v[0:2] != "0x": return '"%s"' % v elif type(v) == bool: return str(v).lower() 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: if m in materials: 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]['DbgName'] = m mtl[m]['DbgIndex'] = index mtl[m]['DbgColor'] = generate_color(index) if BAKE_COLORS: mtl[m]['vertexColors'] = "face" 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] = { 'DbgName': m, 'DbgIndex': index, 'DbgColor': generate_color(index) } return mtl def generate_materials_string(materials, mtlfilename, basename): """Generate final materials string. """ if not materials: materials = { 'default': 0 } mtl = create_materials(materials, mtlfilename, basename) return generate_materials(mtl, materials) def create_materials(materials, mtlfilename, basename): """Parse MTL file and create mapping between its materials and OBJ materials. Eventual edge cases are handled here (missing materials, missing MTL file). """ random.seed(42) # to get well defined color order for debug colors # default materials with debug colors for when # there is no specified MTL / MTL loading failed, # or if there were no materials / null materials mtl = generate_mtl(materials) if mtlfilename: # create full pathname for MTL (included from OBJ) path = os.path.dirname(basename) fname = os.path.join(path, mtlfilename) 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 mtl # ##################################################### # 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, morphfiles, colorfiles, 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 # parse OBJ / MTL files faces, vertices, uvs, normals, materials, mtllib = parse_obj(infile) n_vertices = len(vertices) n_faces = len(faces) # align model if ALIGN == "center": center(vertices) elif ALIGN == "centerxz": centerxz(vertices) elif ALIGN == "bottom": bottom(vertices) elif ALIGN == "top": top(vertices) # generate normals string nnormal = 0 normals_string = "" if SHADING == "smooth": normals_string = ",".join(generate_normal(n) for n in normals) nnormal = len(normals) # extract morph vertices morphTargets = generate_morph_targets(morphfiles, n_vertices, infile) # extract morph colors morphColors, colorFaces, materialColors = generate_morph_colors(colorfiles, n_vertices, n_faces) # generate colors string ncolor = 0 colors_string = "" if len(colorFaces) < len(faces): colorFaces = faces materialColors = extract_material_colors(materials, mtllib, infile) if BAKE_COLORS: colors_string = ",".join(generate_color_decimal(c) for c in materialColors) ncolor = len(materialColors) # generate ascii model string text = TEMPLATE_FILE_ASCII % { "name" : get_name(outfile), "fname" : os.path.basename(infile), "nvertex" : len(vertices), "nface" : len(faces), "nuv" : len(uvs), "nnormal" : nnormal, "ncolor" : ncolor, "nmaterial" : len(materials), "materials" : generate_materials_string(materials, mtllib, infile), "normals" : normals_string, "colors" : colors_string, "uvs" : ",".join(generate_uv(uv) for uv in uvs), "vertices" : ",".join(generate_vertex(v, TRUNCATE, SCALE) for v in vertices), "morphTargets" : morphTargets, "morphColors" : morphColors, "faces" : ",".join(generate_face(f, fc) for f, fc in zip(faces, colorFaces)), "scale" : SCALE } 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 dump_materials_to_buffer(faces, buffer): for f in faces: data = struct.pack('