Browse Source

Added experimental Blender exporter for the new ascii JSON format.

Models exported by this version should be loaded like this (the same as for slim OBJ converter):

    var loader = new THREE.Loader();
    loader.loadAscii( "path/to/model/Model.js", function( geometry ) { createScene( geometry ) }, "path/to/model" );

Compared to OBJ converter, currently only single mesh is exported (many models are composed of several meshes).

TODO
    - model alignment
    - copy used images to folder where exported file goes
    - export all selected meshes / all meshes in the scene?
    - binary format
alteredq 14 years ago
parent
commit
c545318b51

+ 78 - 0
utils/exporters/blender/2.54/scripts/op/io_mesh_threejs_slim/__init__.py

@@ -0,0 +1,78 @@
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+#  This program is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software Foundation,
+#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# To support reload properly, try to access a package var, if it's there, reload everything
+if "bpy" in locals():
+    import sys
+    reload(sys.modules.get("io_mesh_threejs_slim.export_threejs_slim", sys))
+
+
+import bpy
+from bpy.props import *
+from io_utils import ExportHelper
+
+
+class ExportTHREEJSSlim(bpy.types.Operator, ExportHelper):
+    '''Export selected object for Three.js (ASCII JSON format).'''
+    bl_idname = "export.threejs_slim"
+    bl_label = "Export Three.js Slim"
+    
+    filename_ext = ".js"
+
+    use_modifiers = BoolProperty(name="Apply Modifiers", description="Apply modifiers to the exported mesh", default=True)
+    use_normals = BoolProperty(name="Normals", description="Export normals", default=True)
+    use_uv_coords = BoolProperty(name="UVs", description="Export texture coordinates", default=True)
+
+    @classmethod
+    def poll(cls, context):
+        return context.active_object != None
+
+    def execute(self, context):
+        print("Selected: " + context.active_object.name)
+
+        if not self.properties.filepath:
+            raise Exception("filename not set")
+
+        filepath = self.filepath
+        import io_mesh_threejs_slim.export_threejs_slim
+        return io_mesh_threejs_slim.export_threejs_slim.save(self, context, **self.properties)
+
+    def draw(self, context):
+        layout = self.layout
+
+        row = layout.row()
+        row.prop(self.properties, "use_modifiers")
+        row.prop(self.properties, "use_normals")
+        row = layout.row()
+        row.prop(self.properties, "use_uv_coords")
+        
+
+def menu_func(self, context):
+    default_path = bpy.data.filepath.replace(".blend", ".js")
+    self.layout.operator(ExportTHREEJSSlim.bl_idname, text="Three.js (.js) Slim").filepath = default_path
+
+
+def register():
+    bpy.types.INFO_MT_file_export.append(menu_func)
+
+
+def unregister():
+    bpy.types.INFO_MT_file_export.remove(menu_func)
+
+if __name__ == "__main__":
+    register()

+ 521 - 0
utils/exporters/blender/2.54/scripts/op/io_mesh_threejs_slim/export_threejs_slim.py

