From 89cb329fec39cd7c570752b09352b70188cac89b Mon Sep 17 00:00:00 2001 From: "Mr.doob" Date: Tue, 23 May 2017 16:47:54 -0700 Subject: [PATCH] Replaced python obj converter with node.js script. --- utils/converters/README.md | 9 + utils/converters/obj/convert_obj_three.py | 1604 ----------------- .../obj/convert_obj_three_for_python3.py | 1604 ----------------- utils/converters/obj/split_obj.py | 504 ------ utils/converters/obj2three.js | 30 + 5 files changed, 39 insertions(+), 3712 deletions(-) create mode 100644 utils/converters/README.md delete mode 100644 utils/converters/obj/convert_obj_three.py delete mode 100644 utils/converters/obj/convert_obj_three_for_python3.py delete mode 100644 utils/converters/obj/split_obj.py create mode 100644 utils/converters/obj2three.js diff --git a/utils/converters/README.md b/utils/converters/README.md new file mode 100644 index 00000000000000..d692ee78ba8a3a --- /dev/null +++ b/utils/converters/README.md @@ -0,0 +1,9 @@ +Utilities for converting model files to the Three.js JSON format. + +## obj2three.js + +Usage: + +``` +node obj2three.js model.obj +``` diff --git a/utils/converters/obj/convert_obj_three.py b/utils/converters/obj/convert_obj_three.py deleted file mode 100644 index d5bef6d83b19e5..00000000000000 --- a/utils/converters/obj/convert_obj_three.py +++ /dev/null @@ -1,1604 +0,0 @@ -"""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(' - - ... - - - -------------------------------------- -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 range(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 range(nVertices): - index = f['vertex'][i] - 1 - faceData.append(index) - - faceData.append( f['material'] ) - - if hasFaceVertexUvs: - for i in range(nVertices): - index = f['uv'][i] - 1 - faceData.append(index) - - if hasFaceVertexNormals: - for i in range(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(' 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 - -# ############################################################################# -# API - Breaker -# ############################################################################# -def break_obj(infile, outfile): - """Break infile.obj to outfile.obj - """ - - if not file_exists(infile): - print "Couldn't find [%s]" % infile - return - - faces, vertices, uvs, normals, materials, mtllib = parse_obj(infile) - - # sort faces by materials - - chunks = {} - - for face in faces: - material = face["material"] - if not material in chunks: - chunks[material] = {"faces": [], "vertices": set(), "normals": set(), "uvs": set()} - - chunks[material]["faces"].append(face) - - # extract unique vertex / normal / uv indices used per chunk - - for material in chunks: - chunk = chunks[material] - for face in chunk["faces"]: - for i in face["vertex"]: - chunk["vertices"].add(i) - - for i in face["normal"]: - chunk["normals"].add(i) - - for i in face["uv"]: - chunk["uvs"].add(i) - - # generate new OBJs - - for mi, material in enumerate(chunks): - chunk = chunks[material] - - # generate separate vertex / normal / uv index lists for each chunk - # (including mapping from original to new indices) - - # get well defined order - - new_vertices = list(chunk["vertices"]) - new_normals = list(chunk["normals"]) - new_uvs = list(chunk["uvs"]) - - # map original => new indices - - vmap = {} - for i, v in enumerate(new_vertices): - vmap[v] = i + 1 - - nmap = {} - for i, n in enumerate(new_normals): - nmap[n] = i + 1 - - tmap = {} - for i, t in enumerate(new_uvs): - tmap[t] = i + 1 - - - # vertices - - pieces = [] - for i in new_vertices: - vertex = vertices[i-1] - txt = TEMPLATE_VERTEX % (vertex[0], vertex[1], vertex[2]) - pieces.append(txt) - - str_vertices = "\n".join(pieces) - - # normals - - pieces = [] - for i in new_normals: - normal = normals[i-1] - txt = TEMPLATE_NORMAL % (normal[0], normal[1], normal[2]) - pieces.append(txt) - - str_normals = "\n".join(pieces) - - # uvs - - pieces = [] - for i in new_uvs: - uv = uvs[i-1] - txt = TEMPLATE_UV % (uv[0], uv[1]) - pieces.append(txt) - - str_uvs = "\n".join(pieces) - - # faces - - pieces = [] - - for face in chunk["faces"]: - - txt = "" - - fv = face["vertex"] - fn = face["normal"] - ft = face["uv"] - - if len(fv) == 3: - - va = vmap[fv[0]] - vb = vmap[fv[1]] - vc = vmap[fv[2]] - - if len(fn) == 3 and len(ft) == 3: - na = nmap[fn[0]] - nb = nmap[fn[1]] - nc = nmap[fn[2]] - - ta = tmap[ft[0]] - tb = tmap[ft[1]] - tc = tmap[ft[2]] - - txt = TEMPLATE_FACE3_VTN % (va, ta, na, vb, tb, nb, vc, tc, nc) - - elif len(fn) == 3: - na = nmap[fn[0]] - nb = nmap[fn[1]] - nc = nmap[fn[2]] - - txt = TEMPLATE_FACE3_VN % (va, na, vb, nb, vc, nc) - - elif len(ft) == 3: - ta = tmap[ft[0]] - tb = tmap[ft[1]] - tc = tmap[ft[2]] - - txt = TEMPLATE_FACE3_VT % (va, ta, vb, tb, vc, tc) - - else: - txt = TEMPLATE_FACE3_V % (va, vb, vc) - - elif len(fv) == 4: - - va = vmap[fv[0]] - vb = vmap[fv[1]] - vc = vmap[fv[2]] - vd = vmap[fv[3]] - - if len(fn) == 4 and len(ft) == 4: - na = nmap[fn[0]] - nb = nmap[fn[1]] - nc = nmap[fn[2]] - nd = nmap[fn[3]] - - ta = tmap[ft[0]] - tb = tmap[ft[1]] - tc = tmap[ft[2]] - td = tmap[ft[3]] - - txt = TEMPLATE_FACE4_VTN % (va, ta, na, vb, tb, nb, vc, tc, nc, vd, td, nd) - - elif len(fn) == 4: - na = nmap[fn[0]] - nb = nmap[fn[1]] - nc = nmap[fn[2]] - nd = nmap[fn[3]] - - txt = TEMPLATE_FACE4_VN % (va, na, vb, nb, vc, nc, vd, nd) - - elif len(ft) == 4: - ta = tmap[ft[0]] - tb = tmap[ft[1]] - tc = tmap[ft[2]] - td = tmap[ft[3]] - - txt = TEMPLATE_FACE4_VT % (va, ta, vb, tb, vc, tc, vd, td) - - else: - txt = TEMPLATE_FACE4_V % (va, vb, vc, vd) - - pieces.append(txt) - - - str_faces = "\n".join(pieces) - - # generate OBJ string - - content = TEMPLATE_OBJ % { - "nfaces" : len(chunk["faces"]), - "nvertices" : len(new_vertices), - "nnormals" : len(new_normals), - "nuvs" : len(new_uvs), - - "vertices" : str_vertices, - "normals" : str_normals, - "uvs" : str_uvs, - "faces" : str_faces - } - - # write OBJ file - - outname = "%s_%03d.obj" % (outfile, mi) - - f = open(outname, "w") - f.write(content) - f.close() - - -# ############################################################################# -# Helpers -# ############################################################################# -def usage(): - print "Usage: %s -i filename.obj -o prefix" % os.path.basename(sys.argv[0]) - -# ##################################################### -# Main -# ##################################################### -if __name__ == "__main__": - - # get parameters from the command line - - try: - opts, args = getopt.getopt(sys.argv[1:], "hi:o:x:", ["help", "input=", "output=", "truncatescale="]) - - except getopt.GetoptError: - usage() - sys.exit(2) - - infile = outfile = "" - - for o, a in opts: - if o in ("-h", "--help"): - usage() - sys.exit() - - elif o in ("-i", "--input"): - infile = a - - elif o in ("-o", "--output"): - outfile = a - - elif o in ("-x", "--truncatescale"): - TRUNCATE = True - SCALE = float(a) - - if infile == "" or outfile == "": - usage() - sys.exit(2) - - print "Splitting [%s] into [%s_XXX.obj] ..." % (infile, outfile) - - break_obj(infile, outfile) - diff --git a/utils/converters/obj2three.js b/utils/converters/obj2three.js new file mode 100644 index 00000000000000..82a9a9a12a1ca5 --- /dev/null +++ b/utils/converters/obj2three.js @@ -0,0 +1,30 @@ +var fs = require( 'fs' ); +var path = require( 'path' ); + +if ( process.argv.length <= 2 ) { + + console.log( "Usage: " + path.basename( __filename ) + " model.obj" ); + process.exit( - 1 ); + +} + +// + +var PRECISION = 6; + +function parseNumber( key, value ) { + + return typeof value === 'number' ? parseFloat( value.toFixed( PRECISION ) ) : value; + +} + +THREE = require( '../../build/three.js' ); +require( '../../examples/js/loaders/OBJLoader.js' ); + +var file = process.argv[ 2 ]; +var loader = new THREE.OBJLoader(); + +var text = fs.readFileSync( file, 'utf8' ); + +var content = JSON.stringify( loader.parse( text ).toJSON(), parseNumber ); +fs.writeFileSync( path.basename( file, '.obj' ) + '.json', content, 'utf8' );