Преглед на файлове

Merge remote-tracking branch 'tapio/blender-2.65' into dev

Mr.doob преди 12 години
родител
ревизия
09e7303bd1

+ 454 - 0
utils/exporters/blender/2.65/scripts/addons/io_mesh_threejs/__init__.py

@@ -0,0 +1,454 @@
+# ##### 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 #####
+
+# ################################################################
+# Init
+# ################################################################
+
+
+bl_info = {
+    "name": "three.js format",
+    "author": "mrdoob, kikko, alteredq, remoe, pxf, n3tfr34k",
+    "version": (1, 4, 0),
+    "blender": (2, 6, 5),
+    "api": 35622,
+    "location": "File > Import-Export",
+    "description": "Import-Export three.js meshes",
+    "warning": "",
+    "wiki_url": "https://github.com/mrdoob/three.js/tree/master/utils/exporters/blender",
+    "tracker_url": "https://github.com/mrdoob/three.js/issues",
+    "category": "Import-Export"}
+
+# To support reload properly, try to access a package var,
+# if it's there, reload everything
+
+import bpy
+
+if "bpy" in locals():
+    import imp
+    if "export_threejs" in locals():
+        imp.reload(export_threejs)
+    if "import_threejs" in locals():
+        imp.reload(import_threejs)
+
+from bpy.props import *
+from bpy_extras.io_utils import ExportHelper, ImportHelper
+
+# ################################################################
+# Custom properties
+# ################################################################
+
+bpy.types.Object.THREE_castShadow = bpy.props.BoolProperty()
+bpy.types.Object.THREE_receiveShadow = bpy.props.BoolProperty()
+bpy.types.Object.THREE_doubleSided = bpy.props.BoolProperty()
+bpy.types.Object.THREE_exportGeometry = bpy.props.BoolProperty(default = True)
+
+bpy.types.Material.THREE_useVertexColors = bpy.props.BoolProperty()
+bpy.types.Material.THREE_depthWrite = bpy.props.BoolProperty(default = True)
+bpy.types.Material.THREE_depthTest = bpy.props.BoolProperty(default = True)
+
+THREE_material_types = [("Basic", "Basic", "Basic"), ("Phong", "Phong", "Phong"), ("Lambert", "Lambert", "Lambert")]
+bpy.types.Material.THREE_materialType = EnumProperty(name = "Material type", description = "Material type", items = THREE_material_types, default = "Lambert")
+
+THREE_blending_types = [("NoBlending", "NoBlending", "NoBlending"), ("NormalBlending", "NormalBlending", "NormalBlending"),
+                        ("AdditiveBlending", "AdditiveBlending", "AdditiveBlending"), ("SubtractiveBlending", "SubtractiveBlending", "SubtractiveBlending"),
+                        ("MultiplyBlending", "MultiplyBlending", "MultiplyBlending"), ("AdditiveAlphaBlending", "AdditiveAlphaBlending", "AdditiveAlphaBlending")]
+bpy.types.Material.THREE_blendingType = EnumProperty(name = "Blending type", description = "Blending type", items = THREE_blending_types, default = "NormalBlending")
+
+class OBJECT_PT_hello( bpy.types.Panel ):
+
+    bl_label = "THREE"
+    bl_space_type = "PROPERTIES"
+    bl_region_type = "WINDOW"
+    bl_context = "object"
+
+    def draw(self, context):
+        layout = self.layout
+        obj = context.object
+
+        row = layout.row()
+        row.label(text="Selected object: " + obj.name )
+
+        row = layout.row()
+        row.prop( obj, "THREE_exportGeometry", text="Export geometry" )
+
+        row = layout.row()
+        row.prop( obj, "THREE_castShadow", text="Casts shadow" )
+
+        row = layout.row()
+        row.prop( obj, "THREE_receiveShadow", text="Receives shadow" )
+
+        row = layout.row()
+        row.prop( obj, "THREE_doubleSided", text="Double sided" )
+
+class MATERIAL_PT_hello( bpy.types.Panel ):
+
+    bl_label = "THREE"
+    bl_space_type = "PROPERTIES"
+    bl_region_type = "WINDOW"
+    bl_context = "material"
+
+    def draw(self, context):
+        layout = self.layout
+        mat = context.material
+
+        row = layout.row()
+        row.label(text="Selected material: " + mat.name )
+
+        row = layout.row()
+        row.prop( mat, "THREE_materialType", text="Material type" )
+
+        row = layout.row()
+        row.prop( mat, "THREE_blendingType", text="Blending type" )
+
+        row = layout.row()
+        row.prop( mat, "THREE_useVertexColors", text="Use vertex colors" )
+
+        row = layout.row()
+        row.prop( mat, "THREE_depthWrite", text="Enable depth writing" )
+
+        row = layout.row()
+        row.prop( mat, "THREE_depthTest", text="Enable depth testing" )
+
+
+# ################################################################
+# Importer
+# ################################################################
+
+class ImportTHREEJS(bpy.types.Operator, ImportHelper):
+    '''Load a Three.js ASCII JSON model'''
+
+    bl_idname = "import.threejs"
+    bl_label = "Import Three.js"
+
+    filename_ext = ".js"
+    filter_glob = StringProperty(default="*.js", options={'HIDDEN'})
+
+    option_flip_yz = BoolProperty(name="Flip YZ", description="Flip YZ", default=True)
+    recalculate_normals = BoolProperty(name="Recalculate normals", description="Recalculate vertex normals", default=True)
+    option_worker = BoolProperty(name="Worker", description="Old format using workers", default=False)
+
+    def execute(self, context):
+        import io_mesh_threejs.import_threejs
+        return io_mesh_threejs.import_threejs.load(self, context, **self.properties)
+
+
+    def draw(self, context):
+        layout = self.layout
+
+        row = layout.row()
+        row.prop(self.properties, "option_flip_yz")
+
+        row = layout.row()
+        row.prop(self.properties, "recalculate_normals")
+
+        row = layout.row()
+        row.prop(self.properties, "option_worker")
+
+
+# ################################################################
+# Exporter - settings
+# ################################################################
+
+SETTINGS_FILE_EXPORT = "threejs_settings_export.js"
+
+import os
+import json
+
+def file_exists(filename):
+    """Return true if file exists and 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_settings_fullpath():
+    return os.path.join(bpy.app.tempdir, SETTINGS_FILE_EXPORT)
+
+def save_settings_export(properties):
+
+    settings = {
+    "option_export_scene" : properties.option_export_scene,
+    "option_embed_meshes" : properties.option_embed_meshes,
+    "option_url_base_html" : properties.option_url_base_html,
+    "option_copy_textures" : properties.option_copy_textures,
+
+    "option_lights" : properties.option_lights,
+    "option_cameras" : properties.option_cameras,
+
+    "option_animation_morph" : properties.option_animation_morph,
+    "option_animation_skeletal" : properties.option_animation_skeletal,
+
+    "option_frame_step" : properties.option_frame_step,
+    "option_all_meshes" : properties.option_all_meshes,
+
+    "option_flip_yz"      : properties.option_flip_yz,
+
+    "option_materials"       : properties.option_materials,
+    "option_normals"         : properties.option_normals,
+    "option_colors"          : properties.option_colors,
+    "option_uv_coords"       : properties.option_uv_coords,
+    "option_faces"           : properties.option_faces,
+    "option_vertices"        : properties.option_vertices,
+
+    "option_skinning"        : properties.option_skinning,
+    "option_bones"           : properties.option_bones,
+
+    "option_vertices_truncate" : properties.option_vertices_truncate,
+    "option_scale"        : properties.option_scale,
+
+    "align_model"         : properties.align_model
+    }
+
+    fname = get_settings_fullpath()
+    f = open(fname, "w")
+    json.dump(settings, f)
+
+def restore_settings_export(properties):
+
+    settings = {}
+
+    fname = get_settings_fullpath()
+    if file_exists(fname):
+        f = open(fname, "r")
+        settings = json.load(f)
+
+    properties.option_vertices = settings.get("option_vertices", True)
+    properties.option_vertices_truncate = settings.get("option_vertices_truncate", False)
+    properties.option_faces = settings.get("option_faces", True)
+    properties.option_normals = settings.get("option_normals", True)
+
+    properties.option_colors = settings.get("option_colors", True)
+    properties.option_uv_coords = settings.get("option_uv_coords", True)
+    properties.option_materials = settings.get("option_materials", True)
+
+    properties.option_skinning = settings.get("option_skinning", True)
+    properties.option_bones = settings.get("option_bones", True)
+
+    properties.align_model = settings.get("align_model", "None")
+
+    properties.option_scale = settings.get("option_scale", 1.0)
+    properties.option_flip_yz = settings.get("option_flip_yz", True)
+
+    properties.option_export_scene = settings.get("option_export_scene", False)
+    properties.option_embed_meshes = settings.get("option_embed_meshes", True)
+    properties.option_url_base_html = settings.get("option_url_base_html", False)
+    properties.option_copy_textures = settings.get("option_copy_textures", False)
+
+    properties.option_lights = settings.get("option_lights", False)
+    properties.option_cameras = settings.get("option_cameras", False)
+
+    properties.option_animation_morph = settings.get("option_animation_morph", False)
+    properties.option_animation_skeletal = settings.get("option_animation_skeletal", False)
+
+    properties.option_frame_step = settings.get("option_frame_step", 1)
+    properties.option_all_meshes = settings.get("option_all_meshes", True)
+
+# ################################################################
+# Exporter
+# ################################################################
+
+class ExportTHREEJS(bpy.types.Operator, ExportHelper):
+    '''Export selected object / scene for Three.js (ASCII JSON format).'''
+
+    bl_idname = "export.threejs"
+    bl_label = "Export Three.js"
+
+    filename_ext = ".js"
+
+    option_vertices = BoolProperty(name = "Vertices", description = "Export vertices", default = True)
+    option_vertices_deltas = BoolProperty(name = "Deltas", description = "Delta vertices", default = False)
+    option_vertices_truncate = BoolProperty(name = "Truncate", description = "Truncate vertices", default = False)
+
+    option_faces = BoolProperty(name = "Faces", description = "Export faces", default = True)
+    option_faces_deltas = BoolProperty(name = "Deltas", description = "Delta faces", default = False)
+
+    option_normals = BoolProperty(name = "Normals", description = "Export normals", default = True)
+
+    option_colors = BoolProperty(name = "Colors", description = "Export vertex colors", default = True)
+    option_uv_coords = BoolProperty(name = "UVs", description = "Export texture coordinates", default = True)
+    option_materials = BoolProperty(name = "Materials", description = "Export materials", default = True)
+
+    option_skinning = BoolProperty(name = "Skinning", description = "Export skin data", default = True)
+    option_bones = BoolProperty(name = "Bones", description = "Export bones", default = True)
+
+    align_types = [("None","None","None"), ("Center","Center","Center"), ("Bottom","Bottom","Bottom"), ("Top","Top","Top")]
+    align_model = EnumProperty(name = "Align model", description = "Align model", items = align_types, default = "None")
+
+    option_scale = FloatProperty(name = "Scale", description = "Scale vertices", min = 0.01, max = 1000.0, soft_min = 0.01, soft_max = 1000.0, default = 1.0)
+    option_flip_yz = BoolProperty(name = "Flip YZ", description = "Flip YZ", default = True)
+
+    option_export_scene = BoolProperty(name = "Scene", description = "Export scene", default = False)
+    option_embed_meshes = BoolProperty(name = "Embed meshes", description = "Embed meshes", default = True)
+    option_copy_textures = BoolProperty(name = "Copy textures", description = "Copy textures", default = False)
+    option_url_base_html = BoolProperty(name = "HTML as url base", description = "Use HTML as url base ", default = False)
+
+    option_lights = BoolProperty(name = "Lights", description = "Export default scene lights", default = False)
+    option_cameras = BoolProperty(name = "Cameras", description = "Export default scene cameras", default = False)
+
+    option_animation_morph = BoolProperty(name = "Morph animation", description = "Export animation (morphs)", default = False)
+    option_animation_skeletal = BoolProperty(name = "Skeletal animation", description = "Export animation (skeletal)", default = False)
+
+    option_frame_step = IntProperty(name = "Frame step", description = "Animation frame step", min = 1, max = 1000, soft_min = 1, soft_max = 1000, default = 1)
+    option_all_meshes = BoolProperty(name = "All meshes", description = "All meshes (merged)", default = True)
+
+    def invoke(self, context, event):
+        restore_settings_export(self.properties)
+        return ExportHelper.invoke(self, context, event)
+
+    @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")
+
+        save_settings_export(self.properties)
+
+        filepath = self.filepath
+
+        import io_mesh_threejs.export_threejs
+        return io_mesh_threejs.export_threejs.save(self, context, **self.properties)
+
+    def draw(self, context):
+        layout = self.layout
+
+        row = layout.row()
+        row.label(text="Geometry:")
+
+        row = layout.row()
+        row.prop(self.properties, "option_vertices")
+        # row = layout.row()
+        # row.enabled = self.properties.option_vertices
+        # row.prop(self.properties, "option_vertices_deltas")
+        row.prop(self.properties, "option_vertices_truncate")
+        layout.separator()
+
+        row = layout.row()
+        row.prop(self.properties, "option_faces")
+        row = layout.row()
+        row.enabled = self.properties.option_faces
+        # row.prop(self.properties, "option_faces_deltas")
+        layout.separator()
+
+        row = layout.row()
+        row.prop(self.properties, "option_normals")
+        layout.separator()
+
+        row = layout.row()
+        row.prop(self.properties, "option_bones")
+        row.prop(self.properties, "option_skinning")
+        layout.separator()
+
+        row = layout.row()
+        row.label(text="Materials:")
+
+        row = layout.row()
+        row.prop(self.properties, "option_uv_coords")
+        row.prop(self.properties, "option_colors")
+        row = layout.row()
+        row.prop(self.properties, "option_materials")
+        layout.separator()
+
+        row = layout.row()
+        row.label(text="Settings:")
+
+        row = layout.row()
+        row.prop(self.properties, "align_model")
+        row = layout.row()
+        row.prop(self.properties, "option_flip_yz")
+        row.prop(self.properties, "option_scale")
+        layout.separator()
+
+        row = layout.row()
+        row.label(text="--------- Experimental ---------")
+        layout.separator()
+
+        row = layout.row()
+        row.label(text="Scene:")
+
+        row = layout.row()
+        row.prop(self.properties, "option_export_scene")
+        row.prop(self.properties, "option_embed_meshes")
+
+        row = layout.row()
+        row.prop(self.properties, "option_lights")
+        row.prop(self.properties, "option_cameras")
+        layout.separator()
+
+        row = layout.row()
+        row.label(text="Animation:")
+
+        row = layout.row()
+        row.prop(self.properties, "option_animation_morph")
+        row = layout.row()
+        row.prop(self.properties, "option_animation_skeletal")
+        row = layout.row()
+        row.prop(self.properties, "option_frame_step")
+        layout.separator()
+
+        row = layout.row()
+        row.label(text="Settings:")
+
+        row = layout.row()
+        row.prop(self.properties, "option_all_meshes")
+
+        row = layout.row()
+        row.prop(self.properties, "option_copy_textures")
+
+        row = layout.row()
+        row.prop(self.properties, "option_url_base_html")
+
+        layout.separator()
+
+
+# ################################################################
+# Common
+# ################################################################
+
+def menu_func_export(self, context):
+    default_path = bpy.data.filepath.replace(".blend", ".js")
+    self.layout.operator(ExportTHREEJS.bl_idname, text="Three.js (.js)").filepath = default_path
+
+def menu_func_import(self, context):
+    self.layout.operator(ImportTHREEJS.bl_idname, text="Three.js (.js)")
+
+def register():
+    bpy.utils.register_module(__name__)
+    bpy.types.INFO_MT_file_export.append(menu_func_export)
+    bpy.types.INFO_MT_file_import.append(menu_func_import)
+
+def unregister():
+    bpy.utils.unregister_module(__name__)
+    bpy.types.INFO_MT_file_export.remove(menu_func_export)
+    bpy.types.INFO_MT_file_import.remove(menu_func_import)
+
+if __name__ == "__main__":
+    register()

+ 2409 - 0
utils/exporters/blender/2.65/scripts/addons/io_mesh_threejs/export_threejs.py

@@ -0,0 +1,2409 @@
+# ##### 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 #####
+
+"""
+Blender exporter for Three.js (ASCII JSON format).
+
+TODO
+    - binary format
+"""
+
+import bpy
+import mathutils
+
+import shutil
+import os
+import os.path
+import math
+import operator
+import random
+
+# #####################################################
+# Configuration
+# #####################################################
+
+DEFAULTS = {
+"bgcolor" : [0, 0, 0],
+"bgalpha" : 1.0,
+
+"position" : [0, 0, 0],
+"rotation" : [-math.pi/2, 0, 0],
+"scale"    : [1, 1, 1],
+
+"camera"  :
+    {
+        "name" : "default_camera",
+        "type" : "PerspectiveCamera",
+        "near" : 1,
+        "far"  : 10000,
+        "fov"  : 60,
+        "aspect": 1.333,
+        "position" : [0, 0, 10],
+        "target"   : [0, 0, 0]
+    },
+
+"light" :
+ {
+    "name"       : "default_light",
+    "type"       : "DirectionalLight",
+    "direction"  : [0, 1, 1],
+    "color"      : [1, 1, 1],
+    "intensity"  : 0.8
+ }
+}
+
+# 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]
+
+
+# skinning
+MAX_INFLUENCES = 2
+
+
+# #####################################################
+# Templates - scene
+# #####################################################
+
+TEMPLATE_SCENE_ASCII = """\
+{
+
+"metadata" :
+{
+	"formatVersion" : 3.2,
+	"type" 			: "scene",
+	"sourceFile"    : "%(fname)s",
+	"generatedBy"   : "Blender 2.65 Exporter",
+	"objects"       : %(nobjects)s,
+	"geometries"    : %(ngeometries)s,
+	"materials"     : %(nmaterials)s,
+	"textures"      : %(ntextures)s
+},
+
+"urlBaseType" : %(basetype)s,
+
+%(sections)s
+
+"transform" :
+{
+	"position"  : %(position)s,
+	"rotation"  : %(rotation)s,
+	"scale"     : %(scale)s
+},
+
+"defaults" :
+{
+	"bgcolor" : %(bgcolor)s,
+	"bgalpha" : %(bgalpha)f,
+	"camera"  : %(defcamera)s
+}
+
+}
+"""
+
+TEMPLATE_SECTION = """
+"%s" :
+{
+%s
+},
+"""
+
+TEMPLATE_OBJECT = """\
+	%(object_id)s : {
+		"geometry"  : %(geometry_id)s,
+		"groups"    : [ %(group_id)s ],
+		"material"  : %(material_id)s,
+		"position"  : %(position)s,
+		"rotation"  : %(rotation)s,
+		"quaternion": %(quaternion)s,
+		"scale"     : %(scale)s,
+		"visible"       : %(visible)s,
+		"castShadow"    : %(castShadow)s,
+		"receiveShadow" : %(receiveShadow)s,
+		"doubleSided"   : %(doubleSided)s
+	}"""
+
+TEMPLATE_EMPTY = """\
+	%(object_id)s : {
+		"groups"    : [ %(group_id)s ],
+		"position"  : %(position)s,
+		"rotation"  : %(rotation)s,
+		"quaternion": %(quaternion)s,
+		"scale"     : %(scale)s
+	}"""
+
+TEMPLATE_GEOMETRY_LINK = """\
+	%(geometry_id)s : {
+		"type" : "ascii",
+		"url"  : %(model_file)s
+	}"""
+
+TEMPLATE_GEOMETRY_EMBED = """\
+	%(geometry_id)s : {
+		"type" : "embedded",
+		"id"  : %(embed_id)s
+	}"""
+
+TEMPLATE_TEXTURE = """\
+	%(texture_id)s : {
+		"url": %(texture_file)s%(extras)s
+	}"""
+
+TEMPLATE_MATERIAL_SCENE = """\
+	%(material_id)s : {
+		"type": %(type)s,
+		"parameters": { %(parameters)s }
+	}"""
+
+TEMPLATE_CAMERA_PERSPECTIVE = """\
+	%(camera_id)s : {
+		"type"  : "PerspectiveCamera",
+		"fov"   : %(fov)f,
+		"aspect": %(aspect)f,
+		"near"  : %(near)f,
+		"far"   : %(far)f,
+		"position": %(position)s,
+		"target"  : %(target)s
+	}"""
+
+TEMPLATE_CAMERA_ORTHO = """\
+	%(camera_id)s : {
+		"type"  : "OrthographicCamera",
+		"left"  : %(left)f,
+		"right" : %(right)f,
+		"top"   : %(top)f,
+		"bottom": %(bottom)f,
+		"near"  : %(near)f,
+		"far"   : %(far)f,
+		"position": %(position)s,
+		"target"  : %(target)s
+	}"""
+
+TEMPLATE_LIGHT_DIRECTIONAL = """\
+	%(light_id)s : {
+		"type"       : "DirectionalLight",
+		"direction"  : %(direction)s,
+		"color"      : %(color)d,
+		"intensity"  : %(intensity).2f
+	}"""
+
+TEMPLATE_LIGHT_POINT = """\
+	%(light_id)s : {
+		"type"       : "PointLight",
+		"position"   : %(position)s,
+		"color"      : %(color)d,
+		"intensity"  : %(intensity).3f
+	}"""
+
+TEMPLATE_VEC4 = '[ %g, %g, %g, %g ]'
+TEMPLATE_VEC3 = '[ %g, %g, %g ]'
+TEMPLATE_VEC2 = '[ %g, %g ]'
+TEMPLATE_STRING = '"%s"'
+TEMPLATE_HEX = "0x%06x"
+
+# #####################################################
+# Templates - model
+# #####################################################
+
+TEMPLATE_FILE_ASCII = """\
+{
+
+	"metadata" :
+	{
+		"formatVersion" : 3.1,
+		"generatedBy"   : "Blender 2.65 Exporter",
+		"vertices"      : %(nvertex)d,
+		"faces"         : %(nface)d,
+		"normals"       : %(nnormal)d,
+		"colors"        : %(ncolor)d,
+		"uvs"           : [%(nuvs)s],
+		"materials"     : %(nmaterial)d,
+		"morphTargets"  : %(nmorphTarget)d,
+		"bones"         : %(nbone)d
+	},
+
+%(model)s
+
+}
+"""
+
+TEMPLATE_MODEL_ASCII = """\
+	"scale" : %(scale)f,
+
+	"materials" : [%(materials)s],
+
+	"vertices" : [%(vertices)s],
+
+	"morphTargets" : [%(morphTargets)s],
+
+	"normals" : [%(normals)s],
+
+	"colors" : [%(colors)s],
+
+	"uvs" : [%(uvs)s],
+
+	"faces" : [%(faces)s],
+
+	"bones" : [%(bones)s],
+
+	"skinIndices" : [%(indices)s],
+
+	"skinWeights" : [%(weights)s],
+
+	"animation" : {%(animation)s}
+"""
+
+TEMPLATE_VERTEX = "%g,%g,%g"
+TEMPLATE_VERTEX_TRUNCATE = "%d,%d,%d"
+
+TEMPLATE_N = "%g,%g,%g"
+TEMPLATE_UV = "%g,%g"
+TEMPLATE_C = "%d"
+
+# #####################################################
+# Utils
+# #####################################################
+
+def veckey3(x,y,z):
+    return round(x, 6), round(y, 6), round(z, 6)
+
+def veckey3d(v):
+    return veckey3(v.x, v.y, v.z)
+
+def veckey2d(v):
+    return round(v[0], 6), round(v[1], 6)
+
+def get_faces(obj):
+    if hasattr(obj, "tessfaces"):
+        return obj.tessfaces
+    else:
+        return obj.faces
+
+def get_normal_indices(v, normals, mesh):
+    n = []
+    mv = mesh.vertices
+
+    for i in v:
+        normal = mv[i].normal
+        key = veckey3d(normal)
+
+        n.append( normals[key] )
+
+    return n
+
+def get_uv_indices(face_index, uvs, mesh, layer_index):
+    uv = []
+    uv_layer = mesh.tessface_uv_textures[layer_index].data
+    for i in uv_layer[face_index].uv:
+        uv.append( uvs[veckey2d(i)] )
+    return uv
+
+def get_color_indices(face_index, colors, mesh):
+    c = []
+    color_layer = mesh.tessface_vertex_colors.active.data
+    face_colors = color_layer[face_index]
+    face_colors = face_colors.color1, face_colors.color2, face_colors.color3, face_colors.color4
+    for i in face_colors:
+        c.append( colors[hexcolor(i)] )
+    return c
+
+def rgb2int(rgb):
+    color = (int(rgb[0]*255) << 16) + (int(rgb[1]*255) << 8) + int(rgb[2]*255);
+    return color
+
+# #####################################################
+# Utils - files
+# #####################################################
+
+def write_file(fname, content):
+    out = open(fname, "w")
+    out.write(content)
+    out.close()
+
+def ensure_folder_exist(foldername):
+    """Create folder (with whole path) if it doesn't exist yet."""
+
+    if not os.access(foldername, os.R_OK|os.W_OK|os.X_OK):
+        os.makedirs(foldername)
+
+def ensure_extension(filepath, extension):
+    if not filepath.lower().endswith(extension):
+        filepath += extension
+    return filepath
+
+def generate_mesh_filename(meshname, filepath):
+    normpath = os.path.normpath(filepath)
+    path, ext = os.path.splitext(normpath)
+    return "%s.%s%s" % (path, meshname, ext)
+
+
+# #####################################################
+# Utils - alignment
+# #####################################################
+
+def bbox(vertices):
+    """Compute bounding box of vertex array.
+    """
+
+    if len(vertices)>0:
+        minx = maxx = vertices[0].co.x
+        miny = maxy = vertices[0].co.y
+        minz = maxz = vertices[0].co.z
+
+        for v in vertices[1:]:
+            if v.co.x < minx:
+                minx = v.co.x
+            elif v.co.x > maxx:
+                maxx = v.co.x
+
+            if v.co.y < miny:
+                miny = v.co.y
+            elif v.co.y > maxy:
+                maxy = v.co.y
+
+            if v.co.z < minz:
+                minz = v.co.z
+            elif v.co.z > maxz:
+                maxz = v.co.z
+
+        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].co.x += t[0]
+        vertices[i].co.y += t[1]
+        vertices[i].co.z += 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])
+
+    return [-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])
+
+    return [-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])
+
+    return [-cx,-cy,-cz]
+
+# #####################################################
+# Elements rendering
+# #####################################################
+
+def hexcolor(c):
+    return ( int(c[0] * 255) << 16  ) + ( int(c[1] * 255) << 8 ) + int(c[2] * 255)
+
+def generate_vertices(vertices, option_vertices_truncate, option_vertices):
+    if not option_vertices:
+        return ""
+
+    return ",".join(generate_vertex(v, option_vertices_truncate) for v in vertices)
+
+def generate_vertex(v, option_vertices_truncate):
+    if not option_vertices_truncate:
+        return TEMPLATE_VERTEX % (v.co.x, v.co.y, v.co.z)
+    else:
+        return TEMPLATE_VERTEX_TRUNCATE % (v.co.x, v.co.y, v.co.z)
+
+def generate_normal(n):
+    return TEMPLATE_N % (n[0], n[1], n[2])
+
+def generate_vertex_color(c):
+    return TEMPLATE_C % c
+
+def generate_uv(uv):
+    return TEMPLATE_UV % (uv[0], uv[1])
+
+# #####################################################
+# Model exporter - faces
+# #####################################################
+
+def setBit(value, position, on):
+    if on:
+        mask = 1 << position
+        return (value | mask)
+    else:
+        mask = ~(1 << position)
+        return (value & mask)
+
+def generate_faces(normals, uv_layers, colors, meshes, option_normals, option_colors, option_uv_coords, option_materials, option_faces):
+
+    if not option_faces:
+        return "", 0
+
+    vertex_offset = 0
+    material_offset = 0
+
+    chunks = []
+    for mesh, object in meshes:
+
+        vertexUV = len(mesh.uv_textures) > 0
+        vertexColors = len(mesh.vertex_colors) > 0
+
+        mesh_colors = option_colors and vertexColors
+        mesh_uvs = option_uv_coords and vertexUV
+
+        if vertexUV:
+            active_uv_layer = mesh.uv_textures.active
+            if not active_uv_layer:
+                mesh_extract_uvs = False
+
+        if vertexColors:
+            active_col_layer = mesh.vertex_colors.active
+            if not active_col_layer:
+                mesh_extract_colors = False
+
+        for i, f in enumerate(get_faces(mesh)):
+            face = generate_face(f, i, normals, uv_layers, colors, mesh, option_normals, mesh_colors, mesh_uvs, option_materials, vertex_offset, material_offset)
+            chunks.append(face)
+
+        vertex_offset += len(mesh.vertices)
+
+        material_count = len(mesh.materials)
+        if material_count == 0:
+            material_count = 1
+
+        material_offset += material_count
+
+    return ",".join(chunks), len(chunks)
+
+def generate_face(f, faceIndex, normals, uv_layers, colors, mesh, option_normals, option_colors, option_uv_coords, option_materials, vertex_offset, material_offset):
+    isTriangle = ( len(f.vertices) == 3 )
+
+    if isTriangle:
+        nVertices = 3
+    else:
+        nVertices = 4
+
+    hasMaterial = option_materials
+
+    hasFaceUvs = False # not supported in Blender
+    hasFaceVertexUvs = option_uv_coords
+
+    hasFaceNormals = False # don't export any face normals (as they are computed in engine)
+    hasFaceVertexNormals = option_normals
+
+    hasFaceColors = False       # not supported in Blender
+    hasFaceVertexColors = option_colors
+
+    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 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.vertices[i] + vertex_offset
+        faceData.append(index)
+
+    if hasMaterial:
+        index = f.material_index + material_offset
+        faceData.append( index )
+
+    if hasFaceVertexUvs:
+        for layer_index, uvs in enumerate(uv_layers):
+            uv = get_uv_indices(faceIndex, uvs, mesh, layer_index)
+            for i in range(nVertices):
+                index = uv[i]
+                faceData.append(index)
+
+    if hasFaceVertexNormals:
+        n = get_normal_indices(f.vertices, normals, mesh)
+        for i in range(nVertices):
+            index = n[i]
+            faceData.append(index)
+
+    if hasFaceVertexColors:
+        c = get_color_indices(faceIndex, colors, mesh)
+        for i in range(nVertices):
+            index = c[i]
+            faceData.append(index)
+
+    return ",".join( map(str, faceData) )
+
+
+# #####################################################
+# Model exporter - normals
+# #####################################################
+
+def extract_vertex_normals(mesh, normals, count):
+    for f in get_faces(mesh):
+        for v in f.vertices:
+
+            normal = mesh.vertices[v].normal
+            key = veckey3d(normal)
+
+            if key not in normals:
+                normals[key] = count
+                count += 1
+
+    return count
+
+def generate_normals(normals, option_normals):
+    if not option_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)
+
+# #####################################################
+# Model exporter - vertex colors
+# #####################################################
+
+def extract_vertex_colors(mesh, colors, count):
+    color_layer = mesh.tessface_vertex_colors.active.data
+
+    for face_index, face in enumerate(get_faces(mesh)):
+
+        face_colors = color_layer[face_index]
+        face_colors = face_colors.color1, face_colors.color2, face_colors.color3, face_colors.color4
+
+        for c in face_colors:
+            key = hexcolor(c)
+            if key not in colors:
+                colors[key] = count
+                count += 1
+
+    return count
+
+def generate_vertex_colors(colors, option_colors):
+    if not option_colors:
+        return ""
+
+    chunks = []
+    for key, index in sorted(colors.items(), key=operator.itemgetter(1)):
+        chunks.append(key)
+
+    return ",".join(generate_vertex_color(c) for c in chunks)
+
+# #####################################################
+# Model exporter - UVs
+# #####################################################
+
+def extract_uvs(mesh, uv_layers, counts):
+    for index, layer in enumerate(mesh.tessface_uv_textures):
+
+        if len(uv_layers) <= index:
+            uvs = {}
+            count = 0
+            uv_layers.append(uvs)
+            counts.append(count)
+        else:
+            uvs = uv_layers[index]
+            count = counts[index]
+
+        uv_layer = layer.data
+
+        for face_index, face in enumerate(get_faces(mesh)):
+
+            for uv_index, uv in enumerate(uv_layer[face_index].uv):
+
+                key = veckey2d(uv)
+                if key not in uvs:
+                    uvs[key] = count
+                    count += 1
+
+        counts[index] = count
+
+    return counts
+
+def generate_uvs(uv_layers, option_uv_coords):
+    if not option_uv_coords:
+        return "[]"
+
+    layers = []
+    for uvs in uv_layers:
+        chunks = []
+        for key, index in sorted(uvs.items(), key=operator.itemgetter(1)):
+            chunks.append(key)
+        layer = ",".join(generate_uv(n) for n in chunks)
+        layers.append(layer)
+
+    return ",".join("[%s]" % n for n in layers)
+
+# ##############################################################################
+# Model exporter - bones
+# (only the first armature will exported)
+# ##############################################################################
+
+def generate_bones(option_bones, flipyz):
+
+    if not option_bones or len(bpy.data.armatures) == 0:
+        return "", 0
+
+    hierarchy = []
+
+    armature = bpy.data.armatures[0]
+
+    TEMPLATE_BONE = '{"parent":%d,"name":"%s","pos":[%g,%g,%g],"rotq":[0,0,0,1]}'
+
+    for bone in armature.bones:
+        if bone.parent == None:
+            if flipyz:
+                joint = TEMPLATE_BONE % (-1, bone.name, bone.head.x, bone.head.z, -bone.head.y)
+                hierarchy.append(joint)
+            else:
+                joint = TEMPLATE_BONE % (-1, bone.name, bone.head.x, bone.head.y, bone.head.z)
+                hierarchy.append(joint)
+        else:
+            index = i = 0
+            for parent in armature.bones:
+                if parent.name == bone.parent.name:
+                    index = i
+                i += 1
+
+            position = bone.head_local - bone.parent.head_local
+
+            if flipyz:
+                joint = TEMPLATE_BONE % (index, bone.name, position.x, position.z, -position.y)
+                hierarchy.append(joint)
+            else:
+                joint = TEMPLATE_BONE % (index, bone.name, position.x, position.y, position.z)
+                hierarchy.append(joint)
+
+    bones_string = ",".join(hierarchy)
+
+    return bones_string, len(armature.bones)
+
+
+# ##############################################################################
+# Model exporter - skin indices and weights
+# ##############################################################################
+
+def generate_indices_and_weights(meshes, option_skinning):
+
+    if not option_skinning or len(bpy.data.armatures) == 0:
+        return "", ""
+
+    indices = []
+    weights = []
+
+    armature = bpy.data.armatures[0]
+
+    for mesh, object in meshes:
+
+        i = 0
+        mesh_index = -1
+
+        # find the original object
+
+        for obj in bpy.data.objects:
+            if obj.name == mesh.name or obj == object:
+                mesh_index = i
+            i += 1
+
+        if mesh_index == -1:
+            print("generate_indices: couldn't find object for mesh", mesh.name)
+            continue
+
+        object = bpy.data.objects[mesh_index]
+
+        for vertex in mesh.vertices:
+
+            # sort bones by influence
+
+            bone_array = []
+
+            for group in vertex.groups:
+                index = group.group
+                weight = group.weight
+
+                bone_array.append( (index, weight) )
+
+            bone_array.sort(key = operator.itemgetter(1), reverse=True)
+
+            # select first N bones
+
+            for i in range(MAX_INFLUENCES):
+
+                if i < len(bone_array):
+                    bone_proxy = bone_array[i]
+
+                    index = bone_proxy[0]
+                    weight = bone_proxy[1]
+
+                    for j, bone in enumerate(armature.bones):
+                        if object.vertex_groups[index].name == bone.name:
+                            indices.append('%d' % j)
+                            weights.append('%g' % weight)
+                            break
+
+                else:
+                    indices.append('0')
+                    weights.append('0')
+
+
+    indices_string = ",".join(indices)
+    weights_string = ",".join(weights)
+
+    return indices_string, weights_string
+
+
+# ##############################################################################
+# Model exporter - skeletal animation
+# (only the first action will exported)
+# ##############################################################################
+
+def generate_animation(option_animation_skeletal, option_frame_step, flipyz):
+
+    if not option_animation_skeletal or len(bpy.data.actions) == 0 or len(bpy.data.armatures) == 0:
+        return ""
+
+    # TODO: Add scaling influences
+
+    action = bpy.data.actions[0]
+    armature = bpy.data.armatures[0]
+
+    parents = []
+    parent_index = -1
+
+    fps = bpy.data.scenes[0].render.fps
+
+    end_frame = action.frame_range[1]
+    start_frame = action.frame_range[0]
+
+    frame_length = end_frame - start_frame
+
+    TEMPLATE_KEYFRAME_FULL  = '{"time":%g,"pos":[%g,%g,%g],"rot":[%g,%g,%g,%g],"scl":[1,1,1]}'
+    TEMPLATE_KEYFRAME       = '{"time":%g,"pos":[%g,%g,%g],"rot":[%g,%g,%g,%g]}'
+    TEMPLATE_KEYFRAME_POS   = '{"time":%g,"pos":[%g,%g,%g]}'
+    TEMPLATE_KEYFRAME_ROT   = '{"time":%g,"rot":[%g,%g,%g,%g]}'
+
+    for hierarchy in armature.bones:
+
+        keys = []
+
+        for frame in range(int(start_frame), int(end_frame / option_frame_step) + 1):
+
+            pos, pchange = position(hierarchy, frame * option_frame_step)
+            rot, rchange = rotation(hierarchy, frame * option_frame_step)
+
+            if flipyz:
+                px, py, pz = pos.x, pos.z, -pos.y
+                rx, ry, rz, rw = rot.x, rot.z, -rot.y, rot.w
+            else:
+                px, py, pz = pos.x, pos.y, pos.z
+                rx, ry, rz, rw = rot.x, rot.y, rot.z, rot.w
+
+            # START-FRAME: needs pos, rot and scl attributes (required frame)
+
+            if frame == int(start_frame):
+
+                time = (frame * option_frame_step - start_frame) / fps
+                keyframe = TEMPLATE_KEYFRAME_FULL % (time, px, py, pz, rx, ry, rz, rw)
+                keys.append(keyframe)
+
+            # END-FRAME: needs pos, rot and scl attributes with animation length (required frame)
+
+            elif frame == int(end_frame / option_frame_step):
+
+                time = frame_length / fps
+                keyframe = TEMPLATE_KEYFRAME_FULL % (time, px, py, pz, rx, ry, rz, rw)
+                keys.append(keyframe)
+
+            # MIDDLE-FRAME: needs only one of the attributes, can be an empty frame (optional frame)
+
+            elif pchange == True or rchange == True:
+
+                time = (frame * option_frame_step - start_frame) / fps
+
+                if pchange == True and rchange == True:
+                    keyframe = TEMPLATE_KEYFRAME % (time, px, py, pz, rx, ry, rz, rw)
+                elif pchange == True:
+                    keyframe = TEMPLATE_KEYFRAME_POS % (time, px, py, pz)
+                elif rchange == True:
+                    keyframe = TEMPLATE_KEYFRAME_ROT % (time, rx, ry, rz, rw)
+
+                keys.append(keyframe)
+
+        keys_string = ",".join(keys)
+        parent = '{"parent":%d,"keys":[%s]}' % (parent_index, keys_string)
+        parent_index += 1
+        parents.append(parent)
+
+    hierarchy_string = ",".join(parents)
+    animation_string = '"name":"%s","fps":%d,"length":%g,"hierarchy":[%s]' % (action.name, fps, (frame_length / fps), hierarchy_string)
+
+    return animation_string
+
+def handle_position_channel(channel, frame, position):
+
+    change = False
+
+    if channel.array_index in [0, 1, 2]:
+        for keyframe in channel.keyframe_points:
+            if keyframe.co[0] == frame:
+                change = True
+
+        value = channel.evaluate(frame)
+
+        if channel.array_index == 0:
+            position.x = value
+
+        if channel.array_index == 1:
+            position.y = value
+
+        if channel.array_index == 2:
+            position.z = value
+
+    return change
+
+def position(bone, frame):
+
+    position = mathutils.Vector((0,0,0))
+    change = False
+
+    action = bpy.data.actions[0]
+    ngroups = len(action.groups)
+
+
+
+    if ngroups > 0:
+
+        index = 0
+
+        for i in range(ngroups):
+            if action.groups[i].name == bone.name:
+                index = i
+
+        for channel in action.groups[index].channels:
+            if "location" in channel.data_path:
+                hasChanged = handle_position_channel(channel, frame, position)
+                change = change or hasChanged
+
+    else:
+
+        bone_label = '"%s"' % bone.name
+
+        for channel in action.fcurves:
+            data_path = channel.data_path
+            if bone_label in data_path and "location" in data_path:
+                hasChanged = handle_position_channel(channel, frame, position)
+                change = change or hasChanged
+
+    position = position * bone.matrix_local.inverted()
+
+    if bone.parent == None:
+
+        position.x += bone.head.x
+        position.y += bone.head.y
+        position.z += bone.head.z
+
+    else:
+
+        parent = bone.parent
+
+        parentInvertedLocalMatrix = parent.matrix_local.inverted()
+        parentHeadTailDiff = parent.tail_local - parent.head_local
+
+        position.x += (bone.head * parentInvertedLocalMatrix).x + parentHeadTailDiff.x
+        position.y += (bone.head * parentInvertedLocalMatrix).y + parentHeadTailDiff.y
+        position.z += (bone.head * parentInvertedLocalMatrix).z + parentHeadTailDiff.z
+
+    return position, change
+
+def handle_rotation_channel(channel, frame, rotation):
+
+    change = False
+
+    if channel.array_index in [0, 1, 2, 3]:
+
+        for keyframe in channel.keyframe_points:
+            if keyframe.co[0] == frame:
+                change = True
+
+        value = channel.evaluate(frame)
+
+        if channel.array_index == 1:
+            rotation.x = value
+
+        elif channel.array_index == 2:
+            rotation.y = value
+
+        elif channel.array_index == 3:
+            rotation.z = value
+
+        elif channel.array_index == 0:
+            rotation.w = value
+
+    return change
+
+def rotation(bone, frame):
+
+    # TODO: calculate rotation also from rotation_euler channels
+
+    rotation = mathutils.Vector((0,0,0,1))
+
+    change = False
+
+    action = bpy.data.actions[0]
+    ngroups = len(action.groups)
+
+    # animation grouped by bones
+
+    if ngroups > 0:
+
+        index = 0
+
+        for i in range(ngroups):
+            if action.groups[i].name == bone.name:
+                index = i
+
+        for channel in action.groups[index].channels:
+            if "quaternion" in channel.data_path:
+                hasChanged = handle_rotation_channel(channel, frame, rotation)
+                change = change or hasChanged
+
+    # animation in raw fcurves
+
+    else:
+
+        bone_label = '"%s"' % bone.name
+
+        for channel in action.fcurves:
+            data_path = channel.data_path
+            if bone_label in data_path and "quaternion" in data_path:
+                hasChanged = handle_rotation_channel(channel, frame, rotation)
+                change = change or hasChanged
+
+    rot3 = rotation.to_3d()
+    rotation.xyz = rot3 * bone.matrix_local.inverted()
+
+    return rotation, change
+
+# #####################################################
+# Model exporter - 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 generate_mtl(materials):
+    """Generate dummy materials.
+    """
+
+    mtl = {}
+    for m in materials:
+        index = materials[m]
+        mtl[m] = {
+            "DbgName": m,
+            "DbgIndex": index,
+            "DbgColor": generate_color(index),
+            "vertexColors" : False
+        }
+    return mtl
+
+def value2string(v):
+    if type(v) == str and v[0:2] != "0x":
+        return '"%s"' % v
+    elif type(v) == bool:
+        return str(v).lower()
+    elif type(v) == list:
+        return "[%s]" % (", ".join(value2string(x) for x in v))
+    return str(v)
+
+def generate_materials(mtl, materials, draw_type):
+    """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]['DbgName'] = m
+        mtl[m]['DbgIndex'] = index
+        mtl[m]['DbgColor'] = generate_color(index)
+
+        if draw_type in [ "BOUNDS", "WIRE" ]:
+            mtl[m]['wireframe'] = True
+            mtl[m]['DbgColor'] = 0xff0000
+
+        mtl_raw = ",\n".join(['\t\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)]), len(mtl_array)
+
+def extract_materials(mesh, scene, option_colors, option_copy_textures, filepath):
+    world = scene.world
+
+    materials = {}
+    for m in mesh.materials:
+        if m:
+            materials[m.name] = {}
+            material = materials[m.name]
+
+            material['colorDiffuse'] = [m.diffuse_intensity * m.diffuse_color[0],
+                                        m.diffuse_intensity * m.diffuse_color[1],
+                                        m.diffuse_intensity * m.diffuse_color[2]]
+
+            material['colorSpecular'] = [m.specular_intensity * m.specular_color[0],
+                                         m.specular_intensity * m.specular_color[1],
+                                         m.specular_intensity * m.specular_color[2]]
+
+            material['colorAmbient'] = [m.ambient * material['colorDiffuse'][0],
+                                        m.ambient * material['colorDiffuse'][1],
+                                        m.ambient * material['colorDiffuse'][2]]
+
+            material['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
+
+            material["specularCoef"] = m.specular_hardness
+
+            textures = guess_material_textures(m)
+
+            handle_texture('diffuse', textures, material, filepath, option_copy_textures)
+            handle_texture('light', textures, material, filepath, option_copy_textures)
+            handle_texture('normal', textures, material, filepath, option_copy_textures)
+            handle_texture('specular', textures, material, filepath, option_copy_textures)
+            handle_texture('bump', textures, material, filepath, option_copy_textures)
+
+            material["vertexColors"] = m.THREE_useVertexColors and option_colors
+
+            # can't really use this reliably to tell apart Phong from Lambert
+            # as Blender defaults to non-zero specular color
+            #if m.specular_intensity > 0.0 and (m.specular_color[0] > 0 or m.specular_color[1] > 0 or m.specular_color[2] > 0):
+            #    material['shading'] = "Phong"
+            #else:
+            #    material['shading'] = "Lambert"
+
+            if textures['normal']:
+                material['shading'] = "Phong"
+            else:
+                material['shading'] = m.THREE_materialType
+
+            material['blending'] = m.THREE_blendingType
+            material['depthWrite'] = m.THREE_depthWrite
+            material['depthTest'] = m.THREE_depthTest
+            material['transparent'] = m.use_transparency
+
+    return materials
+
+def generate_materials_string(mesh, scene, option_colors, draw_type, option_copy_textures, filepath, offset):
+
+    random.seed(42) # to get well defined color order for debug materials
+
+    materials = {}
+    if mesh.materials:
+        for i, m in enumerate(mesh.materials):
+            mat_id = i + offset
+            if m:
+                materials[m.name] = mat_id
+            else:
+                materials["undefined_dummy_%0d" % mat_id] = mat_id
+
+
+    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, option_colors, option_copy_textures, filepath))
+
+    return generate_materials(mtl, materials, draw_type)
+
+def handle_texture(id, textures, material, filepath, option_copy_textures):
+
+    if textures[id]:
+        texName     = 'map%s'       % id.capitalize()
+        repeatName  = 'map%sRepeat' % id.capitalize()
+        wrapName    = 'map%sWrap'   % id.capitalize()
+
+        slot = textures[id]['slot']
+        texture = textures[id]['texture']
+        image = texture.image
+        fname = extract_texture_filename(image)
+        material[texName] = fname
+
+        if option_copy_textures:
+            save_image(image, fname, filepath)
+
+        if texture.repeat_x != 1 or texture.repeat_y != 1:
+            material[repeatName] = [texture.repeat_x, texture.repeat_y]
+
+        if texture.extension == "REPEAT":
+            wrap_x = "repeat"
+            wrap_y = "repeat"
+
+            if texture.use_mirror_x:
+                wrap_x = "mirror"
+            if texture.use_mirror_y:
+                wrap_y = "mirror"
+
+            material[wrapName] = [wrap_x, wrap_y]
+
+        if slot.use_map_normal:
+            if slot.normal_factor != 1.0:
+                if id == "bump":
+                    material['mapBumpScale'] = slot.normal_factor
+                else:
+                    material['mapNormalFactor'] = slot.normal_factor
+
+
+# #####################################################
+# ASCII model generator
+# #####################################################
+
+def generate_ascii_model(meshes, morphs,
+                         scene,
+                         option_vertices,
+                         option_vertices_truncate,
+                         option_faces,
+                         option_normals,
+                         option_uv_coords,
+                         option_materials,
+                         option_colors,
+                         option_bones,
+                         option_skinning,
+                         align_model,
+                         flipyz,
+                         option_scale,
+                         option_copy_textures,
+                         filepath,
+                         option_animation_morph,
+                         option_animation_skeletal,
+                         option_frame_step):
+
+    vertices = []
+
+    vertex_offset = 0
+    vertex_offsets = []
+
+    nnormal = 0
+    normals = {}
+
+    ncolor = 0
+    colors = {}
+
+    nuvs = []
+    uv_layers = []
+
+    nmaterial = 0
+    materials = []
+
+    for mesh, object in meshes:
+
+        vertexUV = len(mesh.uv_textures) > 0
+        vertexColors = len(mesh.vertex_colors) > 0
+
+        mesh_extract_colors = option_colors and vertexColors
+        mesh_extract_uvs = option_uv_coords and vertexUV
+
+        if vertexUV:
+            active_uv_layer = mesh.uv_textures.active
+            if not active_uv_layer:
+                mesh_extract_uvs = False
+
+        if vertexColors:
+            active_col_layer = mesh.vertex_colors.active
+            if not active_col_layer:
+                mesh_extract_colors = False
+
+        vertex_offsets.append(vertex_offset)
+        vertex_offset += len(vertices)
+
+        vertices.extend(mesh.vertices[:])
+
+        if option_normals:
+            nnormal = extract_vertex_normals(mesh, normals, nnormal)
+
+        if mesh_extract_colors:
+            ncolor = extract_vertex_colors(mesh, colors, ncolor)
+
+        if mesh_extract_uvs:
+            nuvs = extract_uvs(mesh, uv_layers, nuvs)
+
+        if option_materials:
+            mesh_materials, nmaterial = generate_materials_string(mesh, scene, mesh_extract_colors, object.draw_type, option_copy_textures, filepath, nmaterial)
+            materials.append(mesh_materials)
+
+
+    morphTargets_string = ""
+    nmorphTarget = 0
+
+    if option_animation_morph:
+        chunks = []
+        for i, morphVertices in enumerate(morphs):
+            morphTarget = '{ "name": "%s_%06d", "vertices": [%s] }' % ("animation", i, morphVertices)
+            chunks.append(morphTarget)
+
+        morphTargets_string = ",\n\t".join(chunks)
+        nmorphTarget = len(morphs)
+
+    if align_model == 1:
+        center(vertices)
+    elif align_model == 2:
+        bottom(vertices)
+    elif align_model == 3:
+        top(vertices)
+
+    faces_string, nfaces = generate_faces(normals, uv_layers, colors, meshes, option_normals, option_colors, option_uv_coords, option_materials, option_faces)
+
+    bones_string, nbone = generate_bones(option_bones, flipyz)
+    indices_string, weights_string = generate_indices_and_weights(meshes, option_skinning)
+
+    materials_string = ",\n\n".join(materials)
+
+    model_string = TEMPLATE_MODEL_ASCII % {
+    "scale" : option_scale,
+
+    "uvs"       : generate_uvs(uv_layers, option_uv_coords),
+    "normals"   : generate_normals(normals, option_normals),
+    "colors"    : generate_vertex_colors(colors, option_colors),
+
+    "materials" : materials_string,
+
+    "vertices" : generate_vertices(vertices, option_vertices_truncate, option_vertices),
+
+    "faces"    : faces_string,
+
+    "morphTargets" : morphTargets_string,
+
+    "bones"     : bones_string,
+    "indices"   : indices_string,
+    "weights"   : weights_string,
+    "animation" : generate_animation(option_animation_skeletal, option_frame_step, flipyz)
+    }
+
+    text = TEMPLATE_FILE_ASCII % {
+    "nvertex"   : len(vertices),
+    "nface"     : nfaces,
+    "nuvs"      : ",".join("%d" % n for n in nuvs),
+    "nnormal"   : nnormal,
+    "ncolor"    : ncolor,
+    "nmaterial" : nmaterial,
+    "nmorphTarget": nmorphTarget,
+    "nbone"     : nbone,
+
+    "model"     : model_string
+    }
+
+
+    return text, model_string
+
+
+# #####################################################
+# Model exporter - export single mesh
+# #####################################################
+
+def extract_meshes(objects, scene, export_single_model, option_scale, flipyz):
+
+    meshes = []
+
+    for object in objects:
+
+        if object.type == "MESH" and object.THREE_exportGeometry:
+
+            # collapse modifiers into mesh
+
+            mesh = object.to_mesh(scene, True, 'RENDER')
+
+            if not mesh:
+                raise Exception("Error, could not get mesh data from object [%s]" % object.name)
+
+            # preserve original name
+
+            mesh.name = object.name
+
+            if export_single_model:
+
+                if flipyz:
+
+                    # that's what Blender's native export_obj.py does to flip YZ
+
+                    X_ROT = mathutils.Matrix.Rotation(-math.pi/2, 4, 'X')
+                    mesh.transform(X_ROT * object.matrix_world)
+
+                else:
+                    mesh.transform(object.matrix_world)
+
+            mesh.calc_normals()
+            mesh.calc_tessface()
+            mesh.transform(mathutils.Matrix.Scale(option_scale, 4))
+            meshes.append([mesh, object])
+
+    return meshes
+
+def generate_mesh_string(objects, scene,
+                option_vertices,
+                option_vertices_truncate,
+                option_faces,
+                option_normals,
+                option_uv_coords,
+                option_materials,
+                option_colors,
+                option_bones,
+                option_skinning,
+                align_model,
+                flipyz,
+                option_scale,
+                export_single_model,
+                option_copy_textures,
+                filepath,
+                option_animation_morph,
+                option_animation_skeletal,
+                option_frame_step):
+
+    meshes = extract_meshes(objects, scene, export_single_model, option_scale, flipyz)
+
+    morphs = []
+
+    if option_animation_morph:
+
+        original_frame = scene.frame_current # save animation state
+
+        scene_frames = range(scene.frame_start, scene.frame_end + 1, option_frame_step)
+
+        for index, frame in enumerate(scene_frames):
+            scene.frame_set(frame, 0.0)
+
+            anim_meshes = extract_meshes(objects, scene, export_single_model, option_scale, flipyz)
+
+            frame_vertices = []
+
+            for mesh, object in anim_meshes:
+                frame_vertices.extend(mesh.vertices[:])
+
+            if index == 0:
+                if align_model == 1:
+                    offset = center(frame_vertices)
+                elif align_model == 2:
+                    offset = bottom(frame_vertices)
+                elif align_model == 3:
+                    offset = top(frame_vertices)
+                else:
+                    offset = False
+            else:
+                if offset:
+                    translate(frame_vertices, offset)
+
+            morphVertices = generate_vertices(frame_vertices, option_vertices_truncate, option_vertices)
+            morphs.append(morphVertices)
+
+            # remove temp meshes
+
+            for mesh, object in anim_meshes:
+                bpy.data.meshes.remove(mesh)
+
+        scene.frame_set(original_frame, 0.0) # restore animation state
+
+
+    text, model_string = generate_ascii_model(meshes, morphs,
+                                scene,
+                                option_vertices,
+                                option_vertices_truncate,
+                                option_faces,
+                                option_normals,
+                                option_uv_coords,
+                                option_materials,
+                                option_colors,
+                                option_bones,
+                                option_skinning,
+                                align_model,
+                                flipyz,
+                                option_scale,
+                                option_copy_textures,
+                                filepath,
+                                option_animation_morph,
+                                option_animation_skeletal,
+                                option_frame_step)
+
+    # remove temp meshes
+
+    for mesh, object in meshes:
+        bpy.data.meshes.remove(mesh)
+
+    return text, model_string
+
+def export_mesh(objects,
+                scene, filepath,
+                option_vertices,
+                option_vertices_truncate,
+                option_faces,
+                option_normals,
+                option_uv_coords,
+                option_materials,
+                option_colors,
+                option_bones,
+                option_skinning,
+                align_model,
+                flipyz,
+                option_scale,
+                export_single_model,
+                option_copy_textures,
+                option_animation_morph,
+                option_animation_skeletal,
+                option_frame_step):
+
+    """Export single mesh"""
+
+    text, model_string = generate_mesh_string(objects,
+                scene,
+                option_vertices,
+                option_vertices_truncate,
+                option_faces,
+                option_normals,
+                option_uv_coords,
+                option_materials,
+                option_colors,
+                option_bones,
+                option_skinning,
+                align_model,
+                flipyz,
+                option_scale,
+                export_single_model,
+                option_copy_textures,
+                filepath,
+                option_animation_morph,
+                option_animation_skeletal,
+                option_frame_step)
+
+    write_file(filepath, text)
+
+    print("writing", filepath, "done")
+
+
+# #####################################################
+# Scene exporter - render elements
+# #####################################################
+
+def generate_vec4(vec):
+    return TEMPLATE_VEC4 % (vec[0], vec[1], vec[2], vec[3])
+
+def generate_vec3(vec):
+    return TEMPLATE_VEC3 % (vec[0], vec[1], vec[2])
+
+def generate_vec2(vec):
+    return TEMPLATE_VEC2 % (vec[0], vec[1])
+
+def generate_hex(number):
+    return TEMPLATE_HEX % number
+
+def generate_string(s):
+    return TEMPLATE_STRING % s
+
+def generate_string_list(src_list):
+    return ", ".join(generate_string(item) for item in src_list)
+
+def generate_section(label, content):
+    return TEMPLATE_SECTION % (label, content)
+
+def get_mesh_filename(mesh):
+    object_id = mesh["data"]["name"]
+    filename = "%s.js" % sanitize(object_id)
+    return filename
+
+def generate_material_id_list(materials):
+    chunks = []
+    for material in materials:
+        chunks.append(material.name)
+
+    return chunks
+
+def generate_group_id_list(obj):
+    chunks = []
+
+    for group in bpy.data.groups:
+        if obj.name in group.objects:
+            chunks.append(group.name)
+
+    return chunks
+
+def generate_bool_property(property):
+    if property:
+        return "true"
+    return "false"
+
+# #####################################################
+# Scene exporter - objects
+# #####################################################
+
+def generate_objects(data):
+    chunks = []
+
+    for obj in data["objects"]:
+
+        if obj.type == "MESH" and obj.THREE_exportGeometry:
+            object_id = obj.name
+
+            if len(obj.modifiers) > 0:
+                geo_name = obj.name
+            else:
+                geo_name = obj.data.name
+
+            geometry_id = "geo_%s" % geo_name
+
+            material_ids = generate_material_id_list(obj.material_slots)
+            group_ids = generate_group_id_list(obj)
+
+            position, quaternion, scale = obj.matrix_world.decompose()
+            rotation = quaternion.to_euler("XYZ")
+
+            # use empty material string for multi-material objects
+            # this will trigger use of MeshFaceMaterial in SceneLoader
+
+            material_string = ""
+            if len(material_ids) == 1:
+                material_string = generate_string_list(material_ids)
+
+            group_string = ""
+            if len(group_ids) > 0:
+                group_string = generate_string_list(group_ids)
+
+            castShadow = obj.THREE_castShadow
+            receiveShadow = obj.THREE_receiveShadow
+            doubleSided = obj.THREE_doubleSided
+
+            visible = True
+
+            geometry_string = generate_string(geometry_id)
+
+            object_string = TEMPLATE_OBJECT % {
+            "object_id"   : generate_string(object_id),
+            "geometry_id" : geometry_string,
+            "group_id"    : group_string,
+            "material_id" : material_string,
+
+            "position"    : generate_vec3(position),
+            "rotation"    : generate_vec3(rotation),
+            "quaternion"  : generate_vec4(quaternion),
+            "scale"       : generate_vec3(scale),
+
+            "castShadow"  : generate_bool_property(castShadow),
+            "receiveShadow"  : generate_bool_property(receiveShadow),
+            "doubleSided"  : generate_bool_property(doubleSided),
+            "visible"      : generate_bool_property(visible)
+            }
+            chunks.append(object_string)
+
+        elif obj.type == "EMPTY" or (obj.type == "MESH" and not obj.THREE_exportGeometry):
+
+            object_id = obj.name
+            group_ids = generate_group_id_list(obj)
+
+            position, quaternion, scale = obj.matrix_world.decompose()
+            rotation = quaternion.to_euler("XYZ")
+
+            group_string = ""
+            if len(group_ids) > 0:
+                group_string = generate_string_list(group_ids)
+
+            object_string = TEMPLATE_EMPTY % {
+            "object_id"   : generate_string(object_id),
+            "group_id"    : group_string,
+
+            "position"    : generate_vec3(position),
+            "rotation"    : generate_vec3(rotation),
+            "quaternion"  : generate_vec4(quaternion),
+            "scale"       : generate_vec3(scale)
+            }
+            chunks.append(object_string)
+
+    return ",\n\n".join(chunks), len(chunks)
+
+# #####################################################
+# Scene exporter - geometries
+# #####################################################
+
+def generate_geometries(data):
+    chunks = []
+
+    geo_set = set()
+
+    for obj in data["objects"]:
+        if obj.type == "MESH" and obj.THREE_exportGeometry:
+
+            if len(obj.modifiers) > 0:
+                name = obj.name
+            else:
+                name = obj.data.name
+
+            if name not in geo_set:
+
+                geometry_id = "geo_%s" % name
+
+                if data["embed_meshes"]:
+
+                    embed_id = "emb_%s" % name
+
+                    geometry_string = TEMPLATE_GEOMETRY_EMBED % {
+                    "geometry_id" : generate_string(geometry_id),
+                    "embed_id"  : generate_string(embed_id)
+                    }
+
+                else:
+
+                    model_filename = os.path.basename(generate_mesh_filename(name, data["filepath"]))
+
+                    geometry_string = TEMPLATE_GEOMETRY_LINK % {
+                    "geometry_id" : generate_string(geometry_id),
+                    "model_file"  : generate_string(model_filename)
+                    }
+
+                chunks.append(geometry_string)
+
+                geo_set.add(name)
+
+    return ",\n\n".join(chunks), len(chunks)
+
+# #####################################################
+# Scene exporter - textures
+# #####################################################
+
+def generate_textures_scene(data):
+    chunks = []
+
+    # TODO: extract just textures actually used by some objects in the scene
+
+    for texture in bpy.data.textures:
+
+        if texture.type == 'IMAGE' and texture.image:
+
+            img = texture.image
+
+            texture_id = img.name
+            texture_file = extract_texture_filename(img)
+
+            if data["copy_textures"]:
+                save_image(img, texture_file, data["filepath"])
+
+            extras = ""
+
+            if texture.repeat_x != 1 or texture.repeat_y != 1:
+                extras += ',\n        "repeat": [%g, %g]' % (texture.repeat_x, texture.repeat_y)
+
+            if texture.extension == "REPEAT":
+                wrap_x = "repeat"
+                wrap_y = "repeat"
+
+                if texture.use_mirror_x:
+                    wrap_x = "mirror"
+                if texture.use_mirror_y:
+                    wrap_y = "mirror"
+
+                extras += ',\n        "wrap": ["%s", "%s"]' % (wrap_x, wrap_y)
+
+            texture_string = TEMPLATE_TEXTURE % {
+            "texture_id"   : generate_string(texture_id),
+            "texture_file" : generate_string(texture_file),
+            "extras"       : extras
+            }
+            chunks.append(texture_string)
+
+    return ",\n\n".join(chunks), len(chunks)
+
+def extract_texture_filename(image):
+    fn = bpy.path.abspath(image.filepath)
+    fn = os.path.normpath(fn)
+    fn_strip = os.path.basename(fn)
+    return fn_strip
+
+def save_image(img, name, fpath):
+    dst_dir = os.path.dirname(fpath)
+    dst_path = os.path.join(dst_dir, name)
+
+    ensure_folder_exist(dst_dir)
+
+    if img.packed_file:
+        img.save_render(dst_path)
+
+    else:
+        src_path = bpy.path.abspath(img.filepath)
+        shutil.copy(src_path, dst_dir)
+
+# #####################################################
+# Scene exporter - materials
+# #####################################################
+
+def extract_material_data(m, option_colors):
+    world = bpy.context.scene.world
+
+    material = { 'name': m.name }
+
+    material['colorDiffuse'] = [m.diffuse_intensity * m.diffuse_color[0],
+                                m.diffuse_intensity * m.diffuse_color[1],
+                                m.diffuse_intensity * m.diffuse_color[2]]
+
+    material['colorSpecular'] = [m.specular_intensity * m.specular_color[0],
+                                 m.specular_intensity * m.specular_color[1],
+                                 m.specular_intensity * m.specular_color[2]]
+
+    material['colorAmbient'] = [m.ambient * material['colorDiffuse'][0],
+                                m.ambient * material['colorDiffuse'][1],
+                                m.ambient * material['colorDiffuse'][2]]
+
+    material['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
+
+    material["specularCoef"] = m.specular_hardness
+
+    material["vertexColors"] = m.THREE_useVertexColors and option_colors
+
+    material['mapDiffuse'] = ""
+    material['mapLight'] = ""
+    material['mapSpecular'] = ""
+    material['mapNormal'] = ""
+    material['mapBump'] = ""
+
+    material['mapNormalFactor'] = 1.0
+    material['mapBumpScale'] = 1.0
+
+    textures = guess_material_textures(m)
+
+    if textures['diffuse']:
+        material['mapDiffuse'] = textures['diffuse']['texture'].image.name
+
+    if textures['light']:
+        material['mapLight'] = textures['light']['texture'].image.name
+
+    if textures['specular']:
+        material['mapSpecular'] = textures['specular']['texture'].image.name
+
+    if textures['normal']:
+        material['mapNormal'] = textures['normal']['texture'].image.name
+        if textures['normal']['slot'].use_map_normal:
+            material['mapNormalFactor'] = textures['normal']['slot'].normal_factor
+
+    if textures['bump']:
+        material['mapBump'] = textures['bump']['texture'].image.name
+        if textures['normal']['slot'].use_map_normal:
+            material['mapBumpScale'] = textures['normal']['slot'].normal_factor
+
+    material['shading'] = m.THREE_materialType
+    material['blending'] = m.THREE_blendingType
+    material['depthWrite'] = m.THREE_depthWrite
+    material['depthTest'] = m.THREE_depthTest
+    material['transparent'] = m.use_transparency
+
+    return material
+
+def guess_material_textures(material):
+    textures = {
+        'diffuse' : None,
+        'light'   : None,
+        'normal'  : None,
+        'specular': None,
+        'bump'    : None
+    }
+
+    # just take first textures of each, for the moment three.js materials can't handle more
+    # assume diffuse comes before lightmap, normalmap has checked flag
+
+    for i in range(len(material.texture_slots)):
+        slot = material.texture_slots[i]
+        if slot:
+            texture = slot.texture
+            if slot.use and texture and texture.type == 'IMAGE':
+
+                # normal map in Blender UI: textures => image sampling => normal map
+
+                if texture.use_normal_map:
+                    textures['normal'] = { "texture": texture, "slot": slot }
+
+                # bump map in Blender UI: textures => influence => geometry => normal
+
+                elif slot.use_map_normal:
+                    textures['bump'] = { "texture": texture, "slot": slot }
+
+                elif slot.use_map_specular or slot.use_map_hardness:
+                    textures['specular'] = { "texture": texture, "slot": slot }
+
+                else:
+                    if not textures['diffuse'] and not slot.blend_type == 'MULTIPLY':
+                        textures['diffuse'] = { "texture": texture, "slot": slot }
+
+                    else:
+                        textures['light'] = { "texture": texture, "slot": slot }
+
+                if textures['diffuse'] and textures['normal'] and textures['light'] and textures['specular'] and textures['bump']:
+                    break
+
+    return textures
+
+def generate_material_string(material):
+
+    material_id = material["name"]
+
+    # default to Lambert
+
+    shading = material.get("shading", "Lambert")
+
+    # normal and bump mapped materials must use Phong
+    # to get all required parameters for normal shader
+
+    if material['mapNormal'] or material['mapBump']:
+        shading = "Phong"
+
+    type_map = {
+    "Lambert"   : "MeshLambertMaterial",
+    "Phong"     : "MeshPhongMaterial"
+    }
+
+    material_type = type_map.get(shading, "MeshBasicMaterial")
+
+    parameters = '"color": %d' % rgb2int(material["colorDiffuse"])
+    parameters += ', "opacity": %.2g' % material["transparency"]
+
+    if shading == "Phong":
+        parameters += ', "ambient": %d' % rgb2int(material["colorAmbient"])
+        parameters += ', "specular": %d' % rgb2int(material["colorSpecular"])
+        parameters += ', "shininess": %.1g' % material["specularCoef"]
+
+    colorMap = material['mapDiffuse']
+    lightMap = material['mapLight']
+    specularMap = material['mapSpecular']
+    normalMap = material['mapNormal']
+    bumpMap = material['mapBump']
+    normalMapFactor = material['mapNormalFactor']
+    bumpMapScale = material['mapBumpScale']
+
+    if colorMap:
+        parameters += ', "map": %s' % generate_string(colorMap)
+    if lightMap:
+        parameters += ', "lightMap": %s' % generate_string(lightMap)
+    if specularMap:
+        parameters += ', "specularMap": %s' % generate_string(specularMap)
+    if normalMap:
+        parameters += ', "normalMap": %s' % generate_string(normalMap)
+    if bumpMap:
+        parameters += ', "bumpMap": %s' % generate_string(bumpMap)
+
+    if normalMapFactor != 1.0:
+        parameters += ', "normalMapFactor": %g' % normalMapFactor
+
+    if bumpMapScale != 1.0:
+        parameters += ', "bumpMapScale": %g' % bumpMapScale
+
+    if material['vertexColors']:
+        parameters += ', "vertexColors": "vertex"'
+
+    if material['transparent']:
+        parameters += ', "transparent": true'
+
+    parameters += ', "blending": "%s"' % material['blending']
+
+    if not material['depthWrite']:
+        parameters += ', "depthWrite": false'
+
+    if not material['depthTest']:
+        parameters += ', "depthTest": false'
+
+
+    material_string = TEMPLATE_MATERIAL_SCENE % {
+    "material_id" : generate_string(material_id),
+    "type"        : generate_string(material_type),
+    "parameters"  : parameters
+    }
+
+    return material_string
+
+def generate_materials_scene(data):
+    chunks = []
+
+    # TODO: extract just materials actually used by some objects in the scene
+
+    for m in bpy.data.materials:
+        material = extract_material_data(m, data["use_colors"])
+        material_string = generate_material_string(material)
+        chunks.append(material_string)
+
+    return ",\n\n".join(chunks), len(chunks)
+
+# #####################################################
+# Scene exporter - cameras
+# #####################################################
+
+def generate_cameras(data):
+    chunks = []
+
+    if data["use_cameras"]:
+
+        cams = bpy.data.objects
+        cams = [ob for ob in cams if (ob.type == 'CAMERA' and ob.select)]
+
+        if not cams:
+            camera = DEFAULTS["camera"]
+
+            if camera["type"] == "PerspectiveCamera":
+
+                camera_string = TEMPLATE_CAMERA_PERSPECTIVE % {
+                "camera_id" : generate_string(camera["name"]),
+                "fov"       : camera["fov"],
+                "aspect"    : camera["aspect"],
+                "near"      : camera["near"],
+                "far"       : camera["far"],
+                "position"  : generate_vec3(camera["position"]),
+                "target"    : generate_vec3(camera["target"])
+                }
+
+            elif camera["type"] == "OrthographicCamera":
+
+                camera_string = TEMPLATE_CAMERA_ORTHO % {
+                "camera_id" : generate_string(camera["name"]),
+                "left"      : camera["left"],
+                "right"     : camera["right"],
+                "top"       : camera["top"],
+                "bottom"    : camera["bottom"],
+                "near"      : camera["near"],
+                "far"       : camera["far"],
+                "position"  : generate_vec3(camera["position"]),
+                "target"    : generate_vec3(camera["target"])
+                }
+
+            chunks.append(camera_string)
+
+        else:
+
+            for cameraobj in cams:
+                camera = bpy.data.cameras[cameraobj.name]
+
+                # TODO:
+                #   Support more than perspective camera
+                #   Calculate a target/lookat
+                #   Get correct aspect ratio
+                if camera.id_data.type == "PERSP":
+
+                    camera_string = TEMPLATE_CAMERA_PERSPECTIVE % {
+                    "camera_id" : generate_string(camera.name),
+                    "fov"       : (camera.angle / 3.14) * 180.0,
+                    "aspect"    : 1.333,
+                    "near"      : camera.clip_start,
+                    "far"       : camera.clip_end,
+                    "position"  : generate_vec3([cameraobj.location[0], -cameraobj.location[1], cameraobj.location[2]]),
+                    "target"    : generate_vec3([0, 0, 0])
+                    }
+
+                chunks.append(camera_string)
+
+    return ",\n\n".join(chunks), len(chunks)
+
+# #####################################################
+# Scene exporter - lights
+# #####################################################
+
+def generate_lights(data):
+    chunks = []
+
+    if data["use_lights"]:
+
+        lights = data.get("lights", [])
+        if not lights:
+            lights.append(DEFAULTS["light"])
+
+        for light in lights:
+
+            if light["type"] == "DirectionalLight":
+                light_string = TEMPLATE_LIGHT_DIRECTIONAL % {
+                "light_id"      : generate_string(light["name"]),
+                "direction"     : generate_vec3(light["direction"]),
+                "color"         : rgb2int(light["color"]),
+                "intensity"     : light["intensity"]
+                }
+
+            elif light["type"] == "PointLight":
+                light_string = TEMPLATE_LIGHT_POINT % {
+                "light_id"      : generate_string(light["name"]),
+                "position"      : generate_vec3(light["position"]),
+                "color"         : rgb2int(light["color"]),
+                "intensity"     : light["intensity"]
+                }
+
+            chunks.append(light_string)
+
+    return ",\n\n".join(chunks), len(chunks)
+
+# #####################################################
+# Scene exporter - embedded meshes
+# #####################################################
+
+def generate_embeds(data):
+
+    if data["embed_meshes"]:
+
+        chunks = []
+
+        for e in data["embeds"]:
+
+            embed = '"emb_%s": {%s}' % (e, data["embeds"][e])
+            chunks.append(embed)
+
+        return ",\n\n".join(chunks)
+
+    return ""
+
+# #####################################################
+# Scene exporter - generate ASCII scene
+# #####################################################
+
+def generate_ascii_scene(data):
+
+    objects, nobjects = generate_objects(data)
+    geometries, ngeometries = generate_geometries(data)
+    textures, ntextures = generate_textures_scene(data)
+    materials, nmaterials = generate_materials_scene(data)
+    lights, nlights = generate_lights(data)
+    cameras, ncameras = generate_cameras(data)
+
+    embeds = generate_embeds(data)
+
+    if nlights > 0:
+        if nobjects > 0:
+            objects = objects + ",\n\n" + lights
+        else:
+            objects = lights
+        nobjects += nlights
+
+    if ncameras > 0:
+        if nobjects > 0:
+            objects = objects + ",\n\n" + cameras
+        else:
+            objects = cameras
+        nobjects += ncameras
+
+    basetype = "relativeTo"
+
+    if data["base_html"]:
+        basetype += "HTML"
+    else:
+        basetype += "Scene"
+
+    sections = [
+    ["objects",    objects],
+    ["geometries", geometries],
+    ["textures",   textures],
+    ["materials",  materials],
+    ["embeds",     embeds]
+    ]
+
+    chunks = []
+    for label, content in sections:
+        if content:
+            chunks.append(generate_section(label, content))
+
+    sections_string = "\n".join(chunks)
+
+    default_camera = ""
+    if data["use_cameras"]:
+        cams = [ob for ob in bpy.data.objects if (ob.type == 'CAMERA' and ob.select)]
+        if not cams:
+            default_camera = "default_camera"
+        else:
+            default_camera = cams[0].name
+
+    parameters = {
+    "fname"     : data["source_file"],
+
+    "sections"  : sections_string,
+
+    "bgcolor"   : generate_vec3(DEFAULTS["bgcolor"]),
+    "bgalpha"   : DEFAULTS["bgalpha"],
+    "defcamera" :  generate_string(default_camera),
+
+    "nobjects"      : nobjects,
+    "ngeometries"   : ngeometries,
+    "ntextures"     : ntextures,
+    "basetype"      : generate_string(basetype),
+    "nmaterials"    : nmaterials,
+
+    "position"      : generate_vec3(DEFAULTS["position"]),
+    "rotation"      : generate_vec3(DEFAULTS["rotation"]),
+    "scale"         : generate_vec3(DEFAULTS["scale"])
+    }
+
+    text = TEMPLATE_SCENE_ASCII % parameters
+
+    return text
+
+def export_scene(scene, filepath, flipyz, option_colors, option_lights, option_cameras, option_embed_meshes, embeds, option_url_base_html, option_copy_textures):
+
+    source_file = os.path.basename(bpy.data.filepath)
+
+    # objects are contained in scene and linked groups
+    objects = []
+
+    # get scene objects
+    sceneobjects = scene.objects
+    for obj in sceneobjects:
+      objects.append(obj)
+
+    # get linked group objcts
+    for group in bpy.data.groups:
+       for object in group.objects:
+          objects.append(object)
+
+    scene_text = ""
+    data = {
+    "scene"        : scene,
+    "objects"      : objects,
+    "embeds"       : embeds,
+    "source_file"  : source_file,
+    "filepath"     : filepath,
+    "flipyz"       : flipyz,
+    "use_colors"   : option_colors,
+    "use_lights"   : option_lights,
+    "use_cameras"  : option_cameras,
+    "embed_meshes" : option_embed_meshes,
+    "base_html"    : option_url_base_html,
+    "copy_textures": option_copy_textures
+    }
+    scene_text += generate_ascii_scene(data)
+
+    write_file(filepath, scene_text)
+
+# #####################################################
+# Main
+# #####################################################
+
+def save(operator, context, filepath = "",
+         option_flip_yz = True,
+         option_vertices = True,
+         option_vertices_truncate = False,
+         option_faces = True,
+         option_normals = True,
+         option_uv_coords = True,
+         option_materials = True,
+         option_colors = True,
+         option_bones = True,
+         option_skinning = True,
+         align_model = 0,
+         option_export_scene = False,
+         option_lights = False,
+         option_cameras = False,
+         option_scale = 1.0,
+         option_embed_meshes = True,
+         option_url_base_html = False,
+         option_copy_textures = False,
+         option_animation_morph = False,
+         option_animation_skeletal = False,
+         option_frame_step = 1,
+         option_all_meshes = True):
+
+    #print("URL TYPE", option_url_base_html)
+
+    filepath = ensure_extension(filepath, '.js')
+
+    scene = context.scene
+
+    if scene.objects.active:
+        bpy.ops.object.mode_set(mode='OBJECT')
+
+    if option_all_meshes:
+        sceneobjects = scene.objects
+    else:
+        sceneobjects = context.selected_objects
+
+    # objects are contained in scene and linked groups
+    objects = []
+
+    # get scene objects
+    for obj in sceneobjects:
+      objects.append(obj)
+
+    # get objects in linked groups
+    for group in bpy.data.groups:
+       for object in group.objects:
+          objects.append(object)
+
+    if option_export_scene:
+
+        geo_set = set()
+        embeds = {}
+
+        for object in objects:
+            if object.type == "MESH" and object.THREE_exportGeometry:
+
+                # create extra copy of geometry with applied modifiers
+                # (if they exist)
+
+                if len(object.modifiers) > 0:
+                    name = object.name
+
+                # otherwise can share geometry
+
+                else:
+                    name = object.data.name
+
+                if name not in geo_set:
+
+                    if option_embed_meshes:
+
+                        text, model_string = generate_mesh_string([object], scene,
+                                                        option_vertices,
+                                                        option_vertices_truncate,
+                                                        option_faces,
+                                                        option_normals,
+                                                        option_uv_coords,
+                                                        option_materials,
+                                                        option_colors,
+                                                        option_bones,
+                                                        option_skinning,
+                                                        False,          # align_model
+                                                        option_flip_yz,
+                                                        option_scale,
+                                                        False,          # export_single_model
+                                                        False,          # option_copy_textures
+                                                        filepath,
+                                                        option_animation_morph,
+                                                        option_animation_skeletal,
+                                                        option_frame_step)
+
+                        embeds[name] = model_string
+
+                    else:
+
+                        fname = generate_mesh_filename(name, filepath)
+                        export_mesh([object], scene,
+                                    fname,
+                                    option_vertices,
+                                    option_vertices_truncate,
+                                    option_faces,
+                                    option_normals,
+                                    option_uv_coords,
+                                    option_materials,
+                                    option_colors,
+                                    option_bones,
+                                    option_skinning,
+                                    False,          # align_model
+                                    option_flip_yz,
+                                    option_scale,
+                                    False,          # export_single_model
+                                    option_copy_textures,
+                                    option_animation_morph,
+                                    option_animation_skeletal,
+                                    option_frame_step)
+
+                    geo_set.add(name)
+
+        export_scene(scene, filepath,
+                     option_flip_yz,
+                     option_colors,
+                     option_lights,
+                     option_cameras,
+                     option_embed_meshes,
+                     embeds,
+                     option_url_base_html,
+                     option_copy_textures)
+
+    else:
+
+        export_mesh(objects, scene, filepath,
+                    option_vertices,
+                    option_vertices_truncate,
+                    option_faces,
+                    option_normals,
+                    option_uv_coords,
+                    option_materials,
+                    option_colors,
+                    option_bones,
+                    option_skinning,
+                    align_model,
+                    option_flip_yz,
+                    option_scale,
+                    True,            # export_single_model
+                    option_copy_textures,
+                    option_animation_morph,
+                    option_animation_skeletal,
+                    option_frame_step)
+
+    return {'FINISHED'}

+ 633 - 0
utils/exporters/blender/2.65/scripts/addons/io_mesh_threejs/import_threejs.py

@@ -0,0 +1,633 @@
+# ##### 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 #####
+
+"""
+Blender importer for Three.js (ASCII JSON format).
+
+"""
+
+import os
+import time
+import json
+import bpy
+import mathutils
+from mathutils.geometry import tessellate_polygon
+from bpy_extras.image_utils import load_image
+
+# #####################################################
+# Generators
+# #####################################################
+def setColor(c, t):
+    c.r = t[0]
+    c.g = t[1]
+    c.b = t[2]
+
+def create_texture(filename, modelpath):
+    name = filename
+    texture = bpy.data.textures.new(name, type='IMAGE')
+
+    image = load_image(filename, modelpath)
+    has_data = False
+
+    if image:
+        texture.image = image
+        has_data = image.has_data
+
+    return texture
+
+def create_materials(data, modelpath):
+    materials = []
+    materials_data = data.get("materials", [])
+
+    for i, m in enumerate(materials_data):
+
+        name = m.get("DbgName", "material_%d" % i)
+
+        colorAmbient = m.get("colorAmbient", None)
+        colorDiffuse = m.get("colorDiffuse", None)
+        colorSpecular = m.get("colorSpecular", None)
+        alpha = m.get("transparency", 1.0)
+        specular_hardness = m.get("specularCoef", 0)
+
+        mapDiffuse = m.get("mapDiffuse", None)
+        mapLightmap = m.get("mapLightmap", None)
+
+        vertexColorsType = m.get("vertexColors", False)
+
+        useVertexColors = False
+        if vertexColorsType:
+            useVertexColors = True
+
+        material = bpy.data.materials.new(name)
+
+        material.THREE_useVertexColors = useVertexColors
+
+        if colorDiffuse:
+            setColor(material.diffuse_color, colorDiffuse)
+            material.diffuse_intensity = 1.0
+
+        if colorSpecular:
+            setColor(material.specular_color, colorSpecular)
+            material.specular_intensity = 1.0
+
+        if alpha < 1.0:
+            material.alpha = alpha
+            material.use_transparency = True
+
+        if specular_hardness:
+            material.specular_hardness = specular_hardness
+
+        if mapDiffuse:
+            texture = create_texture(mapDiffuse, modelpath)
+            mtex = material.texture_slots.add()
+            mtex.texture = texture
+            mtex.texture_coords = 'UV'
+            mtex.use = True
+            mtex.use_map_color_diffuse = True
+
+            material.active_texture = texture
+
+        materials.append(material)
+
+    return materials
+
+def create_mesh_object(name, vertices, materials, face_data, flipYZ, recalculate_normals):
+
+    faces         = face_data["faces"]
+    vertexNormals = face_data["vertexNormals"]
+    vertexColors  = face_data["vertexColors"]
+    vertexUVs     = face_data["vertexUVs"]
+    faceMaterials = face_data["materials"]
+    faceColors    = face_data["faceColors"]
+
+    edges = []
+
+    # Create a new mesh
+
+    me = bpy.data.meshes.new(name)
+    me.from_pydata(vertices, edges, faces)
+
+    # Handle normals
+
+    if not recalculate_normals:
+        me.update(calc_edges = True)
+
+    if face_data["hasVertexNormals"]:
+
+        print("setting vertex normals")
+
+        for fi in range(len(faces)):
+
+            if vertexNormals[fi]:
+
+                #print("setting face %i with %i vertices" % (fi, len(normals[fi])))
+
+                # if me.update() is called after setting vertex normals
+                # setting face.use_smooth overrides these normals
+                #  - this fixes weird shading artefacts (seems to come from sharing
+                #    of vertices between faces, didn't find a way how to set vertex normals
+                #    per face use of vertex as opposed to per vertex),
+                #  - probably this just overrides all custom vertex normals
+                #  - to preserve vertex normals from the original data
+                #    call me.update() before setting them
+
+                me.faces[fi].use_smooth = True
+
+                if not recalculate_normals:
+                    for j in range(len(vertexNormals[fi])):
+
+                        vertexNormal = vertexNormals[fi][j]
+
+                        x = vertexNormal[0]
+                        y = vertexNormal[1]
+                        z = vertexNormal[2]
+
+                        if flipYZ:
+                            tmp = y
+                            y = -z
+                            z = tmp
+
+                            # flip normals (this make them look consistent with the original before export)
+
+                            #x = -x
+                            #y = -y
+                            #z = -z
+
+                        vi = me.faces[fi].vertices[j]
+
+                        me.vertices[vi].normal.x = x
+                        me.vertices[vi].normal.y = y
+                        me.vertices[vi].normal.z = z
+
+    if recalculate_normals:
+        me.update(calc_edges = True)
+
+    # Handle colors
+
+    if face_data["hasVertexColors"]:
+
+        print("setting vertex colors")
+
+        me.vertex_colors.new("vertex_color_layer_0")
+
+        for fi in range(len(faces)):
+
+            if vertexColors[fi]:
+
+                face_colors = me.vertex_colors[0].data[fi]
+                face_colors = face_colors.color1, face_colors.color2, face_colors.color3, face_colors.color4
+
+                for vi in range(len(vertexColors[fi])):
+
+                    r = vertexColors[fi][vi][0]
+                    g = vertexColors[fi][vi][1]
+                    b = vertexColors[fi][vi][2]
+
+                    face_colors[vi].r = r
+                    face_colors[vi].g = g
+                    face_colors[vi].b = b
+
+    elif face_data["hasFaceColors"]:
+
+        print("setting vertex colors from face colors")
+
+        me.vertex_colors.new("vertex_color_layer_0")
+
+        for fi in range(len(faces)):
+
+            if faceColors[fi]:
+
+                r = faceColors[fi][0]
+                g = faceColors[fi][1]
+                b = faceColors[fi][2]
+
+                face_colors = me.vertex_colors[0].data[fi]
+                face_colors = face_colors.color1, face_colors.color2, face_colors.color3, face_colors.color4
+
+                for vi in range(len(faces[fi])):
+
+                    face_colors[vi].r = r
+                    face_colors[vi].g = g
+                    face_colors[vi].b = b
+
+    # Handle uvs
+
+    if face_data["hasVertexUVs"]:
+
+        print("setting vertex uvs")
+
+        for li, layer in enumerate(vertexUVs):
+
+            me.uv_textures.new("uv_layer_%d" % li)
+
+            for fi in range(len(faces)):
+
+                if layer[fi]:
+
+                    uv_face = me.uv_textures[li].data[fi]
+                    face_uvs = uv_face.uv1, uv_face.uv2, uv_face.uv3, uv_face.uv4
+
+                    for vi in range(len(layer[fi])):
+
+                        u = layer[fi][vi][0]
+                        v = layer[fi][vi][1]
+
+                        face_uvs[vi].x = u
+                        face_uvs[vi].y = v
+
+                    active_texture = materials[faceMaterials[fi]].active_texture
+
+                    if active_texture:
+                        uv_face.image = active_texture.image
+
+
+    # Handle materials # 1
+
+    if face_data["hasMaterials"]:
+
+
+        print("setting materials (mesh)")
+
+        for m in materials:
+
+            me.materials.append(m)
+
+        print("setting materials (faces)")
+
+        for fi in range(len(faces)):
+
+            if faceMaterials[fi] >= 0:
+
+                me.faces[fi].material_index = faceMaterials[fi]
+
+    # Create a new object
+
+    ob = bpy.data.objects.new(name, me)
+    ob.data = me                                # link the mesh data to the object
+
+
+    scene = bpy.context.scene                   # get the current scene
+    scene.objects.link(ob)                      # link the object into the scene
+
+    ob.location = scene.cursor_location         # position object at 3d-cursor
+
+
+# #####################################################
+# Faces
+# #####################################################
+
+def extract_faces(data):
+
+    result = {
+    "faces"         : [],
+    "materials"     : [],
+    "faceUVs"       : [],
+    "vertexUVs"     : [],
+    "faceNormals"   : [],
+    "vertexNormals" : [],
+    "faceColors"    : [],
+    "vertexColors"  : [],
+
+    "hasVertexNormals"  : False,
+    "hasVertexUVs"      : False,
+    "hasVertexColors"   : False,
+    "hasFaceColors"     : False,
+    "hasMaterials"      : False
+    }
+
+    faces = data.get("faces", [])
+    normals = data.get("normals", [])
+    colors = data.get("colors", [])
+
+    offset = 0
+    zLength = len(faces)
+
+    # disregard empty arrays
+
+    nUvLayers = 0
+
+    for layer in data["uvs"]:
+
+        if len(layer) > 0:
+            nUvLayers += 1
+            result["faceUVs"].append([])
+            result["vertexUVs"].append([])
+
+
+    while ( offset < zLength ):
+
+        type = faces[ offset ]
+        offset += 1
+
+        isQuad          	= isBitSet( type, 0 )
+        hasMaterial         = isBitSet( type, 1 )
+        hasFaceUv           = isBitSet( type, 2 )
+        hasFaceVertexUv     = isBitSet( type, 3 )
+        hasFaceNormal       = isBitSet( type, 4 )
+        hasFaceVertexNormal = isBitSet( type, 5 )
+        hasFaceColor	    = isBitSet( type, 6 )
+        hasFaceVertexColor  = isBitSet( type, 7 )
+
+        #print("type", type, "bits", isQuad, hasMaterial, hasFaceUv, hasFaceVertexUv, hasFaceNormal, hasFaceVertexNormal, hasFaceColor, hasFaceVertexColor)
+
+        result["hasVertexUVs"] = result["hasVertexUVs"] or hasFaceVertexUv
+        result["hasVertexNormals"] = result["hasVertexNormals"] or hasFaceVertexNormal
+        result["hasVertexColors"] = result["hasVertexColors"] or hasFaceVertexColor
+        result["hasFaceColors"] = result["hasFaceColors"] or hasFaceColor
+        result["hasMaterials"] = result["hasMaterials"] or hasMaterial
+
+        # vertices
+
+        if isQuad:
+
+            a = faces[ offset ]
+            offset += 1
+
+            b = faces[ offset ]
+            offset += 1
+
+            c = faces[ offset ]
+            offset += 1
+
+            d = faces[ offset ]
+            offset += 1
+
+            face = [a, b, c, d]
+
+            nVertices = 4
+
+        else:
+
+            a = faces[ offset ]
+            offset += 1
+
+            b = faces[ offset ]
+            offset += 1
+
+            c = faces[ offset ]
+            offset += 1
+
+            face = [a, b, c]
+
+            nVertices = 3
+
+        result["faces"].append(face)
+
+        # material
+
+        if hasMaterial:
+
+            materialIndex = faces[ offset ]
+            offset += 1
+
+        else:
+
+            materialIndex = -1
+
+        result["materials"].append(materialIndex)
+
+        # uvs
+
+        for i in range(nUvLayers):
+
+            faceUv = None
+
+            if hasFaceUv:
+
+                uvLayer = data["uvs"][ i ]
+
+                uvIndex = faces[ offset ]
+                offset += 1
+
+                u = uvLayer[ uvIndex * 2 ]
+                v = uvLayer[ uvIndex * 2 + 1 ]
+
+                faceUv = [u, v]
+
+            result["faceUVs"][i].append(faceUv)
+
+
+            if hasFaceVertexUv:
+
+                uvLayer = data["uvs"][ i ]
+
+                vertexUvs = []
+
+                for j in range(nVertices):
+
+                    uvIndex = faces[ offset ]
+                    offset += 1
+
+                    u = uvLayer[ uvIndex * 2 ]
+                    v = uvLayer[ uvIndex * 2 + 1 ]
+
+                    vertexUvs.append([u, v])
+
+            result["vertexUVs"][i].append(vertexUvs)
+
+
+        if hasFaceNormal:
+
+            normalIndex = faces[ offset ] * 3
+            offset += 1
+
+            x = normals[ normalIndex ]
+            y = normals[ normalIndex + 1 ]
+            z = normals[ normalIndex + 2 ]
+
+            faceNormal = [x, y, z]
+
+        else:
+
+            faceNormal = None
+
+        result["faceNormals"].append(faceNormal)
+
+
+        if hasFaceVertexNormal:
+
+            vertexNormals = []
+
+            for j in range(nVertices):
+
+                normalIndex = faces[ offset ] * 3
+                offset += 1
+
+                x = normals[ normalIndex ]
+                y = normals[ normalIndex + 1 ]
+                z = normals[ normalIndex + 2 ]
+
+                vertexNormals.append( [x, y, z] )
+
+
+        else:
+
+            vertexNormals = None
+
+        result["vertexNormals"].append(vertexNormals)
+
+
+        if hasFaceColor:
+
+            colorIndex = faces[ offset ]
+            offset += 1
+
+            faceColor = hexToTuple( colors[ colorIndex ] )
+
+        else:
+
+            faceColor = None
+
+        result["faceColors"].append(faceColor)
+
+
+        if hasFaceVertexColor:
+
+            vertexColors = []
+
+            for j in range(nVertices):
+
+                colorIndex = faces[ offset ]
+                offset += 1
+
+                color = hexToTuple( colors[ colorIndex ] )
+                vertexColors.append( color )
+
+        else:
+
+            vertexColors = None
+
+        result["vertexColors"].append(vertexColors)
+
+
+    return result
+
+# #####################################################
+# Utils
+# #####################################################
+
+def hexToTuple( hexColor ):
+    r = (( hexColor >> 16 ) & 0xff) / 255.0
+    g = (( hexColor >> 8 ) & 0xff) / 255.0
+    b = ( hexColor & 0xff) / 255.0
+    return (r, g, b)
+
+def isBitSet(value, position):
+    return value & ( 1 << position )
+
+def splitArray(data, chunkSize):
+    result = []
+    chunk = []
+    for i in range(len(data)):
+        if i > 0 and i % chunkSize == 0:
+            result.append(chunk)
+            chunk = []
+        chunk.append(data[i])
+    result.append(chunk)
+    return result
+
+
+def extract_json_string(text):
+    marker_begin = "var model ="
+    marker_end = "postMessage"
+
+    start = text.find(marker_begin) + len(marker_begin)
+    end = text.find(marker_end)
+    end = text.rfind("}", start, end)
+    return text[start:end+1].strip()
+
+def get_name(filepath):
+    return os.path.splitext(os.path.basename(filepath))[0]
+
+def get_path(filepath):
+    return os.path.dirname(filepath)
+
+# #####################################################
+# Parser
+# #####################################################
+
+def load(operator, context, filepath, option_flip_yz = True, recalculate_normals = True, option_worker = False):
+
+    print('\nimporting %r' % filepath)
+
+    time_main = time.time()
+
+    print("\tparsing JSON file...")
+
+    time_sub = time.time()
+
+    file = open(filepath, 'rU')
+    rawcontent = file.read()
+    file.close()
+
+    if option_worker:
+        json_string = extract_json_string(rawcontent)
+    else:
+        json_string = rawcontent
+    data = json.loads( json_string )
+
+    time_new = time.time()
+
+    print('parsing %.4f sec' % (time_new - time_sub))
+
+    time_sub = time_new
+
+    # flip YZ
+
+    vertices = splitArray(data["vertices"], 3)
+
+    if option_flip_yz:
+        vertices[:] = [(v[0], -v[2], v[1]) for v in vertices]
+
+    # extract faces
+
+    face_data = extract_faces(data)
+
+    # deselect all
+
+    bpy.ops.object.select_all(action='DESELECT')
+
+    nfaces = len(face_data["faces"])
+    nvertices = len(vertices)
+    nnormals = len(data.get("normals", [])) / 3
+    ncolors = len(data.get("colors", [])) / 3
+    nuvs = len(data.get("uvs", [])) / 2
+    nmaterials = len(data.get("materials", []))
+
+    print('\tbuilding geometry...\n\tfaces:%i, vertices:%i, vertex normals: %i, vertex uvs: %i, vertex colors: %i, materials: %i ...' % (
+        nfaces, nvertices, nnormals, nuvs, ncolors, nmaterials ))
+
+    # Create materials
+
+    materials = create_materials(data, get_path(filepath))
+
+    # Create new obj
+
+    create_mesh_object(get_name(filepath), vertices, materials, face_data, option_flip_yz, recalculate_normals)
+
+    scene = bpy.context.scene
+    scene.update()
+
+    time_new = time.time()
+
+    print('finished importing: %r in %.4f sec.' % (filepath, (time_new - time_main)))
+    return {'FINISHED'}
+
+
+if __name__ == "__main__":
+    register()