@@ -0,0 +1,521 @@
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+#  This program is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software Foundation,
+#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# Based on export_obj.py and export_ply.py
+# Contributors: Mr.doob, Kikko, alteredq
+
+"""
+Blender exporter for Three.js (ASCII JSON format).
+
+TODO
+    - model alignment
+    - copy used images to folder where exported file goes
+    - export all selected meshes
+    - binary format
+"""
+
+import bpy
+import mathutils
+
+import os
+import os.path
+import math
+import operator
+import random
+
+# #####################################################
+# Configuration
+# #####################################################
+ALIGN = "center"    # center bottom top none
+SHADING = "smooth"  # smooth flat 
+TYPE = "ascii"      # ascii binary
+
+# default colors for debugging (each material gets one distinct color): 
+# white, red, green, blue, yellow, cyan, magenta
+COLORS = [0xeeeeee, 0xee0000, 0x00ee00, 0x0000ee, 0xeeee00, 0x00eeee, 0xee00ee]
+
+
+# #####################################################
+# Templates
+# #####################################################
+TEMPLATE_FILE_ASCII = """\
+//  vertices: %(nvertex)d
+//  faces: %(nface)d 
+//  materials: %(nmaterial)d
+//
+//  Generated with Blender 2.54 slim exporter
+//  https://github.com/alteredq/three.js/tree/master/utils/exporters/blender/
+
+
+var model = {
+    'materials': [%(materials)s],
+
+    'normals': [%(normals)s],
+
+    'vertices': [%(vertices)s],
+
+    'uvs': [%(uvs)s],
+
+    'triangles': [%(triangles)s],
+    'triangles_n': [%(triangles_n)s],
+    'triangles_uv': [%(triangles_uv)s],
+    'triangles_n_uv': [%(triangles_n_uv)s],
+
+    'quads': [%(quads)s],
+    'quads_n': [%(quads_n)s],
+    'quads_uv': [%(quads_uv)s],
+    'quads_n_uv': [%(quads_n_uv)s],
+
+    'end': (new Date).getTime()
+    }
+    
+postMessage( model );
+"""
+
+TEMPLATE_VERTEX = "%f,%f,%f"
+
+TEMPLATE_UV_TRI = "%f,%f,%f,%f,%f,%f"
+TEMPLATE_UV_QUAD = "%f,%f,%f,%f,%f,%f,%f,%f"
+
+TEMPLATE_TRI = "%d,%d,%d,%d"
+TEMPLATE_QUAD = "%d,%d,%d,%d,%d"
+
+TEMPLATE_TRI_UV = "%d,%d,%d,%d,%d,%d,%d"
+TEMPLATE_QUAD_UV = "%d,%d,%d,%d,%d,%d,%d,%d,%d"
+
+TEMPLATE_TRI_N = "%d,%d,%d,%d,%d,%d,%d"
+TEMPLATE_QUAD_N = "%d,%d,%d,%d,%d,%d,%d,%d,%d"
+
+TEMPLATE_TRI_N_UV = "%d,%d,%d,%d,%d,%d,%d,%d,%d,%d"
+TEMPLATE_QUAD_N_UV = "%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d"
+
+TEMPLATE_N = "%f,%f,%f"
+TEMPLATE_UV = "%f,%f"
+
+# #####################################################
+# Utils
+# #####################################################
+def veckey3d(v):
+    return round(v.x, 6), round(v.y, 6), round(v.z, 6)
+
+def veckey2d(v):
+    return round(v[0], 6), round(v[1], 6)
+
+def get_normal_indices(v, normals, mesh):
+    n = []
+    mv = mesh.vertices
+    for i in v:
+        n.append( normals[veckey3d(mv[i].normal)] )
+    return n
+
+def get_uv_indices(f, uvs, mesh):
+    uv = []
+    face_index = f[1]
+    uv_layer = mesh.uv_textures.active.data
+    for i in uv_layer[face_index].uv:
+        uv.append( uvs[veckey2d(i)] )
+    return uv
+
+# #####################################################
+# Elements
+# #####################################################
+def generate_vertex(v):
+    return TEMPLATE_VERTEX % (v.co.x, v.co.y, v.co.z)
+
+def generate_normal(n):
+    return TEMPLATE_N % (n[0], n[1], n[2])
+
+def generate_uv(uv):
+    return TEMPLATE_UV % (uv[0], 1.0 - uv[1])
+
+def generate_triangle(f):
+    v = f[0].vertices
+    m = f[0].material_index
+    return TEMPLATE_TRI % (v[0], v[1], v[2], 
+                           m)
+    
+def generate_quad(f):
+    v = f[0].vertices
+    m = f[0].material_index
+    return TEMPLATE_QUAD % (v[0], v[1], v[2], v[3], 
+                            m)
+    
+def generate_triangle_n(f, normals, mesh):    
+    v = f[0].vertices
+    m = f[0].material_index
+    n = get_normal_indices(v, normals, mesh)
+    
+    return TEMPLATE_TRI_N % (v[0], v[1], v[2], 
+                             m, 
+                             n[0], n[1], n[2])
+    
+def generate_quad_n(f, normals, mesh):    
+    v = f[0].vertices
+    m = f[0].material_index
+    n = get_normal_indices(v, normals, mesh)
+    
+    return TEMPLATE_QUAD_N % (v[0], v[1], v[2], v[3],
+                              m, 
+                              n[0], n[1], n[2], n[3])
+
+def generate_triangle_uv(f, uvs, mesh):
+    v = f[0].vertices  
+    m = f[0].material_index
+    uv = get_uv_indices(f, uvs, mesh)
+    
+    return TEMPLATE_TRI_UV % (v[0], v[1], v[2], 
+                              m, 
+                              uv[0], uv[1], uv[2])
+    
+def generate_quad_uv(f, uvs, mesh):
+    v = f[0].vertices  
+    m = f[0].material_index
+    uv = get_uv_indices(f, uvs, mesh)
+    
+    return TEMPLATE_QUAD_UV % (v[0], v[1], v[2], v[3],
+                               m, 
+                               uv[0], uv[1], uv[2], uv[3])
+                              
+def generate_triangle_n_uv(f, normals, uvs, mesh):
+    v = f[0].vertices    
+    m = f[0].material_index
+    n = get_normal_indices(v, normals, mesh)
+    uv = get_uv_indices(f, uvs, mesh)    
+    
+    return TEMPLATE_TRI_N_UV % (v[0], v[1], v[2], 
+                                m, 
+                                n[0], n[1], n[2], 
+                                uv[0], uv[1], uv[2])
+    
+def generate_quad_n_uv(f, normals, uvs, mesh):
+    v = f[0].vertices    
+    m = f[0].material_index
+    n = get_normal_indices(v, normals, mesh)
+    uv = get_uv_indices(f, uvs, mesh)    
+    
+    return TEMPLATE_QUAD_N_UV % (v[0], v[1], v[2], v[3], 
+                                 m, 
+                                 n[0], n[1], n[2], n[3],
+                                 uv[0], uv[1], uv[2], uv[3])
+                                
+# #####################################################
+# Faces
+# #####################################################
+def sort_faces(faces, use_normals, use_uv_coords):
+    data = {
+    'triangles_flat': [],
+    'triangles_flat_uv': [],
+    'triangles_smooth': [],
+    'triangles_smooth_uv': [],
+    
+    'quads_flat': [],
+    'quads_flat_uv': [],
+    'quads_smooth': [],
+    'quads_smooth_uv': []
+    }
+    
+    for i, f in enumerate(faces):
+        
+        if len(f.vertices) == 3:
+            
+            if use_normals and use_uv_coords:
+                data['triangles_smooth_uv'].append([f,i])
+            elif use_normals and not use_uv_coords:
+                data['triangles_smooth'].append([f,i])
+            elif use_uv_coords:
+                data['triangles_flat_uv'].append([f,i])
+            else:
+                data['triangles_flat'].append([f,i])
+        
+        elif len(f.vertices) == 4:
+            
+            if use_normals and use_uv_coords:
+                data['quads_smooth_uv'].append([f,i])
+            elif use_normals and not use_uv_coords:
+                data['quads_smooth'].append([f,i])
+            elif use_uv_coords:
+                data['quads_flat_uv'].append([f,i])
+            else:
+                data['quads_flat'].append([f,i])
+            
+    return data
+    
+# #####################################################
+# Normals
+# #####################################################
+def extract_vertex_normals(mesh, use_normals):
+    if not use_normals:
+        return {}
+        
+    count = 0
+    normals = {}
+    
+    for f in mesh.faces:
+        for v in f.vertices:
+            key = veckey3d(mesh.vertices[v].normal)
+            if key not in normals:
+                normals[key] = count
+                count += 1
+    
+    return normals
+    
+def generate_normals(normals, use_normals):
+    if not use_normals:
+        return ""
+    
+    chunks = []
+    for key, index in sorted(normals.items(), key=operator.itemgetter(1)):
+        chunks.append(key)
+        
+    return ",".join(generate_normal(n) for n in chunks)
+    
+# #####################################################
+# UVs
+# #####################################################
+def extract_uvs(mesh, use_uv_coords):
+    if not use_uv_coords:
+        return {}
+        
+    count = 0
+    uvs = {}
+    
+    uv_layer = mesh.uv_textures.active.data
+    
+    for face_index, face in enumerate(mesh.faces):
+        for uv_index, uv in enumerate(uv_layer[face_index].uv):
+            key = veckey2d(uv)
+            if key not in uvs:
+                uvs[key] = count
+                count += 1
+            
+    return uvs
+    
+def generate_uvs(uvs, use_uv_coords):
+    if not use_uv_coords:
+        return ""
+        
+    chunks = []
+    for key, index in sorted(uvs.items(), key=operator.itemgetter(1)):
+        chunks.append(key)
+        
+    return ",".join(generate_uv(n) for n in chunks)
+    
+# #####################################################
+# Materials
+# #####################################################
+def generate_color(i):
+    """Generate hex color corresponding to integer.
+    
+    Colors should have well defined ordering.
+    First N colors are hardcoded, then colors are random 
+    (must seed random number  generator with deterministic value 
+    before getting colors).
+    """
+    
+    if i < len(COLORS):
+        return "0x%06x" % COLORS[i]
+    else:
+        return "0x%06x" % int(0xffffff * random.random())
+
+def generate_mtl(materials):
+    """Generate dummy materials.
+    """
+    
+    mtl = {}
+    for m in materials:
+        index = materials[m]
+        mtl[m] = {
+            'a_dbg_name': m,
+            'a_dbg_index': index,
+            'a_dbg_color': generate_color(index)
+        }
+    return mtl
+
+def value2string(v):
+    if type(v)==str and v[0] != "0":
+        return '"%s"' % v
+    return str(v)
+    
+def generate_materials(mtl, materials):
+    """Generate JS array of materials objects    
+    """
+    
+    mtl_array = []
+    for m in mtl:
+        index = materials[m]
+        
+        # add debug information
+        #  materials should be sorted according to how
+        #  they appeared in OBJ file (for the first time)
+        #  this index is identifier used in face definitions
+        mtl[m]['a_dbg_name'] = m
+        mtl[m]['a_dbg_index'] = index
+        mtl[m]['a_dbg_color'] = generate_color(index)
+        
+        mtl_raw = ",\n".join(['\t"%s" : %s' % (n, value2string(v)) for n,v in sorted(mtl[m].items())])
+        mtl_string = "\t{\n%s\n\t}" % mtl_raw
+        mtl_array.append([index, mtl_string])
+        
+    return ",\n\n".join([m for i,m in sorted(mtl_array)])
+        
+def extract_materials(mesh, scene):
+    world = scene.world
+
+    materials = {}
+    for m in mesh.materials:
+        materials[m.name] = {}
+        materials[m.name]['col_diffuse'] = [m.diffuse_intensity * m.diffuse_color[0], 
+                                            m.diffuse_intensity * m.diffuse_color[1], 
+                                            m.diffuse_intensity * m.diffuse_color[2]]
+                                            
+        materials[m.name]['col_specular'] = [m.specular_intensity * m.specular_color[0], 
+                                             m.specular_intensity * m.specular_color[1], 
+                                             m.specular_intensity * m.specular_color[2]]
+                                             
+        materials[m.name]['col_ambient'] = [m.ambient * world.ambient_color[0], 
+                                            m.ambient * world.ambient_color[1], 
+                                            m.ambient * world.ambient_color[2]]
+                                            
+        materials[m.name]['transparency'] = m.alpha
+        
+        # not sure about mapping values to Blinn-Phong shader
+        # Blender uses INT from [1,511] with default 0
+        # http://www.blender.org/documentation/blender_python_api_2_54_0/bpy.types.Material.html#bpy.types.Material.specular_hardness
+        materials[m.name]["specular_coef"] = m.specular_hardness 
+        
+        if m.active_texture:
+            fn = bpy.path.abspath(m.active_texture.image.filepath)
+            fn = os.path.normpath(fn)
+            fn_strip = os.path.basename(fn)
+            materials[m.name]['map_diffuse'] = fn_strip
+            
+    return materials
+        
+def generate_materials_string(mesh, scene):
+
+    random.seed(42) # to get well defined color order for debug materials
+
+    materials = {}
+    for i, m in enumerate(mesh.materials):
+        materials[m.name] = i
+    
+    if not materials:
+        materials = { 'default':0 }
+    
+    # default dummy materials
+    mtl = generate_mtl(materials)
+    
+    # extract real materials from the mesh
+    mtl.update(extract_materials(mesh, scene))
+    
+    return generate_materials(mtl, materials)
+        
+# #####################################################
+# ASCII exporter
+# #####################################################
+def generate_ascii_model(mesh, scene, use_normals, use_uv_coords):
+    sfaces = sort_faces(mesh.faces, use_normals, use_uv_coords)
+    
+    normals = extract_vertex_normals(mesh, use_normals)
+    uvs = extract_uvs(mesh, use_uv_coords)
+    
+    text = TEMPLATE_FILE_ASCII % {
+    "vertices"      : ",".join(generate_vertex(v) for v in mesh.vertices),
+    
+    "triangles"     : ",".join(generate_triangle(f) for f in sfaces['triangles_flat']),
+    "triangles_n"   : ",".join(generate_triangle_n(f, normals, mesh) for f in sfaces['triangles_smooth']),
+    "triangles_uv"  : ",".join(generate_triangle_uv(f, uvs, mesh) for f in sfaces['triangles_flat_uv']),
+    "triangles_n_uv": ",".join(generate_triangle_n_uv(f, normals, uvs, mesh) for f in sfaces['triangles_smooth_uv']),
+    
+    "quads"         : ",".join(generate_quad(f) for f in sfaces['quads_flat']),
+    "quads_n"       : ",".join(generate_quad_n(f, normals, mesh) for f in sfaces['quads_smooth']),
+    "quads_uv"      : ",".join(generate_quad_uv(f, uvs, mesh) for f in sfaces['quads_flat_uv']),
+    "quads_n_uv"    : ",".join(generate_quad_n_uv(f, normals, uvs, mesh) for f in sfaces['quads_smooth_uv']),
+    
+    "uvs"           : generate_uvs(uvs, use_uv_coords),
+    "normals"       : generate_normals(normals, use_normals),
+    
+    "materials" : generate_materials_string(mesh, scene),
+    
+    "nvertex"   : len(mesh.vertices),
+    "nface"     : len(mesh.faces),
+    "nmaterial" : 0
+    }
+    
+    return text
+
+# #####################################################
+# Main
+# #####################################################
+def save(operator, context, filepath="", use_modifiers=True, use_normals=True, use_uv_coords=True):
+
+    def rvec3d(v):
+        return round(v[0], 6), round(v[1], 6), round(v[2], 6)
+
+    def rvec2d(v):
+        return round(v[0], 6), round(v[1], 6)
+
+    scene = context.scene
+    obj = context.object
+
+    if not filepath.lower().endswith('.js'):
+        filepath += '.js'
+
+    classname = os.path.basename(filepath).split(".")[0]
+
+    if not obj:
+        raise Exception("Error, Select 1 active object")
+
+    if scene.objects.active:
+        bpy.ops.object.mode_set(mode='OBJECT')
+
+    if use_modifiers:
+        mesh = obj.create_mesh(scene, True, 'PREVIEW')
+    else:
+        mesh = obj.data
+
+    if not mesh:
+        raise Exception("Error, could not get mesh data from active object")
+
+    # that's what Blender's native export_obj.py does
+    x_rot = mathutils.Matrix.Rotation(-math.pi/2, 4, 'X')
+    mesh.transform(x_rot * obj.matrix_world)
+    mesh.calc_normals()
+    
+    faceUV = (len(mesh.uv_textures) > 0)
+    vertexUV = (len(mesh.sticky) > 0)
+
+    if (not faceUV) and (not vertexUV):
+        use_uv_coords = False
+
+    if faceUV:
+        active_uv_layer = mesh.uv_textures.active
+        if not active_uv_layer:
+            use_uv_coords = False
+
+    text = generate_ascii_model(mesh, scene, use_normals, use_uv_coords)
+    file = open(filepath, 'w')
+    file.write(text)
+    file.close()
+
+    print("writing", filepath, "done")
+
+    if use_modifiers:
+        bpy.data.meshes.remove(mesh)
+
+    return {'FINISHED'}