diff --git a/utils/exporters/convert_obj_threejs_slim.py b/utils/exporters/convert_obj_threejs_slim.py index 9152f6b8..8a95f3ae 100644 --- a/utils/exporters/convert_obj_threejs_slim.py +++ b/utils/exporters/convert_obj_threejs_slim.py @@ -1,1195 +1,1195 @@ -"""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(' + + ... + + + +------------------------------------- +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('