diff --git a/utils/converters/obj/convert_obj_three_for_python3.py b/utils/converters/obj/convert_obj_three_for_python3.py new file mode 100644 index 0000000000000000000000000000000000000000..a877ea2b89c0cfc1f0636805d4c901fb238a8e5a --- /dev/null +++ b/utils/converters/obj/convert_obj_three_for_python3.py @@ -0,0 +1,1623 @@ +""" +------ +Original Author - Formerly Python 2 version / file: convert_obj_three.py +------ +AlteredQualia http://alteredqualia.com + +------ +Upgraded - Python 3 version / file: convert_obj_three_for_python3.py +------ +Leroy T + +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) + -d invert|normal invert transparency + -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 + original model is assumed to use non-inverted transparency / dissolve (0.0 fully transparent, 1.0 fully opaque) + 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 + +""" + +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 +TRANSPARENCY = "normal" # normal invert + +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]) + + # Ambient texture + # map_Ka texture_ambient.jpg + if chunks[0] == "map_Ka" and len(chunks) == 2: + materials[identifier]["mapAmbient"] = 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])] + + # Ambient color + # Ka 1.000 1.000 1.000 + if chunks[0] == "Ka" and len(chunks) == 4: + materials[identifier]["colorAmbient"] = [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]) + + # Transparency + # Tr 0.9 or d 0.9 + if (chunks[0] == "Tr" or chunks[0] == "d") and len(chunks) == 2: + materials[identifier]["transparent"] = True + if TRANSPARENCY == "invert": + materials[identifier]["opacity"] = float(chunks[1]) + else: + materials[identifier]["opacity"] = 1.0 - float(chunks[1]) + + # 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('