Browse Source

Added Blender exporter from Reattiva.

Lasse Öörni 12 years ago
parent
commit
4741b3cf08

+ 1 - 0
Docs/Urho3D.dox

@@ -66,6 +66,7 @@ Urho3D development, contributions and bugfixes by:
 - Firegorilla
 - Magic.Lixin
 - amadeus_osa
+- reattiva
 - skaiware
 
 Urho3D is greatly inspired by OGRE (http://www.ogre3d.org/) and Horde3D (http://www.horde3d.org/). Additional inspiration & research used:

+ 1 - 0
Readme.txt

@@ -28,6 +28,7 @@ Urho3D development, contributions and bugfixes by:
 - Firegorilla
 - Magic.Lixin
 - amadeus_osa
+- reattiva
 - skaiware
 
 Urho3D is greatly inspired by OGRE (http://www.ogre3d.org) and Horde3D

+ 13 - 0
Source/Extras/BlenderExporter/Readme.txt

@@ -0,0 +1,13 @@
+Installation instructions (from the Urho3D Google discussion group)
+
+> extract the folder 'io_mesh_urho' from the zip to the blender scripts/addons folder:
+Windows 7 - C:\Users\%username%\AppData\Roaming\Blender Foundation\Blender\2.xx\scripts\addons
+Windows XP - C:\Documents and Settings\%username%\Application Data\Blender Foundation\Blender\2.xx\scripts\addons
+Linux - /home/$user/.config/blender/$version/scripts/addons
+
+> start Blender
+
+> enable the addon: menu File > User preferences > Addons > Import-Export > Urho3D export (tick the checkbox on the right)
+> if you can't check the box, search the console for errors (menu Window > toggle System Console)
+
+> you'll find a new panel in Render page (at the bottom) of the Properties window

+ 844 - 0
Source/Extras/BlenderExporter/io_mesh_urho/__init__.py

@@ -0,0 +1,844 @@
+
+#
+# This script is licensed as public domain.
+#
+
+# http://www.blender.org/documentation/blender_python_api_2_57_release/bpy.props.html
+# http://www.blender.org/documentation/blender_python_api_2_59_0/bpy.props.html
+# http://www.blender.org/documentation/blender_python_api_2_66_4/bpy.props.html
+# http://www.blender.org/documentation/blender_python_api_2_57_release/bpy.types.Panel.html
+# http://www.blender.org/documentation/blender_python_api_2_57_release/bpy.types.PropertyGroup.html
+# http://www.blender.org/documentation/blender_python_api_2_66_4/bpy.types.WindowManager.html
+# http://wiki.blender.org/index.php/Dev:2.5/Py/Scripts/Guidelines/Layouts
+# http://wiki.blender.org/index.php/Dev:2.5/Py/Scripts/Cookbook/Code_snippets/Properties
+# http://wiki.blender.org/index.php/Dev:2.5/Py/Scripts/Cookbook/Code_snippets/Interface
+# http://wiki.blender.org/index.php/Dev:IT/2.5/Py/Scripts/Cookbook/Code_snippets/Interface
+# http://wiki.blender.org/index.php/Dev:IT/2.5/Py/Scripts/Cookbook/Code_snippets/Multi-File_packages
+# http://wiki.blender.org/index.php/Doc:2.6/Manual/Extensions/Python/Properties
+# http://www.blender.org/documentation/blender_python_api_2_66_4/info_tutorial_addon.html
+
+#print("Urho init ---------------------------------------------------")
+
+bl_info = {
+    "name": "Urho3D export",
+    "description": "Urho3D export",
+    "author": "reattiva",
+    "version": (0, 3),
+    "blender": (2, 66, 0),
+    "location": "Properties > Render > Urho export",
+    "warning": "big bugs, use at your own risk",
+    "wiki_url": "",
+    "tracker_url": "",
+    "category": "Import-Export"}
+
+if "decompose" in locals():
+    import imp
+    imp.reload(decompose)
+    imp.reload(export_urho)
+
+from .decompose import TOptions, Scan
+from .export_urho import UrhoExportData, UrhoExportOptions, UrhoWriteModel, UrhoWriteAnimation, UrhoWriteMaterial, UrhoExport
+    
+import os
+import time
+import sys
+import shutil
+import logging
+
+import bpy
+from bpy.props import StringProperty, BoolProperty, EnumProperty, FloatProperty, IntProperty
+
+#--------------------
+# Loggers
+#--------------------
+
+# A list to save export messages
+logList = []
+
+# Create a logger
+log = logging.getLogger("ExportLogger")
+log.setLevel(logging.DEBUG)
+
+# Formatter for the logger
+FORMAT = '%(levelname)s:%(message)s'
+formatter = logging.Formatter(FORMAT)
+
+# Console filter: no more than 3 identical messages 
+consoleFilterMsg = None
+consoleFilterCount = 0
+class ConsoleFilter(logging.Filter):
+    def filter(self, record):
+        global consoleFilterMsg
+        global consoleFilterCount
+        if consoleFilterMsg == record.msg:
+            consoleFilterCount += 1
+            if consoleFilterCount > 2:
+                return False
+        else:
+            consoleFilterCount = 0
+            consoleFilterMsg = record.msg
+        return True
+consoleFilter = ConsoleFilter()
+
+# Logger handler which saves unique messages in the list
+logMaxCount = 500
+class ExportLoggerHandler(logging.StreamHandler):
+    def emit(self, record):
+        global logList
+        try:
+            if len(logList) < logMaxCount:
+                msg = self.format(record)
+                if not msg in logList:
+                    logList.append(msg)
+            #self.flush()
+        except (KeyboardInterrupt, SystemExit):
+            raise
+        except:
+            self.handleError(record)
+
+# Delete old handlers
+for handler in reversed(log.handlers):
+    log.removeHandler(handler)
+
+# Create a logger handler for the list
+listHandler = ExportLoggerHandler()
+listHandler.setFormatter(formatter)
+log.addHandler(listHandler)
+    
+# Create a logger handler for the console
+consoleHandler = logging.StreamHandler()
+consoleHandler.addFilter(consoleFilter)
+log.addHandler(consoleHandler)
+
+#--------------------
+# Blender UI
+#--------------------
+
+# Addon preferences, they are visible in the Users Preferences Addons page,
+# under the Uhro exporter addon row
+class UrhoAddonPreferences(bpy.types.AddonPreferences):
+    bl_idname = __name__
+
+    outputPath = StringProperty(
+            name = "Default export path",
+            description = "Default path where to export",
+            default = "", 
+            maxlen = 1024,
+            subtype = "DIR_PATH")
+
+    reportWidth = IntProperty(
+            name = "Window width",
+            description = "Width of the report window",
+            default = 500)
+
+    maxMessagesCount = IntProperty(
+            name = "Max number of messages",
+            description = "Max number of messages in the report window",
+            default = 500)
+            
+    def draw(self, context):
+        layout = self.layout
+        layout.prop(self, "outputPath")
+        row = layout.row()
+        row.label("Report window:")
+        row.prop(self, "reportWidth")
+        row.prop(self, "maxMessagesCount")
+
+
+# Here we define all the UI objects we'll add in the export panel
+class UrhoExportSettings(bpy.types.PropertyGroup):
+
+    # This is called each time a property (created with the parameter 'update')
+    # changes its value
+    def update_func(self, context):
+
+        addonPrefs = context.user_preferences.addons[__name__].preferences
+        if self.outputPath:
+            addonPrefs.outputPath = self.outputPath
+        if self.skeletons:
+            self.geometryWei = True
+        else:
+            self.geometryWei = False
+            self.animations = False
+        if not self.geometries:
+            self.morphs = False
+        if not self.geometryPos or not self.geometryNor or not self.geometryUV:
+            self.geometryTan = False
+        if not self.morphNor:
+            self.morphTan = False
+
+    # Set all the export settings back to their default values
+    def reset(self, context): 
+        
+        addonPrefs = context.user_preferences.addons[__name__].preferences
+        if not self.outputPath:
+            self.outputPath = addonPrefs.outputPath
+        
+        self.useStandardDirs = True
+        self.fileOverwrite = False
+
+        self.source = 'ONLY_SELECTED'
+        self.scale = 1.0
+        self.modifiers = False
+        self.modifiersRes = 'PREVIEW'
+        self.origin = 'LOCAL'
+        self.forceElements = False
+        self.merge = False
+        self.geometrySplit = False
+        self.lods = False
+        self.optimizeIndices = False
+
+        self.skeletons = True
+        self.onlyKeyedBones = False
+
+        self.animations = False
+        self.actions = True
+        self.onlyUsedActions = False
+        self.tracks = False
+        self.timeline = False
+        self.animationPos = True
+        self.animationRot = True
+        self.animationSca = False
+
+        self.geometries = True
+        self.geometryPos = True
+        self.geometryNor = True
+        self.geometryCol = False
+        self.geometryUV = False
+        self.geometryTan = False
+        self.geometryWei = False
+
+        self.morphs = False
+        self.morphNor = True
+        self.morphTan = False
+
+        self.materials = False
+        self.textures = False
+
+    # --- Output settings ---
+    
+    outputPath = StringProperty(
+            name = "",
+            description = "Path where to export",
+            default = "", 
+            maxlen = 1024,
+            subtype = "DIR_PATH",
+            update = update_func)   
+
+    useStandardDirs = BoolProperty(
+            name = "Use Urho standard folders",
+            description = "Use Urho standard folders (Materials, Models, Textures ...)",
+            default = True)
+
+    fileOverwrite = BoolProperty(
+            name = "Files overwrite",
+            description = "If enabled existing files are overwritten without warnings",
+            default = False)
+
+    # --- Source settings ---
+            
+    source = EnumProperty(
+            name = "Source",
+            description = "Objects to be exported",
+            items=(('ALL', "All", "export all the objects in the scene"),
+                   ('ONLY_SELECTED', "Only selected", "export only the selected objects")),
+            default='ONLY_SELECTED')
+
+    scale = FloatProperty(
+            name = "Scale", 
+            description = "Scale to apply on the exported objects", 
+            default = 1.0,
+            min = 0.0, 
+            max = 1000.0,
+            step = 10,
+            precision = 1)
+
+    modifiers = BoolProperty(
+            name = "Apply modifiers",
+            description = "Apply the object modifiers before exporting",
+            default = False)
+
+    modifiersRes = EnumProperty(
+            name = "Modifiers setting",
+            description = "Resolution setting to use while applying modifiers",
+            items=(('PREVIEW', "Preview", "use the Preview resolution setting"),
+                   ('RENDER', "Render", "use the Render resolution setting")),
+            default='PREVIEW')
+
+    origin = EnumProperty(
+            name = "Mesh origin",
+            description = "Origin for the position of vertices/bones",
+            items=(('GLOBAL', "Global", "Blender's global origin"),
+                   ('LOCAL', "Local", "object's local origin (orange dot)")),
+            default='LOCAL')
+
+    forceElements = BoolProperty(
+            name = "Force missing elements",
+            description = "If a vertec elements is missing add it with a zero value (UV, color, weights)",
+            default = False)
+
+    merge = BoolProperty(
+            name = "Merge objects (uses current object name)",
+            description = "Merge all the objects in a single file (one geometry for object)",
+            default = False)
+
+    geometrySplit = BoolProperty(
+            name = "One vertex buffer per object",
+            description = "Split each object into its own vertex buffer",
+            default = False)
+
+    lods = BoolProperty(
+            name = "Use LODs",
+            description = "Search for the LOD distance if the object name, objects with the same name are added as LODs",
+            default = False)
+
+    optimizeIndices = BoolProperty(
+            name = "Optimize indices (slow)",
+            description = "Linear-Speed vertex cache optimisation",
+            default = False)
+
+    # --- Components settings ---
+
+    skeletons = BoolProperty(
+            name = "Skeletons",
+            description = "Export model armature bones",
+            default = False,
+            update = update_func)
+
+    onlyKeyedBones = BoolProperty(
+            name = "Only keyed bones",
+            description = "Export only bones with keys",
+            default = False)
+
+    animations = BoolProperty(
+            name = "Animations",
+            description = "Export animations (Skeletons needed)",
+            default = False)
+            
+    actions = BoolProperty(
+            name = "Actions",
+            description = "Export actions",
+            default = True)
+
+    onlyUsedActions = BoolProperty(
+            name = "Only actions used in tracks",
+            description = "Export only actions used in the NLA tracks",
+            default = True)
+
+    tracks = BoolProperty(
+            name = "Tracks (only not muted)",
+            description = "Export not muted NLA tracks",
+            default = False)
+
+    timeline = BoolProperty(
+            name = "Timeline",
+            description = "Export the timeline (NLA tracks sum)",
+            default = False)
+
+    animationPos = BoolProperty(
+            name = "Position",
+            description = "Within animations export bone positions",
+            default = True)
+
+    animationRot = BoolProperty(
+            name = "Rotation",
+            description = "Within animations export bone rotations",
+            default = True)
+
+    animationSca = BoolProperty(
+            name = "Scale",
+            description = "Within animations export bone scales",
+            default = False)
+
+    geometries = BoolProperty(
+            name = "Geometries",
+            description = "Export vertex buffers, index buffers, geometries, lods",
+            default = True,
+            update = update_func)
+
+    geometryPos = BoolProperty(
+            name = "Position",
+            description = "Within geometry export vertex position",
+            default = True,
+            update = update_func)
+
+    geometryNor = BoolProperty(
+            name = "Normal",
+            description = "Within geometry export vertex normal",
+            default = True,
+            update = update_func)
+
+    geometryCol = BoolProperty(
+            name = "Color",
+            description = "Within geometry export vertex color",
+            default = False)
+
+    geometryUV = BoolProperty(
+            name = "UV",
+            description = "Within geometry export vertex UV",
+            default = True,
+            update = update_func)
+
+    geometryTan = BoolProperty(
+            name = "Tangent",
+            description = "Within geometry export vertex tangent",
+            default = False)
+
+    geometryWei = BoolProperty(
+            name = "Weights",
+            description = "Within geometry export vertex bones weights (Skeletons needed)",
+            default = False)
+
+    morphs = BoolProperty(
+            name = "Morphs (shape keys)",
+            description = "Export vertex morphs (Geometries needed)",
+            default = False)
+
+    morphNor = BoolProperty(
+            name = "Normal",
+            description = "Within morph export vertex normal",
+            default = True,
+            update = update_func)
+
+    morphTan = BoolProperty(
+            name = "Tangent",
+            description = "Within morph export vertex tangent",
+            default = False)
+
+    materials = BoolProperty(
+            name = "Export materials",
+            description = "Export XML materials",
+            default = False,
+            update = update_func)
+
+    textures = BoolProperty(
+            name = "Copy textures",
+            description = "Copy diffuse textures",
+            default = False,
+            update = update_func)            
+
+    bonesGlobalOrigin = BoolProperty(name = "Bones global origin", default = False)
+    actionsGlobalOrigin = BoolProperty(name = "Actions global origin", default = False)
+    
+
+# Reset settings button    
+class UrhoExportResetOperator(bpy.types.Operator):
+    """ Reset export settings """
+    
+    bl_idname = "urho.exportreset"
+    bl_label = "Reset"
+ 
+    def execute(self, context):
+        context.scene.urho_exportsettings.reset(context)
+        return {'FINISHED'}
+     
+
+# View log button
+class UrhoReportDialog(bpy.types.Operator):
+    """ View export log """
+    
+    bl_idname = "urho.report"
+    bl_label = "Urho export report"
+ 
+    def execute(self, context):
+        return {'FINISHED'}
+ 
+    def invoke(self, context, event):
+        global logMaxCount
+        wm = context.window_manager
+        addonPrefs = context.user_preferences.addons[__name__].preferences
+        logMaxCount = addonPrefs.maxMessagesCount
+        return wm.invoke_props_dialog(self, width = addonPrefs.reportWidth)
+        #return wm.invoke_popup(self, width = addonPrefs.reportWidth)
+     
+    def draw(self, context):
+        layout = self.layout
+        scene = context.scene
+        
+        for line in logList:
+            lines = line.split(":", 1)
+            if lines[0] == 'CRITICAL':
+                lineicon = 'RADIO'
+            elif lines[0] == 'ERROR':
+                lineicon = 'CANCEL'
+            elif lines[0] == 'WARNING':
+                lineicon = 'ERROR'
+            elif lines[0] == 'INFO':
+                lineicon = 'INFO'
+            else:
+                lineicon = 'TEXT'
+            layout.label(text = lines[1], icon = lineicon)
+            
+# Export button
+class UrhoExportOperator(bpy.types.Operator):
+    """ Start exporting """
+    
+    bl_idname = "urho.export"
+    bl_label = "Export"
+  
+    def execute(self, context):
+        ExecuteUrhoExport(context)
+        return {'FINISHED'}
+ 
+    def invoke(self, context, event):
+        return self.execute(context)
+    
+# The export panel, here we draw the panel using properties we have created earlier
+class UrhoExportRenderPanel(bpy.types.Panel):
+    
+    bl_idname = "urho.exportrenderpanel"
+    bl_label = "Urho export"
+    bl_space_type = 'PROPERTIES'
+    bl_region_type = 'WINDOW'
+    bl_context = "render"
+    #bl_options = {'DEFAULT_CLOSED'}
+    
+    # Draw the export panel
+    def draw(self, context):
+        layout = self.layout
+        scene = context.scene
+        settings = scene.urho_exportsettings
+
+        row = layout.row()
+        #row=layout.row(align=True)
+        row.operator("urho.export", icon='EXPORT')
+        #split = layout.split(percentage=0.1)
+        row.operator("urho.report", text="", icon='TEXT')
+
+        layout.label("Output:")
+        box = layout.box()      
+
+        box.label("Output folder:")
+        box.prop(settings, "outputPath")
+        box.prop(settings, "useStandardDirs")
+        box.prop(settings, "fileOverwrite")
+
+        row = layout.row()    
+        row.label("Settings:")
+        row.operator("urho.exportreset", text="", icon='FILE')
+        
+        box = layout.box()
+
+        row = box.row()
+        row.label("Objects:")
+        row.prop(settings, "source", expand=True)
+
+        row = box.row()
+        row.label("Origin:")
+        row.prop(settings, "origin", expand=True)
+
+        box.prop(settings, "scale")
+        
+        box.prop(settings, "modifiers")
+        if settings.modifiers:
+            row = box.row()
+            row.separator()
+            row.prop(settings, "modifiersRes", expand=True)
+
+        box.prop(settings, "forceElements")
+        box.prop(settings, "merge")
+        box.prop(settings, "geometrySplit")
+        box.prop(settings, "lods")
+        box.prop(settings, "optimizeIndices")
+
+        box = layout.box()
+
+        row = box.row()
+        row.prop(settings, "skeletons")
+        row.label("", icon='BONE_DATA')
+        #if settings.skeletons:
+            #row = box.row()
+            #row.separator()
+            #row.prop(settings, "bonesGlobalOrigin")
+            #row.prop(settings, "actionsGlobalOrigin")
+
+        row = box.row()
+        row.enabled = settings.skeletons
+        row.prop(settings, "animations")
+        row.label("", icon='ANIM_DATA')
+        if settings.skeletons and settings.animations:
+            row = box.row()
+            row.separator()
+            column = row.column()
+            column.prop(settings, "actions")
+            if settings.actions:
+                row = column.row()
+                row.separator()
+                row.prop(settings, "onlyUsedActions")
+            column.prop(settings, "tracks")
+            column.prop(settings, "timeline")
+            column.prop(settings, "onlyKeyedBones")
+            row = column.row()
+            row.prop(settings, "animationPos")
+            row.prop(settings, "animationRot")
+            row.prop(settings, "animationSca")
+        
+        row = box.row()
+        row.prop(settings, "geometries")
+        row.label("", icon='MESH_DATA')
+        if settings.geometries:
+            row = box.row()
+            row.separator()
+            row.prop(settings, "geometryPos")
+            row.prop(settings, "geometryNor")
+            row.prop(settings, "geometryUV")
+            row = box.row()
+            row.separator()
+            col = row.column()
+            col.enabled = settings.geometryPos and settings.geometryNor and settings.geometryUV
+            col.prop(settings, "geometryTan")
+            col = row.column()
+            col.enabled = settings.skeletons
+            col.prop(settings, "geometryWei")
+            row.prop(settings, "geometryCol")
+
+        row = box.row()
+        row.enabled = settings.geometries
+        row.prop(settings, "morphs")
+        row.label("", icon='SHAPEKEY_DATA')
+        if settings.geometries and settings.morphs:
+            row = box.row()
+            row.separator()
+            row.prop(settings, "morphNor")
+            col = row.column()
+            col.enabled = settings.morphNor
+            col.prop(settings, "morphTan")
+
+        row = box.row()
+        row.prop(settings, "materials")
+        row.label("", icon='MATERIAL_DATA')
+
+        row = box.row()
+        row.prop(settings, "textures")
+        row.label("", icon='TEXTURE_DATA')
+        
+# This is a test to set the default path if the path edit box is empty        
+def PostLoad(dummy):
+
+    addonPrefs = bpy.context.user_preferences.addons[__name__].preferences
+    settings = bpy.context.scene.urho_exportsettings
+    if addonPrefs.outputPath:
+        settings.outputPath = addonPrefs.outputPath
+
+# Called when the addon is enabled. Here we register out UI classes so they can be 
+# used by Python scripts.
+def register():
+    print("Urho export register")
+    
+    #bpy.utils.register_module(__name__)
+        
+    bpy.utils.register_class(UrhoAddonPreferences)
+    bpy.utils.register_class(UrhoExportSettings)
+    bpy.utils.register_class(UrhoExportOperator) 
+    bpy.utils.register_class(UrhoExportResetOperator) 
+    bpy.utils.register_class(UrhoExportRenderPanel)
+    bpy.utils.register_class(UrhoReportDialog)
+    
+    bpy.types.Scene.urho_exportsettings = bpy.props.PointerProperty(type=UrhoExportSettings)
+    
+    bpy.context.user_preferences.filepaths.use_relative_paths = False
+    
+    #if not PostLoad in bpy.app.handlers.load_post:
+    #    bpy.app.handlers.load_post.append(PostLoad)
+
+
+# Note: the script __init__.py is executed only the first time the addons is enabled. After that
+# disabling or enabling the script will only call unregister() or register(). So in unregister()
+# delete only objects created with register(), do not delete global objects as they will not be
+# created re-enabling the addon.
+# __init__.py is re-executed pressing F8 or randomly(?) enabling the addon.
+
+# Called when the addon is disabled. Here we remove our UI classes.
+def unregister():
+    print("Urho export unregister")
+    
+    #bpy.utils.unregister_module(__name__)
+    
+    bpy.utils.unregister_class(UrhoAddonPreferences)
+    bpy.utils.unregister_class(UrhoExportSettings)
+    bpy.utils.unregister_class(UrhoExportOperator) 
+    bpy.utils.unregister_class(UrhoExportResetOperator) 
+    bpy.utils.unregister_class(UrhoExportRenderPanel)
+    bpy.utils.unregister_class(UrhoReportDialog)
+    
+    del bpy.types.Scene.urho_exportsettings
+    
+    #if PostLoad in bpy.app.handlers.load_post:
+    #    bpy.app.handlers.load_post.remove(PostLoad)
+
+
+#-------------------------------------------------------------------------
+    
+def ExecuteUrhoExport(context):
+    global logList
+
+    startTime = time.time()
+    
+    print("----------------------Urho export start----------------------")
+    
+    # Clear log list
+    logList[:] = []
+    
+    # Get exporter UI settings
+    settings = context.scene.urho_exportsettings
+    
+    # List where to store tData (decomposed object)
+    tDataList = []
+    # Decompose options
+    tOptions = TOptions()
+        
+    # Copy from exporter UI settings to Decompose options
+    tOptions.mergeObjects = settings.merge
+    tOptions.doForceElements = settings.forceElements
+    tOptions.useLods = settings.lods
+    tOptions.onlySelected = (settings.source == 'ONLY_SELECTED')
+    tOptions.scale = settings.scale
+    tOptions.globalOrigin = (settings.origin == 'GLOBAL')
+    tOptions.applyModifiers = settings.modifiers
+    tOptions.applySettings = settings.modifiersRes
+    tOptions.doBones = settings.skeletons
+    tOptions.doOnlyKeyedBones = settings.onlyKeyedBones
+    tOptions.doAnimations = settings.animations
+    tOptions.doActions = settings.actions
+    tOptions.doOnlyUsedActions = settings.onlyUsedActions
+    tOptions.doTracks = settings.tracks
+    tOptions.doTimeline = settings.timeline
+    tOptions.doAnimationPos = settings.animationPos
+    tOptions.doAnimationRot = settings.animationRot
+    tOptions.doAnimationSca = settings.animationSca
+    tOptions.doGeometries = settings.geometries
+    tOptions.doGeometryPos = settings.geometryPos
+    tOptions.doGeometryNor = settings.geometryNor
+    tOptions.doGeometryCol = settings.geometryCol
+    tOptions.doGeometryUV  = settings.geometryUV
+    tOptions.doGeometryTan = settings.geometryTan
+    tOptions.doGeometryWei = settings.geometryWei
+    tOptions.doMorphs = settings.morphs
+    tOptions.doMorphNor = settings.morphNor
+    tOptions.doMorphTan = settings.morphTan
+    tOptions.doMorphUV = settings.morphTan
+    tOptions.doOptimizeIndices = settings.optimizeIndices
+    tOptions.doMaterials = settings.materials or settings.textures
+    tOptions.bonesGlobalOrigin = settings.bonesGlobalOrigin
+    tOptions.actionsGlobalOrigin = settings.actionsGlobalOrigin
+    
+    if tOptions.mergeObjects and not tOptions.globalOrigin:
+        log.warning("Probably you should use Origin = Global")
+
+    # Decompose
+    ttt = time.time() #!TIME
+    Scan(context, tDataList, tOptions)
+    print("[TIME] Decompose in {:.4f} sec".format(time.time() - ttt) ) #!TIME
+
+    if not settings.outputPath:
+        log.error( "Output path is not set" )
+        tDataList.clear()
+
+    # Export each decomposed object
+    for tData in tDataList:
+        log.info("---- Exporting {:s} ----".format(tData.objectName))
+
+        uExportData = UrhoExportData()
+        uExportOptions = UrhoExportOptions()
+        uExportOptions.splitSubMeshes = settings.geometrySplit
+
+        ttt = time.time() #!TIME
+        UrhoExport(tData, uExportOptions, uExportData)
+        print("[TIME] Export in {:.4f} sec".format(time.time() - ttt) ) #!TIME
+        ttt = time.time() #!TIME
+
+        #PrintUrhoData(uExportData, 0)
+        #PrintUrhoData(uExportData, PRINTMASK_COORD | PRINTMASK_NORMAL | PRINTMASK_TANGENT | PRINTMASK_WEIGHT)
+        
+        modelsPath = settings.outputPath
+        if settings.useStandardDirs:
+            modelsPath = os.path.join(modelsPath, 'Models')
+
+        if not os.path.isdir(modelsPath):
+            log.info( "Creating path {:s}".format(modelsPath) )
+            os.makedirs(modelsPath)
+            
+        for uModel in uExportData.models:
+            if uModel.geometries:
+                filename = os.path.join(modelsPath, uModel.name + os.path.extsep + "mdl")
+                #filename = bpy.path.ensure_ext(filename, ".mdl")
+                #filename = bpy.path.clean_name(filename)
+                if not os.path.exists(filename) or settings.fileOverwrite:
+                    log.info( "Creating file {:s}".format(filename) )
+                    UrhoWriteModel(uModel, filename)
+                else:
+                    log.error( "File already exist {:s}".format(filename) )
+            
+        for uAnimation in uExportData.animations:
+            filename = os.path.join(modelsPath, uAnimation.name + os.path.extsep + "ani")
+            if not os.path.exists(filename) or settings.fileOverwrite:
+                log.info( "Creating file {:s}".format(filename) )
+                UrhoWriteAnimation(uAnimation, filename)
+            else:
+                log.error( "File already exist {:s}".format(filename) )
+    
+        if settings.materials:
+            materailsPath = settings.outputPath
+            if settings.useStandardDirs:
+                materailsPath = os.path.join(materailsPath, 'Materials')
+
+            if not os.path.isdir(materailsPath):
+                log.info( "Creating path {:s}".format(materailsPath) )
+                os.makedirs(materailsPath)
+
+            for tMaterial in tData.materialsList:
+                if not tMaterial.name or not tMaterial.imageName:
+                    continue
+                filename = os.path.join(materailsPath, tMaterial.name + os.path.extsep + "xml")
+                if not os.path.exists(filename) or settings.fileOverwrite:
+                    log.info( "Creating file {:s}".format(filename) )
+                    UrhoWriteMaterial(tMaterial, filename, settings.useStandardDirs)
+                else:
+                    log.error( "File already exist {:s}".format(filename) )
+
+        if settings.textures:
+            texturesPath = settings.outputPath
+            if settings.useStandardDirs:
+                texturesPath = os.path.join(texturesPath, 'Textures')
+
+            if not os.path.isdir(texturesPath):
+                log.info( "Creating path {:s}".format(texturesPath) )
+                os.makedirs(texturesPath)
+
+            for tMaterial in tData.materialsList:
+                if not tMaterial.name or not tMaterial.image:
+                    continue
+                filename = os.path.join(texturesPath, tMaterial.imageName)
+                
+                if '.' not in filename:
+                    filename += '.png'
+                
+                image = tMaterial.image
+                if image.packed_file:
+                    originalPath = image.filepath
+                    image.filepath = filename
+                    image.save()
+                    # TODO: this gives errors if the path does not exist. 
+                    # What to do? set it to None? replace with the new?
+                    image.filepath = originalPath  
+                    log.info( "Texture unpacked {:s}".format(filename) )
+                elif not os.path.exists(tMaterial.imagePath):
+                    log.error( "Cannot find texture {:s}".format(tMaterial.imagePath) )
+                elif not os.path.exists(filename) or settings.fileOverwrite:
+                    try:
+                        shutil.copyfile(src = tMaterial.imagePath, dst = filename)
+                        log.info( "Texture copied {:s}".format(filename) )
+                    except:
+                        log.error( "Cannot copy texture in {:s}".format(filename) )
+                else:
+                    log.error( "File already exist {:s}".format(filename) )
+                
+        print("[TIME] Write in {:.4f} sec".format(time.time() - ttt) ) #!TIME
+        
+    log.info("Export ended in {:.4f} sec".format(time.time() - startTime) )
+    
+    bpy.ops.urho.report('INVOKE_DEFAULT')
+
+    
+if __name__ == "__main__":
+	register()

+ 1540 - 0
Source/Extras/BlenderExporter/io_mesh_urho/decompose.py

@@ -0,0 +1,1540 @@
+
+#
+# This script is licensed as public domain.
+# Based on "Export Inter-Quake Model (.iqm/.iqe)" by Lee Salzman
+#
+
+#  http://www.blender.org/documentation/blender_python_api_2_63_2/info_best_practice.html
+#  http://www.blender.org/documentation/blender_python_api_2_63_2/info_gotcha.html
+# Blender types:
+#  http://www.blender.org/documentation/blender_python_api_2_63_7/bpy.types.Mesh.html
+#  http://www.blender.org/documentation/blender_python_api_2_63_7/bpy.types.MeshTessFace.html
+#  http://www.blender.org/documentation/blender_python_api_2_63_7/bpy.types.Material.html
+# UV:
+#  http://www.blender.org/documentation/blender_python_api_2_63_2/bpy.types.MeshTextureFaceLayer.html
+#  http://www.blender.org/documentation/blender_python_api_2_63_2/bpy.types.MeshTextureFace.html
+# Skeleton:
+#  http://www.blender.org/documentation/blender_python_api_2_66_4/bpy.types.Armature.html
+#  http://www.blender.org/documentation/blender_python_api_2_66_4/bpy.types.Bone.html
+#  http://www.blender.org/documentation/blender_python_api_2_66_4/bpy.types.Pose.html
+#  http://www.blender.org/documentation/blender_python_api_2_66_4/bpy.types.PoseBone.html
+# Animations:
+#  http://www.blender.org/documentation/blender_python_api_2_66_4/bpy.types.Action.html
+#  http://www.blender.org/documentation/blender_python_api_2_66_4/bpy.types.AnimData.html
+# Vertex color:
+#  http://www.blender.org/documentation/blender_python_api_2_66_4/bpy.types.MeshColor.html
+# Moprhs (Shape keys):
+#  http://www.blender.org/documentation/blender_python_api_2_66_4/bpy.types.Key.html
+#  http://www.blender.org/documentation/blender_python_api_2_66_4/bpy.types.ShapeKey.html
+
+# Inverse transpose for normals
+#  http://www.arcsynthesis.org/gltut/Illumination/Tut09%20Normal%20Transformation.html
+
+# Pthon binary writing:
+#  http://docs.python.org/2/library/struct.html
+
+import bpy
+import bmesh
+import math
+import time
+from mathutils import Vector, Matrix, Quaternion
+from collections import OrderedDict
+import os
+import operator
+import heapq
+import logging
+
+log = logging.getLogger("ExportLogger")
+
+#------------------
+# Geometry classes
+#------------------
+
+# Vertex class
+class TVertex:
+    def __init__(self):
+        # Index of the vertex in the Blender buffer
+        self.blenderIndex = None
+        # Position of the vertex: Vector((0.0, 0.0, 0.0))
+        self.pos = None
+        # Normal of the vertex: Vector((0.0, 0.0, 0.0))
+        self.normal = None
+        # Color of the vertex: (0, 0, 0, 0)...(255, 255, 255, 255)
+        self.color = None
+        # UV coordinates of the vertex: Vector((0.0, 0.0))..Vector((1.0, 1.0))
+        self.uv = None
+        # Tangent of the vertex: Vector((0.0, 0.0, 0.0))
+        self.tangent = None
+        # Bitangent of the vertex: Vector((0.0, 0.0, 0.0))
+        self.bitangent = None
+        # Bones weights: list of tuple(boneIndex, weight)
+        self.weights = None
+
+    # used by the function index() of lists
+    def __eq__(self, other):
+        # TODO: can we do without color and weights?
+        #return (self.__dict__ == other.__dict__)
+        return (self.pos == other.pos and 
+                self.normal == other.normal and 
+                self.uv == other.uv)
+    
+    def __hash__(self):
+        hashValue = 0
+        if self.pos:
+            hashValue ^= hash(self.pos.x) ^ hash(self.pos.y) ^ hash(self.pos.z)
+        if self.normal:
+            hashValue ^= hash(self.normal.x) ^ hash(self.normal.y) ^ hash(self.normal.z)
+        if self.uv:
+            hashValue ^= hash(self.uv.x) ^ hash(self.uv.y)
+        return hashValue
+    
+    def __str__(self):
+        s  = "  coords: {: .3f} {: .3f} {: .3f}".format(self.pos.x, self.pos.y, self.pos.z)
+        s += "\n normals: {: .3f} {: .3f} {: .3f}".format(self.normal.x, self.normal.y, self.normal.z)
+        if self.color:
+            s += "\n   color: {:3d} {:3d} {:3d} {:3d}".format(self.color[0], self.color[1], self.color[2], self.color[3])
+        if self.uv:
+            s += "\n      uv: {: .3f} {: .3f}".format(self.uv[0], self.uv[1])
+        if self.tangent:
+            s += "\n tangent: {: .3f} {: .3f} {: .3f}".format(self.tangent.x, self.tangent.y, self.tangent.z)
+        if self.weights:
+            s += "\n weights: "
+            for w in self.weights:
+                s += "{:d} {:.3f}  ".format(w[0],w[1])
+        return s
+
+# Geometry LOD level class
+class TLodLevel:
+    def __init__(self):
+        self.distance = 0.0
+        # Set of all vertex indices use by this LOD
+        self.indexSet = set()
+        # List of triangles of the LOD (triples of vertex indices)
+        self.triangleList = []
+
+    def __str__(self):  
+        s = "  distance: {:.3f}\n".format(self.distance)
+        s += "  triangles: "
+        for i, t in enumerate(self.triangleList):
+            if i and (i % 5) == 0:
+                s += "\n             "
+            s += "{:3d} {:3d} {:3d} |".format(t[0],t[1],t[2])
+        return s
+    
+# Geometry class
+class TGeometry:
+    def __init__(self):
+        # List of TLodLevel
+        self.lodLevels = []
+
+    def __str__(self):
+        s = ""
+        for i, l in enumerate(self.lodLevels):
+            s += " {:d}\n".format(i) + str(l)
+        return s
+
+#------------------
+# Morph classes
+#------------------
+
+class TMorph:
+    def __init__(self, name):
+        # Morph name
+        self.name = name
+        # Set of all vertex indices use by this morph
+        self.indexSet = set()
+        # List of triangles of the morph (triples of vertex indices)
+        self.triangleList = []
+        # Maps vertex index to morphed TVertex
+        self.vertexMap = {}
+
+    def __str__(self):  
+        s = " name: {:s}\n".format(self.name)
+        s += " Vertices: "
+        for k, v in sorted(self.vertices.items()):
+            s += "\n  index: {:d}".format(k)
+            s += "\n" + str(v)
+        return s
+
+#-------------------
+# Materials classes
+#-------------------
+
+class TMaterial:
+    def __init__(self, index, imageName):
+        # Blender material index
+        self.index = index
+        # Image name (it is the filename without path)
+        self.imageName = imageName
+
+        # Blender image data
+        self.image = None
+        # Image full path
+        self.imagePath = None
+        # Material name
+        self.name = None
+        # Material specular color (useless for now)
+        self.specularColor = None
+
+    # used by the function index() of lists
+    def __eq__(self, other):
+        #return (self.__dict__ == other.__dict__)
+        return (self.index == other.index and self.imageName == other.imageName)
+
+    def __str__(self):  
+        return (" index: {:d}\n"
+                " image: \"{:s}\""
+                .format(self.index, self.image) )
+
+#--------------------
+# Animations classes
+#--------------------
+
+class TBone:    
+    def __init__(self, index, parentName, position, rotation, scale, transform):
+        # Position of the bone in the OrderedDict
+        self.index = index
+        # Name of the parent bone
+        self.parentName = parentName
+        # Bone position in the parent bone tail space (you first apply this)
+        self.bindPosition = position
+        # Bone rotation in the parent bone tail space (and then this)
+        self.bindRotation = rotation
+        # Bone scale
+        self.bindScale = scale
+        # Bone transformation in object space
+        self.worldTransform = transform
+
+    def __str__(self):
+        s = " bind pos " + str(self.bindPosition)
+        s += "\n bind rot " + str(self.bindRotation) #+ "\n" + str(self.bindRotation.to_axis_angle())
+        #s += "\n" + str(self.worldTransform.inverted())
+        s += "\n" + str(self.worldTransform)
+        return s
+
+class TFrame:
+    def __init__(self, time, position, rotation, scale):
+        self.time = time
+        self.position = position
+        self.rotation = rotation
+        self.scale = scale
+        
+    def hasMoved(self, other):
+        return (self.position != other.position or self.rotation != other.rotation or self.scale != other.scale)
+
+class TTrack:
+    def __init__(self, name):
+        self.name = name
+        self.frames = []
+
+class TAnimation:
+    def __init__(self, name):
+        self.name = name
+        self.tracks = []
+
+#---------------------
+# Export data classes
+#---------------------
+
+class TData:
+    def __init__(self):
+        self.objectName = None
+        # List of all the TVertex of all the geometries
+        self.verticesList = []
+        # List of TGeometry, they contains triangles, triangles are made of vertex indices
+        self.geometriesList = []
+        # List of TMorph: a subset of the vertices list with modified position
+        self.morphsList = []
+        # List of TMaterial
+        self.materialsList = []
+        # Material index to geometry index map
+        self.materialGeometryMap = {}
+        # Ordered dictionary of TBone: bone name to TBone
+        self.bonesMap = OrderedDict()
+        # List of TAnimation
+        self.animationsList = []
+
+class TOptions:
+    def __init__(self):
+        self.newLod = True
+        self.lodDistance = 0.0
+        self.doForceElements = False
+        self.mergeObjects = False
+        self.useLods = False
+        self.onlySelected = False
+        self.scale = 1.0
+        self.globalOrigin = True
+        self.bonesGlobalOrigin = False  #useless
+        self.actionsGlobalOrigin = False  #
+        self.applyModifiers = False
+        self.applySettings = 'PREVIEW'
+        self.doBones = True
+        self.doOnlyKeyedBones = False   #TODO: check
+        self.doAnimations = True
+        self.doActions = True
+        self.doOnlyUsedActions = False
+        self.doTracks = False
+        self.doTimeline = False
+        self.doAnimationPos = True
+        self.doAnimationRot = True
+        self.doAnimationSca = True
+        self.doGeometries = True
+        self.doGeometryPos = True
+        self.doGeometryNor = True
+        self.doGeometryCol = True
+        self.doGeometryUV  = True
+        self.doGeometryTan = True
+        self.doGeometryWei = True
+        self.doMorphs = True
+        self.doMorphNor = True
+        self.doMorphTan = True
+        self.doMorphUV = True
+        self.doOptimizeIndices = True
+        self.doMaterials = True
+        
+
+#--------------------
+# “Computing Tangent Space Basis Vectors for an Arbitrary Mesh” by Lengyel, Eric. 
+# Terathon Software 3D Graphics Library, 2001.
+# http://www.terathon.com/code/tangent.html
+#--------------------
+        
+def GenerateTangents(tLodLevel, tVertexList):
+
+    if not tLodLevel.indexSet or not tLodLevel.triangleList or not tVertexList:
+        return
+
+    tangentOverwritten = False
+    bitangentOverwritten = False
+    invalidUV = False
+    
+    minVertexIndex = None
+    maxVertexIndex = None
+    for vertexIndex in tLodLevel.indexSet:
+        if minVertexIndex is None:
+            minVertexIndex = vertexIndex
+            maxVertexIndex = vertexIndex
+        elif minVertexIndex > vertexIndex:
+            minVertexIndex = vertexIndex
+        elif maxVertexIndex < vertexIndex:
+            maxVertexIndex = vertexIndex
+
+        vertex = tVertexList[vertexIndex]
+        if vertex.tangent:
+            #log.warning("Overwriting tangent of vertex {:d}".format(vertexIndex))
+            tangentOverwritten = True
+        if vertex.bitangent:
+            #log.warning("Overwriting bitangent of vertex {:d}".format(vertexIndex))
+            bitangentOverwritten = True
+        if vertex.pos is None:
+            log.warning("Missing position on vertex {:d}, tangent generation cancelled.".format(vertexIndex))
+            return
+        if vertex.normal is None:
+            log.warning("Missing normal on vertex {:d}, tangent generation cancelled.".format(vertexIndex))
+            return
+        if vertex.uv is None:
+            log.warning("Missing UV on vertex {:d}, tangent generation cancelled.".format(vertexIndex))
+            return
+           
+        vertex.tangent = Vector((0.0, 0.0, 0.0))
+        vertex.bitangent = Vector((0.0, 0.0, 0.0))
+
+    if tangentOverwritten:
+        log.warning("Overwriting tangent")
+    if bitangentOverwritten:
+        log.warning("Overwriting bitangent")
+
+    for i, triangle in enumerate(tLodLevel.triangleList):
+        vertex1 = tVertexList[triangle[0]]
+        vertex2 = tVertexList[triangle[1]]
+        vertex3 = tVertexList[triangle[2]]
+
+        x1 = vertex2.pos.x - vertex1.pos.x
+        x2 = vertex3.pos.x - vertex1.pos.x
+        y1 = vertex2.pos.y - vertex1.pos.y
+        y2 = vertex3.pos.y - vertex1.pos.y
+        z1 = vertex2.pos.z - vertex1.pos.z
+        z2 = vertex3.pos.z - vertex1.pos.z
+        
+        if vertex2.uv == vertex3.uv or vertex1.uv == vertex2.uv or vertex1.uv == vertex3.uv:
+            #log.error("Invalid UV on vertex {:d}".format(i))
+            invalidUV = True
+            # Note: don't quit here because we need tangents with 4 components (we need '.w')
+            continue
+
+        s1 = vertex2.uv.x - vertex1.uv.x
+        s2 = vertex3.uv.x - vertex1.uv.x
+        t1 = vertex2.uv.y - vertex1.uv.y
+        t2 = vertex3.uv.y - vertex1.uv.y
+
+        r = 1.0 / (s1 * t2 - s2 * t1)
+        sdir = Vector( ((t2 * x1 - t1 * x2) * r, (t2 * y1 - t1 * y2) * r, (t2 * z1 - t1 * z2) * r) )
+        tdir = Vector( ((s1 * x2 - s2 * x1) * r, (s1 * y2 - s2 * y1) * r, (s1 * z2 - s2 * z1) * r) )
+        
+        vertex1.tangent += sdir;
+        vertex2.tangent += sdir;
+        vertex3.tangent += sdir;
+        
+        vertex1.bitangent += tdir;
+        vertex2.bitangent += tdir;
+        vertex3.bitangent += tdir;
+
+    if invalidUV:
+        log.error("Invalid UV")
+
+    for vertexIndex in tLodLevel.indexSet:
+        vertex = tVertexList[vertexIndex]
+        
+        # Gram-Schmidt orthogonalize
+        v = ( vertex.tangent - vertex.normal * vertex.normal.dot(vertex.tangent) ).normalized()
+        
+        # Calculate handedness
+        w = 1.0
+        if vertex.normal.cross(vertex.tangent).dot(vertex.bitangent) < 0.0:
+            w = -1.0
+        
+        vertex.bitangent = vertex.normal.cross(vertex.tangent).normalized()
+        vertex.tangent = Vector((v.x, v.y, v.z, w))
+
+        
+#--------------------
+# Linear-Speed Vertex Cache Optimisation algorithm by Tom Forsyth
+#  https://home.comcast.net/~tom_forsyth/papers/fast_vert_cache_opt.html
+#--------------------
+
+# This is an optimized version, but it is still slow.
+# (on an avarage pc, 5 minutes for 30K smooth vertices)
+
+#  We try to sort triangles in the index buffer so that we gain an optimal use
+#  of the hardware vertices cache.
+#  We assign a score to each triangle, we find the best and save it in a new 
+#  ordered list.
+#  The score of each triangle is the sum of the score of its vertices, and the
+#  score of a vertex is higher if it is:
+#  - used recently (it is still in the cache) but we also try to avoid the last
+#    triangle added (n this way we get better result),
+#  - lonely isolated vertices (otherwise the will be keep for last and drawing
+#    them will require an higher cost)
+#  The order of vertices in the triangle does not matter.
+#  We'll apply this optimization to each lod of each geometry.
+
+# These are the constants used in the algorithm:
+VERTEX_CACHE_SIZE = 32
+CACHE_DECAY_POWER = 1.5
+LAST_TRI_SCORE = 0.75
+VALENCE_BOOST_SCALE = 2.0
+VALENCE_BOOST_POWER = 0.5
+
+def CalculateScore(rank):
+
+    if rank.useCount == 0:
+        rank.score = -1.0
+        return
+
+    score = 0.0
+    cachePosition = rank.cachePosition
+    
+    if cachePosition < 0:
+        # Vertex is not in FIFO cache - no score
+        pass
+    elif cachePosition < 3:
+        # This vertex was used in the last triangle,
+        # so it has a fixed score, whichever of the three
+        # it's in. Otherwise, you can get very different
+        # answers depending on whether you add
+        # the triangle 1,2,3 or 3,1,2 - which is silly.
+        score = LAST_TRI_SCORE
+    else:
+        # Points for being high in the cache
+        score = 1.0 - float(rank.cachePosition - 3) / (VERTEX_CACHE_SIZE - 3)
+        score = pow(score, CACHE_DECAY_POWER)
+
+    # Bonus points for having a low number of tris still to
+    # use the vert, so we get rid of lone verts quickly
+    valenceBoost = VALENCE_BOOST_SCALE * pow(rank.useCount, -VALENCE_BOOST_POWER);
+    rank.score = score + valenceBoost;
+
+# Triangles score list sizes
+TRIANGLERANK_SIZE = 500
+TRIANGLERANK_MAX_SIZE = 505
+
+def OptimizeIndices(lodLevel):
+    
+    # Ranks are used to store data for each vertex
+    class Rank:
+        def __init__(self):
+            self.score = 0.0
+            self.useCount = 1
+            self.cachePosition = -1
+    
+    # Create a map: vertex index to its corresponding Rank
+    ranking = {}
+    
+    # This list contains the original triangles (not in optimal order), we'll move them 
+    # one by one in a new list following the optimal order
+    oldTriangles = lodLevel.triangleList
+
+    # For each vertex index of each triangle increment the use counter
+    # (we can find the same vertex index more than once)
+    for triangle in oldTriangles:
+        for index in triangle:
+            try:
+                ranking[index].useCount += 1
+            except KeyError:
+                ranking[index] = Rank()
+
+    # Calculate the first round of scores
+    # (Rank is mutable, so CalculateScore will be able to modify it)
+    for rank in ranking.values():        
+        CalculateScore(rank)
+
+    # Ths list will contain the triangles sorted in optimal order
+    newTriangles = []
+
+    # Cache of vertex indices
+    vertexCache = []
+    
+    # The original algorithm was:
+    # - scan all the old triangles and find the one with the best score;
+    # - move it to the new triangles;
+    # - move its vertices in the cache;
+    # - recalculate the score on all the vertices on the cache.
+    # The slowest part is the first step, scanning all the old triangles,
+    # but in the last step we update only a little subset of these triangles,
+    # and it is a waste to recalculate the triangle score of each old triamgle.
+    # So we do this:
+    # - create a map 'trianglesMap': vertex index to triangles;
+    # - keep a list 'trianglesRanking' of the best triangles;
+    # - at first this list is empty, we start adding triangles; we add tuples like
+    #   (score, triangle) and we keep track of the min score, we don't add triangles
+    #   with score lower than the min; for now we add triangles without bothering
+    #   about order; if the triangle is already present in the list we only update
+    #   its score (even if it is lower);
+    # - when the list is a little too big (TRIANGLERANK_MAX_SIZE), we sort the list 
+    #   by score and we only keep the best TRIANGLERANK_SIZE triangles, we update 
+    #   the min score;
+    # - after scanning all the old triangles, we take out from the list the best
+    #   triangle;
+    # - move it to the new triangles and remove it from the map;
+    # - move its vertices in the cache;
+    # - recalculate the score on all the vertices in the cache, if the score of one
+    #   vertex is changed, we use the map to find what triangles are affected and
+    #   we add them to the list (unordered and only if their score is > min);
+    # - now when we repeat we have the list already populated, so we don't need to
+    #   recalculate all old triangles scores, we only need to sort the list and take
+    #   out the best triangle.
+
+        
+    # Vertex index to triangle indices list map
+    trianglesMap = {}
+    # Populate the map
+    for triangle in oldTriangles:
+        for vertexIndex in triangle:
+            try:
+                triangleList = trianglesMap[vertexIndex]
+            except KeyError:
+                triangleList = []
+                trianglesMap[vertexIndex] = triangleList
+            triangleList.append(triangle)
+
+    class TrianglesRanking:
+        def __init__(self):
+            self.ranklist = []
+            self.min = None
+            self.isSorted = True
+    
+        def update(self, triangle):            
+            # Sum the score of all its vertex. 
+            # >> This is the slowest part of the algorithm <<
+            triangleScore = ranking[triangle[0]].score + ranking[triangle[1]].score + ranking[triangle[2]].score
+            # If needed, add it to the list
+            if not self.ranklist:
+                self.ranklist.append( (triangleScore, triangle) )
+                self.min = triangleScore
+            else:
+                # We add only triangles with score > min
+                if triangleScore > self.min:
+                    found = False
+                    # Search of the triangle is already present in the list
+                    for i, rank in enumerate(self.ranklist):
+                        if triangle == rank[1]:
+                            if triangleScore != rank[0]:
+                                self.ranklist[i] = (triangleScore, triangle)
+                                self.isSorted = False
+                            found = True
+                            break
+                    # It is a new triangle
+                    if not found:
+                        self.ranklist.append( (triangleScore, triangle) )
+                        self.isSorted = False
+
+        def sort(self):
+            if self.isSorted:
+                return
+            #self.ranklist = sorted(self.ranklist, key=operator.itemgetter(0), reverse=True)[:TRIANGLERANK_SIZE]
+            self.ranklist = heapq.nlargest(TRIANGLERANK_SIZE, self.ranklist, key = operator.itemgetter(0))
+            self.min = self.ranklist[-1][0]
+            self.isSorted = True
+        
+        def popBest(self):
+            bestTriangle = self.ranklist[0][1]
+            del self.ranklist[0]
+            return bestTriangle
+
+    trianglesRanking = TrianglesRanking()
+
+    # Progress counter
+    progressCur = 0
+    progressTot = 0.01 * len(oldTriangles)
+
+    ttt = time.time() #!TIME
+
+    # While there still are unsorted triangles
+    while oldTriangles:
+        # Print progress
+        if (progressCur & 0x7F) == 0:
+            print("{:.3f}%\r".format(progressCur / progressTot), end='' )
+        progressCur += 1
+        
+        # When the list is empty, we need to scan all the old triangles
+        if not trianglesRanking.ranklist:
+            for triangle in oldTriangles:
+                # We add the triangle but we don't search for the best one
+                trianglesRanking.update(triangle)
+                # If the list is too big, sort and truncate it
+                if len(trianglesRanking.ranklist) > TRIANGLERANK_MAX_SIZE:
+                    trianglesRanking.sort()
+
+        if trianglesRanking:
+            # Only if needed, we sort and truncate
+            trianglesRanking.sort()
+            # We take the best triangles out of the list
+            bestTriangle = trianglesRanking.popBest()
+        else:
+            log.error("Could not find next triangle")
+            return        
+        
+        # Move the best triangle to the output list
+        oldTriangles.remove(bestTriangle)
+        newTriangles.append(bestTriangle)
+            
+        # Model the LRU cache behaviour
+        # Recreate the cache removing the vertices of the best triangle
+        vertexCache = [i for i in vertexCache if i not in bestTriangle]
+        
+        for vertexIndex in bestTriangle:
+            # Then push them to the front
+            vertexCache.insert(0, vertexIndex)
+            # Decrement the use counter of its vertices
+            ranking[vertexIndex].useCount -= 1
+            # Remove best triangle from the map
+            triangleList = trianglesMap[vertexIndex]
+            triangleList.remove(bestTriangle)
+
+        # Update positions & scores of all vertices in the cache
+        # Give position -1 if vertex is going to be erased
+        for i, vertexIndex in enumerate(vertexCache):
+            rank = ranking[vertexIndex]
+            if (i > VERTEX_CACHE_SIZE):
+                rank.cachePosition = -1
+            else:
+                rank.cachePosition = i
+            # Calculate the new score
+            oldScore = rank.score
+            CalculateScore(rank)
+            # If the score is changed
+            if oldScore != rank.score:
+                # Add to the list all the triangles affected
+                triangleList = trianglesMap[vertexIndex]
+                for triangle in triangleList:   
+                    trianglesRanking.update(triangle)
+                
+        # Finally erase the extra vertices
+        vertexCache[:] = vertexCache[:VERTEX_CACHE_SIZE]
+
+    print("[TIME2] {:.4f}".format(time.time() - ttt) ) #!TIME
+
+    # Rewrite the index data now
+    lodLevel.triangleList = newTriangles
+
+
+#--------------------
+# Decompose armatures
+#--------------------
+
+# How to read a skeleton: 
+# start from the root bone, move it of bindPosition in the armature space
+# then rotate the armature space with bindRotation, this will be the parent
+# space used by its childs. For each child bone move it of bindPosition in 
+# the parent space then rotate the parent space with bindRotation, and so on.
+
+# We need each bone position and rotation in parent bone space:
+# upAxis = Matrix.Rotation(pi/2, 4, 'X')
+# poseMatrix = bone.matrix
+# if parent:
+#   poseMatrix = parentBone.matrix.inverted() * poseMatrix
+# else:
+#   poseMatrix = upAxis.matrix.inverted() * origin.matrix * poseMatrix  
+
+def DecomposeArmature(scene, armatureObj, tData, tOptions):
+    
+    bonesMap = tData.bonesMap
+
+    ## armature.pose_position = 'REST'
+    ## bpy.data.armatures[0].pose_position = 'REST'
+
+    # 'armature.pose.bones' contains bones data for the current frame
+    # 'armature.data.bones' contains bones data for the rest position
+    armature = armatureObj.data
+    
+    if not armature.bones:
+        log.warning('Armature {:s} has no bones'.format(armatureObj.name))
+        return
+
+    log.info("Decomposing armature: {:s} ({:d} bones)".format(armatureObj.name, len(armature.bones)) )
+
+    originMatrix = Matrix.Identity(4)    
+    if tOptions.bonesGlobalOrigin:
+        originMatrix = armatureObj.matrix_world
+        
+    def DecomposeBone(bone, parentName):        
+        # Be sure bone is a real child of parent
+        if (bone.parent and bone.parent.name != parentName):
+            log.error("Bone parent mismatch on bone {:s}".format(bone.parent.name))
+            return
+
+        parent = bone.parent
+        
+        # 'bone.matrix_local' is referred to the armature, we need
+        # the trasformation between the current bone and its parent.
+        boneMatrix = bone.matrix_local.copy()
+        
+        # Here 'bone.matrix_local' is in object(armature) space, so we have to
+        # calculate the bone trasformation in parent bone space
+        if parent:
+            boneMatrix = parent.matrix_local.inverted() * boneMatrix
+        else:
+            # Normally we don't have to worry that Blender is Z up and we want
+            # Y up because we use relative trasformations between bones. However
+            # the parent bone is relative to the armature so we need to convert
+            # Z up to Y up by rotating its matrix by -90° on X
+            boneMatrix = Matrix.Rotation(math.radians(-90.0), 4, 'X' ) * originMatrix * boneMatrix
+
+        if tOptions.scale != 1.0:
+            boneMatrix.translation *= tOptions.scale
+
+        # Extract position and rotation relative to parent in parent space        
+        t = boneMatrix.to_translation()
+        q = boneMatrix.to_quaternion()
+        s = boneMatrix.to_scale()
+                
+        # Convert position and rotation to left hand:
+        tl = Vector((t.x, t.y, -t.z))
+        ql = Quaternion((q.w, -q.x, -q.y, q.z))
+        sl = Vector((s.x, s.y, s.z))
+        
+        # Now we need the bone matrix relative to the armature. 'matrix_local' is
+        # what we are looking for, but it needs to be converted:
+        # 1) rotate of -90° on X axis:
+        # - swap column 1 with column 2
+        # - negate column 1
+        # 2) convert bone trasformation in object space to left hand:        
+        # - swap row 1 with row 2
+        # - swap column 1 with column 2
+        # So putting them together:
+        # - swap row 1 with row 2
+        # - negate column 2
+        ml = bone.matrix_local.copy()
+        if tOptions.scale != 1.0:
+            ml.translation *= tOptions.scale
+        (ml[1][:], ml[2][:]) = (ml[2][:], ml[1][:])
+        ml[0][2] = -ml[0][2]
+        ml[1][2] = -ml[1][2]
+        ml[2][2] = -ml[2][2]
+
+        # Create a new bone
+        tBone = TBone(len(bonesMap), parentName, tl, ql, sl, ml)
+
+        # If new, add the bone to the map with its name
+        if bone.name not in bonesMap:
+            bonesMap[bone.name] = tBone
+        else:
+            log.critical("Bone {:s} already present in the map.".format(bone.name))
+        
+        # Recursively repeat on the bone children
+        for child in bone.children: 
+            DecomposeBone(child, bone.name)
+
+    # Start with root bones then recursively with children
+    for bone in armature.bones.values():
+        if bone.parent is None: 
+            DecomposeBone(bone, None)
+    
+#--------------------
+# Decompose animations
+#--------------------
+
+def DecomposeActions(scene, armatureObj, tData, tOptions):
+
+    bonesMap = tData.bonesMap
+    animationsList = tData.animationsList
+    
+    if not armatureObj.animation_data:
+        log.warning('Armature {:s} has no animation data'.format(armatureObj.name))
+        return
+
+    # armatureObj.animation_data.action ???
+            
+    originMatrix = Matrix.Identity(4)
+    if tOptions.actionsGlobalOrigin:
+        originMatrix = armatureObj.matrix_world
+        if tOptions.globalOrigin and originMatrix != Matrix.Identity(4):
+            # Blender moves/rotates the armature together with the mesh, so if you set a global origin
+            # for Mesh and Actions you'll have twice the transformations. Set only one global origin.
+            log.warning("Use local origin for the object otherwise trasformations are applied twice")
+    
+    # Save current action and frame, we'll restore them later
+    savedAction = armatureObj.animation_data.action
+    savedFrame = scene.frame_current
+    savedUseNla = armatureObj.animation_data.use_nla
+    
+    # TODO: not correct: this contains also unused/deleted actions
+    # TODO: how to get only actions associated with the armature
+    # armature.object.animation_data.nla_tracks[0].strips[1].action
+        
+    animationObjects = []
+    
+    if tOptions.doActions:
+        if tOptions.doOnlyUsedActions:
+            for track in armatureObj.animation_data.nla_tracks:
+                for strip in track.strips:
+                    action = strip.action
+                    if action and not action in animationObjects:
+                        animationObjects.append(action)
+        else:
+            animationObjects.extend(bpy.data.actions)
+            
+    
+    if tOptions.doTracks:
+        for track in armatureObj.animation_data.nla_tracks:
+            if not track.mute:
+                track.is_solo = False
+                animationObjects.append(track)
+            
+    if tOptions.doTimeline:
+        animationObjects.append(armatureObj)
+    
+    if not animationObjects:
+        log.warning('Armature {:s} has no animation to export'.format(armatureObj.name))
+        return
+    
+    for object in animationObjects:
+        tAnimation = TAnimation(object.name)
+    
+        if type(object) is bpy.types.Action:
+            (startframe, endframe) = object.frame_range
+            startframe = int(startframe)
+            endframe = int(endframe+1)
+        else:
+            startframe = int(scene.frame_start)
+            endframe = int(scene.frame_end+1)
+
+        # Here we save every action used by this animation, so we can filter the only used bones
+        actionSet = set()
+
+        # Clear current action on the armature
+        try:
+            armatureObj.animation_data.action = None
+        except AttributeError:
+            log.error("You need to exit action edit mode")
+            return
+        
+        # If it is an action, set the current action; also disable NLA to disable influences from others NLA tracks
+        if type(object) is bpy.types.Action:
+            log.info("Decomposing action: {:s} (frames {:.1f} {:.1f})".format(object.name, startframe, endframe))
+            # Set action on the armature
+            armatureObj.animation_data.use_nla = False
+            armatureObj.animation_data.action = object
+            # Get the actions
+            actionSet.add(object)
+            
+        # If it is a track (not muted), set it as solo
+        if type(object) is bpy.types.NlaTrack:
+            log.info("Decomposing track: {:s} (frames {:.1f} {:.1f})".format(object.name, startframe, endframe))
+            # Set the NLA track as solo
+            object.is_solo = True
+            armatureObj.animation_data.use_nla = True
+            # Get the actions
+            for strip in object.strips:
+                if strip.action:
+                    actionSet.add(strip.action)
+            
+        # If it is the timeline, merge all the tracks (not muted)
+        if type(object) is bpy.types.Object:        
+            log.info("Decomposing animation: {:s} (frames {:.1f} {:.1f})".format(object.name, startframe, endframe))
+            armatureObj.animation_data.use_nla = True
+            # Get the actions
+            for track in object.animation_data.nla_tracks:
+                for strip in track.strips:
+                    if strip.action:
+                        actionSet.add(strip.action)
+
+        if not animationObjects:
+            log.warning("No actions for animation {:s}".format(object.name))
+
+        # Get the bones names
+        bones = []
+        if tOptions.doOnlyKeyedBones:
+            # Get all the names of the bones used by the actions
+            boneSet = set()
+            for action in actionSet:
+                for group in action.groups:
+                    bonesSet.update(group.name)
+            # Add the bones name respecting the order of bonesMap
+            for bone in bonesMap.keys():
+                if bone in boneSet:
+                    bones.append(bone)
+                    boneSet.remove(bone)
+            # Check if any bones used by actions is missing in the map
+            for bone in boneSet:
+                log.warning("Action group(bone) {:s} is not in the skeleton".format(bone))
+        else:
+            # Get all the names of the bones in the map
+            bones = bonesMap.keys()
+	
+        if not bones:
+            log.warning("No bones for animation {:s}".format(object.name))
+            continue
+        
+        # Reset position/rotation/scale of each bone
+        for poseBone in armatureObj.pose.bones:
+            poseBone.matrix_basis = Matrix.Identity(4)
+
+        # Progress counter
+        progressCur = 0
+        progressTot = 0.01 * len(bones) * (endframe-startframe)/scene.frame_step
+    
+        for boneName in bones:
+            if not boneName in bonesMap:
+                log.warning("Skeleton does not contain bone {:s}".format(boneName))
+                continue
+
+            if not boneName in armatureObj.pose.bones:
+                log.warning("Pose does not contain bone {:s}".format(boneName))
+                continue
+            
+            tTrack = TTrack(boneName)
+            
+            # Get the Blender pose bone (bpy.types.PoseBone)
+            poseBone = armatureObj.pose.bones[boneName]
+            parent = poseBone.parent
+        
+            # For each frame
+            for time in range( startframe, endframe, scene.frame_step):
+                
+                if (progressCur % 10) == 0:
+                    print("{:.3f}%\r".format(progressCur / progressTot), end='' )
+                progressCur += 1
+                
+                # Set frame
+                scene.frame_set(time)
+            
+                # This matrix is referred to the armature (object space)
+                poseMatrix = poseBone.matrix.copy()
+
+                if parent:
+                    # Bone matrix relative to its parent bone
+                    poseMatrix = parent.matrix.inverted() * poseMatrix
+                else:
+                    # Root bone matrix relative to the armature
+                    poseMatrix = Matrix.Rotation(math.radians(-90.0), 4, 'X' ) * originMatrix * poseMatrix
+
+                if tOptions.scale != 1.0:
+                    poseMatrix.translation *= tOptions.scale
+
+                # Extract position and rotation relative to parent in parent space        
+                t = poseMatrix.to_translation()
+                q = poseMatrix.to_quaternion()
+                s = poseMatrix.to_scale()
+                
+                # Convert position and rotation to left hand:
+                tl = Vector((t.x, t.y, -t.z))
+                ql = Quaternion((q.w, -q.x, -q.y, q.z))
+                sl = Vector((s.x, s.y, s.z))
+                
+                if not tOptions.doAnimationPos:
+                    tl = None
+                if not tOptions.doAnimationRot:
+                    ql = None
+                if not tOptions.doAnimationSca:
+                    sl = None
+        
+                tFrame = TFrame(time / scene.render.fps, tl, ql, sl)
+                
+                if not tTrack.frames or tTrack.frames[-1].hasMoved(tFrame):
+                    tTrack.frames.append(tFrame)
+                
+            if tTrack.frames:
+                tAnimation.tracks.append(tTrack)
+
+        if tAnimation.tracks:
+            animationsList.append(tAnimation)
+        
+        if type(object) is bpy.types.NlaTrack:
+            object.is_solo = False
+
+    # Restore initial action and frame
+    armatureObj.animation_data.action = savedAction
+    armatureObj.animation_data.use_nla = savedUseNla
+    scene.frame_set(savedFrame)
+
+
+#--------------------
+# Decompose geometries and morphs
+#--------------------
+
+def DecomposeMesh(scene, meshObj, tData, tOptions):
+    
+    verticesList = tData.verticesList
+    geometriesList = tData.geometriesList
+    materialsList = tData.materialsList
+    materialGeometryMap = tData.materialGeometryMap
+    morphsList = tData.morphsList
+    bonesMap = tData.bonesMap
+    
+    verticesMap = {}
+    
+    # Create a Mesh datablock with modifiers applied
+    # (note: do not apply if not needed, it loses precision)
+    mesh = meshObj.to_mesh(scene, tOptions.applyModifiers, tOptions.applySettings)
+    
+    log.info("Decomposing mesh: {:s} ({:d} vertices)".format(meshObj.name, len(mesh.vertices)) )
+    
+    # If we use the object local origin (orange dot) we don't need trasformations
+    posMatrix = Matrix.Identity(4)
+    normalMatrix = Matrix.Identity(4)
+    
+    if tOptions.globalOrigin:
+        posMatrix = meshObj.matrix_world
+        # Use the inverse transpose to rotate normals without scaling (math trick)
+        normalMatrix = meshObj.matrix_world.inverted().transposed()
+    
+    # Apply custom scaling last
+    if tOptions.scale != 1.0:
+        posMatrix = Matrix.Scale(tOptions.scale, 4) * posMatrix 
+
+    # Vertices map: vertex Blender index to TVertex index
+    faceVertexMap = {}
+
+    # Mesh vertex groups
+    meshVertexGroups = meshObj.vertex_groups
+    notBonesGroups = set()
+    missingGroups = set()
+
+    # Python trick: C = A and B, if A is False (None, empty list) then C=A, if A is
+    # True (object, populated list) then C=B
+    
+    # TODO: check for 'active' and 'active.data'
+    # Check if the mesh has UV data
+    uvs = mesh.tessface_uv_textures.active and mesh.tessface_uv_textures.active.data
+    if not uvs and mesh.tessface_uv_textures:
+        uvs = mesh.tessface_uv_textures[0].data
+    if tOptions.doGeometryUV and not uvs:
+        log.warning('Object {:s} has no UV data'.format(meshObj.name))
+
+    # TODO: use another color layer to do alpha?
+    # Check if the mesh has vertex color data
+    colors = mesh.tessface_vertex_colors.active and mesh.tessface_vertex_colors.active.data
+    if not colors and mesh.tessface_vertex_colors:
+        colors = mesh.tessface_vertex_colors[0].data
+    if tOptions.doGeometryCol and not colors:
+        log.warning('Object {:s} has no color data'.format(meshObj.name))
+
+    if tOptions.doMaterials and not mesh.materials:
+        log.warning('Object {:s} has no materials data'.format(meshObj.name))
+
+    # Progress counter
+    progressCur = 0
+    progressTot = 0.01 * len(mesh.tessfaces)
+
+    for face in mesh.tessfaces:
+
+        if (progressCur % 10) == 0:
+            print("{:.3f}%\r".format(progressCur / progressTot), end='' )
+        progressCur += 1
+
+        # Skip if this face has less than 3 unique vertices
+        # (a frozenset is an immutable set of unique elements)
+        if len(frozenset(face.vertices)) < 3: 
+            face.hide = True
+            continue
+
+        if face.hide:
+            continue
+
+        # Get face vertices UV, type: MeshTextureFace(bpy_struct)
+        faceUv = uvs and uvs[face.index]
+
+        # Get face 4 vertices colors
+        fcol = colors and colors[face.index]
+        faceColor = fcol and (fcol.color1, fcol.color2, fcol.color3, fcol.color4)
+
+        # Get texture's filename
+        imageName = None
+        if faceUv and faceUv.image:
+            #imageName = os.path.basename(faceUv.image.filepath)
+            imageName = faceUv.image.name
+        
+        # Material from Blender material index and texture image name
+        # (if no materials are associated the index is zero)
+        tMaterial = TMaterial(face.material_index, imageName)
+        
+        # Search for the material in the list and get its index, or add it to the list if missing
+        try:
+            materialIndex = materialsList.index(tMaterial)
+        except ValueError:
+            materialIndex = len(materialsList)
+            materialsList.append(tMaterial)
+            
+        if tOptions.doMaterials and mesh.materials:
+            if imageName:
+                # type: Image(ID)
+                tMaterial.image = faceUv.image
+                tMaterial.imagePath = bpy.path.abspath(faceUv.image.filepath)
+
+            material = mesh.materials[face.material_index]
+            tMaterial.name = material.name
+            specColor = material.specular_color
+            tMaterial.specularColor = Vector((specColor.r, specColor.g, specColor.b, 1.0))
+
+        # From the material index search for the geometry index, or add it to the map if missing
+        try:
+            geometryIndex = materialGeometryMap[materialIndex]
+        except KeyError:
+            geometryIndex = len(geometriesList)
+            geometriesList.append(TGeometry())
+            materialGeometryMap[materialIndex] = geometryIndex
+
+        # Get the geometry associated to the material
+        geometry = geometriesList[geometryIndex]
+        
+        # Get the last LOD level, or add a new one if requested in the options
+        if not geometry.lodLevels or tOptions.newLod:
+            tOptions.newLod = False
+            lodLevelIndex = len(geometry.lodLevels)
+            tLodLevel = TLodLevel()
+            tLodLevel.distance = tOptions.lodDistance
+            geometry.lodLevels.append(tLodLevel)
+        else:
+            tLodLevel = geometry.lodLevels[-1]
+
+        indexSet = tLodLevel.indexSet
+        triangleList = tLodLevel.triangleList
+            
+        # Here we store all the indices of the face, then we decompose it into triangles
+        tempList = []
+
+        for i, vertexIndex in enumerate(face.vertices):
+            # i: vertex index in the face (0..2 tris, 0..3 quad)
+            # vertexIndex: vertex index in Blender buffer
+
+            # Blender vertex
+            vertex = mesh.vertices[vertexIndex]
+
+            position = posMatrix * vertex.co
+                
+            # if face is smooth use vertex normal else use face normal
+            if face.use_smooth:
+                normal = vertex.normal
+            else:
+                normal = face.normal
+            normal = normalMatrix * normal
+            
+            # Create a new vertex
+            tVertex = TVertex()
+            
+            # Set Blender index
+            tVertex.blenderIndex = vertexIndex
+
+            # Set Vertex position
+            if tOptions.doGeometryPos:
+                tVertex.pos = Vector((position.x, position.z, position.y))
+
+            # Set Vertex normal
+            if tOptions.doGeometryNor:
+                tVertex.normal = Vector((normal.x, normal.z, normal.y))
+                
+            # Set Vertex UV coordinates
+            if tOptions.doGeometryUV:
+                if faceUv:
+                    uv = faceUv.uv[i]
+                    tVertex.uv = Vector((uv[0], 1.0 - uv[1]))
+                elif tOptions.doForceElements:
+                    tVertex.uv = Vector(0.0, 0.0)
+
+            # Set Vertex color
+            if tOptions.doGeometryCol:
+                if faceColor:
+                    # This is an array of 3 floats from 0.0 to 1.0
+                    color = faceColor[i]
+                    # Approx 255*float to the closest int
+                    vertcol = ( int(round(color.r * 255.0)), 
+                                int(round(color.g * 255.0)), 
+                                int(round(color.b * 255.0)),
+                                255 )
+                    tVertex.color = vertcol
+                elif tOptions.doForceElements:
+                    tVertex.uv = Vector(0.0, 0.0)
+
+            # Set Vertex bones weights
+            if tOptions.doGeometryWei:
+                weights = []
+                # Scan all the vertex group associated to the vertex, type: VertexGroupElement(bpy_struct)
+                for g in vertex.groups:
+                    # The group name should be the bone name, but it can also be an user made vertex group
+                    try:
+                        boneName = meshVertexGroups[g.group].name
+                        try:
+                            boneIndex = bonesMap[boneName].index
+                            if g.weight > 0.0 or not weights:
+                                weights.append( (boneIndex, g.weight) )
+                        except KeyError:
+                            notBonesGroups.add(boneName)
+                    except IndexError:
+                        missingGroups.add(str(g.group))
+                # If we found no bone weight (not even one with weight zero) leave the list equal to None
+                if weights:
+                    tVertex.weights = weights
+                elif tOptions.doForceElements:
+                    tVertex.weights = [(0, 0.0)]
+                
+            # All this code do is "tVertexIndex = verticesMapList.index(tVertex)", but we use
+            # a map to speed up.
+
+            # Get an hash of the vertex (different vertices with the same hash are ok)
+            vertexHash = hash(tVertex)
+            
+            try:
+                # Get a list of vertex indices with the same hash
+                verticesMapList = verticesMap[vertexHash]
+            except KeyError:
+                # If the hash is not mapped, create a new list (we should use sets but lists are faster)
+                verticesMapList = []
+                verticesMap[vertexHash] = verticesMapList
+            
+            # For each index in the list, test if it is the same as the current tVertex.
+            # If Position, Normal and UV must be the same get its index.
+            ## tVertexIndex = next((j for j in verticesMapList if verticesList[j] == tVertex), None)
+            tVertexIndex = None
+            for j in verticesMapList:
+                if verticesList[j] == tVertex:
+                    tVertexIndex = j
+                    break
+
+            # If we cannot find it, the vertex is new, add it to the list, and its index to the map list
+            if tVertexIndex is None:
+                tVertexIndex = len(verticesList)
+                verticesList.append(tVertex)
+                verticesMapList.append(tVertexIndex)
+
+
+            # Add the vertex index to the temp list to create triangles later
+            tempList.append(tVertexIndex)
+                        
+            # Map Blender face index and Blender vertex index to our TVertex index (this is used later by Morphs)
+            faceVertexMap[(face.index, vertexIndex)] = tVertexIndex
+            
+            # Save every unique vertex this LOD is using
+            indexSet.add(tVertexIndex)
+
+            # Create triangles
+            if i == 2:
+                triangle = (tempList[0], tempList[2], tempList[1])
+                triangleList.append(triangle)
+
+            if i == 3:
+                triangle = (tempList[0], tempList[3], tempList[2])
+                triangleList.append(triangle)
+        # end loop vertices
+    # end loop faces
+    
+    if notBonesGroups:
+        log.warning("Maybe these groups have no bone: {:s}".format( ", ".join(notBonesGroups) ))
+    if missingGroups:
+        log.warning("These group indices are missing: {:s}".format( ", ".join(missingGroups) ))
+
+    for geometry in geometriesList:
+        # Generate tangents (only for first LOD level)
+        if tOptions.doGeometryTan and geometry.lodLevels:
+            log.info("Generating tangents {:s}".format(meshObj.name) )
+            GenerateTangents(geometry.lodLevels[0], verticesList)
+        # Optimize vertex index buffer for each LOD level
+        if tOptions.doOptimizeIndices:
+            for lodLevel in geometry.lodLevels:
+                log.info("Optimizing indices {:s}".format(meshObj.name) )
+                OptimizeIndices(lodLevel)
+    
+    # Check if we need and can work on shape keys (morphs)
+    shapeKeys = meshObj.data.shape_keys
+    keyBlocks = []
+    if tOptions.doMorphs:
+        if not shapeKeys or len(shapeKeys.key_blocks) < 1:
+            log.warning("Object {:s} has no shape keys".format(meshObj.name))
+        else:
+            keyBlocks = shapeKeys.key_blocks
+
+    # Decompose shape keys (morphs)
+    for j, block in enumerate(keyBlocks):
+        # Skip 'Basis' shape key
+        if j == 0:
+            continue
+        
+        tMorph = TMorph(block.name)
+        
+        log.info("Decomposing shape: {:s} ({:d} vertices)".format(block.name, len(block.data)) )
+
+        # Make a temporary copy of the mesh
+        shapeMesh = mesh.copy()
+        
+        if len(shapeMesh.vertices) != len(block.data):
+            log.error("Vertex count mismatch on shape {:s}.".format(block.name))
+            continue
+        
+        # Appy the shape
+        for i, data in enumerate(block.data):
+            shapeMesh.vertices[i].co = data.co
+
+        # Recalculate normals
+        shapeMesh.update(calc_edges = True, calc_tessface = True)
+        ##shapeMesh.calc_tessface()
+        ##shapeMesh.calc_normals()
+        
+        # TODO: if set use 'vertex group' of the shape to filter affected vertices
+        # TODO: can we use mesh tessfaces and not shapeMesh tessfaces ?
+        
+        for face in shapeMesh.tessfaces:
+            if face.hide:
+                continue
+
+            # TODO: add only affected triangles not faces, use morphed as a mask
+            morphed = False
+
+            # In this list we store vertex index and morphed vertex of each face, we'll add them
+            # to the morph only if at least one vertex on the face is affected by the moprh
+            tempList = []
+            
+            # For each Blender vertex index in the face
+            for vertexIndex in face.vertices:
+
+                # Get the Blender morphed vertex
+                vertex = shapeMesh.vertices[vertexIndex]
+                
+                position = posMatrix * vertex.co
+                
+                # If face is smooth use vertex normal else use face normal
+                if face.use_smooth:
+                    normal = vertex.normal
+                else:
+                    normal = face.normal
+                normal = normalMatrix * normal
+
+                # Try to find the TVertex index corresponding to this Blender vertex index
+                try:
+                    tVertexIndex = faceVertexMap[(face.index, vertexIndex)]
+                except KeyError:
+                    log.error("Cannot find vertex {:d} of face {:d} of shape {:s}."
+                          .format(vertexIndex, face.index, block.name) )
+                    continue
+
+                # Get the original not morphed TVertex
+                tVertex = verticesList[tVertexIndex]
+                   
+                # Create a new morphed vertex
+                # (note: this vertex stores absolute values, not relative to original values)
+                tMorphVertex = TVertex()
+
+                # Set Blender index
+                tMorphVertex.blenderIndex = vertexIndex
+
+                # Set Vertex position
+                tMorphVertex.pos = Vector((position.x, position.z, position.y))
+
+                # Set Vertex normal
+                if tOptions.doMorphNor:
+                    tMorphVertex.normal = Vector((normal.x, normal.z, normal.y))
+                
+                # If we have UV, copy them to the TVertex, we only need them to calculate tangents
+                if tOptions.doMorphUV:
+                    if tVertex.uv:
+                        tMorphVertex.uv = tVertex.uv
+                    elif tOptions.doForceElements:
+                        tVertex.uv = Vector(0.0, 0.0)
+                
+                # Save vertex index and morphed vertex, to be added later if at least one
+                # vertex in the face was morphed
+                tempList.append((tVertexIndex, tMorphVertex))
+                
+                # Check if the morph has effect
+                if tMorphVertex != tVertex or tVertex.pos is None:
+                    morphed = True
+            
+            # If at least one vertex in the face was morphed
+            if morphed:
+                # Add vertices to the morph
+                for i, (tVertexIndex, tMorphVertex) in enumerate(tempList):
+                    try:
+                        # Check if already present
+                        oldTMorphVertex = tMorph.vertexMap[tVertexIndex]
+                        if tMorphVertex != oldTMorphVertex:
+                            log.error('Different vertex {:d} of face {:d} of shape {:s}.'
+                                .format(vertexIndex, face.index, block.name) )
+                            continue
+                    except KeyError:
+                        # Add a new morph vertex
+                        tMorph.vertexMap[tVertexIndex] = tMorphVertex
+                        
+                    # Save how many unique vertex this LOD is using (for tangents calculation)
+                    tMorph.indexSet.add(tVertexIndex)
+
+                    # Create triangles (for tangents calculation)
+                    if i == 2:
+                        triangle = (tempList[0][0], tempList[2][0], tempList[1][0])
+                        tMorph.triangleList.append(triangle)
+
+                    if i == 3:
+                        triangle = (tempList[0][0], tempList[3][0], tempList[2][0])
+                        tMorph.triangleList.append(triangle)
+                    
+        if tOptions.doMorphTan:
+            log.info("Generating morph tangents {:s}".format(block.name) )
+            GenerateTangents(tMorph, tMorph.vertexMap)
+
+        # If valid add the morph to the model list
+        if tMorph.vertexMap:
+            morphsList.append(tMorph)
+        else:
+            log.warning('Empty shape {:s}.'.format(block.name))
+
+        # Delete the temporary copy 
+        bpy.data.meshes.remove(shapeMesh)
+
+    bpy.data.meshes.remove(mesh)    
+
+    return
+
+#--------------------
+# Scan objects
+#--------------------
+
+# Scan and decompose objects
+def Scan(context, tDataList, tOptions):
+    
+    scene = context.scene
+    
+    # Get all objects in the scene or only the selected
+    if tOptions.onlySelected: 
+        objs = context.selected_objects 
+    else:
+        objs = scene.objects
+    
+    # Sort by name
+    objs = sorted(objs, key = operator.attrgetter('name'))
+
+    tData = None
+    noWork = True
+    lodName = None
+    
+    for obj in objs:
+        if obj.type == 'MESH':
+
+            noWork = False
+        
+            name = obj.name
+            createNew = True
+            
+            log.info("---- Decomposing {:s} ----".format(name))
+
+            # Search in the object's name if it is a LOD: <name>LOD<distance>
+            # (LODs must have dot aligned distance, ex. nameLOD09.0, nameLOD12.0)
+            if tOptions.useLods:
+                splitted = name.rsplit(sep="LOD", maxsplit=1)
+                try:
+                    distance = float(splitted[1])
+                    name = splitted[0].rstrip()
+                    if lodName is None or lodName != name:
+                        lodName = name
+                        if distance != 0.0:
+                            log.warning("First LOD should have 0.0 distance")
+                    else:
+                        createNew = False
+                        tOptions.newLod = True
+                        if distance <= tOptions.lodDistance:
+                            log.warning("Wrong LOD sequence: {:d} then {:d}".format(tOptions.lodDistance, distance) )
+                        tOptions.lodDistance = distance
+                    log.info("Added as LOD with distance {:f}".format(distance))
+                except (IndexError, ValueError):
+                    log.warning("Object {:s} has no LODs".format(name) )
+        
+            if tOptions.mergeObjects and createNew:
+                if tData:
+                    # To create a new geometry clear the Material to Geometry dict
+                    tData.materialGeometryMap.clear()
+                createNew = False
+            
+            # Create a new container where to save decomposed data
+            if not tData or createNew:
+                tData = TData()
+                # If we a marging objects, if it exists use the current object name
+                if tOptions.mergeObjects and context.selected_objects  and context.selected_objects[0].name:
+                    tData.objectName = context.selected_objects[0].name
+                else:
+                    tData.objectName = name
+                tDataList.append(tData)
+                tOptions.newLod = True
+                tOptions.lodDistance = 0.0
+            
+            # First we need to populate the skeleton, then animations and then geometries
+            if tOptions.doBones:
+                armatureObj = None
+                # Check if obj has an armature parent, and if it is not attached to a bone (like hair to head bone)
+                if obj.parent and obj.parent.type == 'ARMATURE' and obj.parent_type != 'BONE':
+                    armatureObj = obj.parent
+                else:
+                    # Check if there is an Armature modifier
+                    for modifier in obj.modifiers:
+                        if modifier.type == 'ARMATURE' and modifier.object and modifier.object.type == 'ARMATURE':
+                            armatureObj = modifier.object
+                            break
+                # Decompose armature and animations
+                if armatureObj:
+                    DecomposeArmature(scene, armatureObj, tData, tOptions)
+                    if tOptions.doAnimations:
+                        DecomposeActions(scene, armatureObj, tData, tOptions)
+                else:
+                    log.warning("Object {:s} has no armature".format(name) )
+
+            # Decompose geometries
+            if tOptions.doGeometries:
+                DecomposeMesh(scene, obj, tData, tOptions)
+
+    if noWork:
+        log.warning("No objects to work on")
+
+#-----------------------------------------------------------------------------
+
+if __name__ == "__main__":
+
+    print("------------------------------------------------------")
+    startTime = time.time()
+
+    tDataList = []
+    tOptions = TOptions()
+    
+    Scan(bpy.context, tDataList, tOptions)
+    if tDataList:
+        PrintAll(tDataList[0])
+                
+    print("Executed in {:.4f} sec".format(time.time() - startTime) )
+    print("------------------------------------------------------")
+    
+    

+ 1006 - 0
Source/Extras/BlenderExporter/io_mesh_urho/export_urho.py

@@ -0,0 +1,1006 @@
+
+#
+# This script is licensed as public domain.
+# Based on the Ogre Importer from the Urho3D project
+#
+
+# http://docs.python.org/2/library/struct.html
+
+from mathutils import Vector, Matrix, Quaternion
+import operator
+import struct
+import os
+
+import logging
+log = logging.getLogger("ExportLogger")
+
+'''
+Sometimes editor crashes, maybe we have to lock files:
+----
+from lockfile import FileLock
+lock = FileLock("/some/file/or/other")
+with lock:
+    print lock.path, 'is locked.'
+----
+lock = FileLock("/some/file/or/other")
+while not lock.i_am_locking():
+    try:
+        lock.acquire(timeout=60)    # wait up to 60 seconds
+    except LockTimeout:
+        lock.break_lock()
+        lock.acquire()
+print "I locked", lock.path
+lock.release()
+----
+class A(object):
+    def __key(self):
+        return (self.attr_a, self.attr_b, self.attr_c)
+
+    def __eq__(x, y):
+        return x.__key() == y.__key()
+
+    def __hash__(self):
+        return hash(self.__key())
+
+'''
+
+#--------------------
+# Urho enums
+#--------------------
+
+ELEMENT_POSITION    = 0x0001
+ELEMENT_NORMAL      = 0x0002
+ELEMENT_COLOR       = 0x0004
+ELEMENT_UV1         = 0x0008
+ELEMENT_UV2         = 0x0010
+ELEMENT_CUBE_UV1    = 0x0020
+ELEMENT_CUBE_UV2    = 0x0040
+ELEMENT_TANGENT     = 0x0080
+ELEMENT_BWEIGHTS    = 0x0100
+ELEMENT_BINDICES    = 0x0200
+
+ELEMENT_BLEND       = 0x0300
+
+BONE_BOUNDING_SPHERE = 0x0001
+BONE_BOUNDING_BOX    = 0x0002
+
+TRACK_POSITION      = 0x0001
+TRACK_ROTATION      = 0x0002
+TRACK_SCALE         = 0x0004
+
+TRIANGLE_LIST       = 0
+LINE_LIST           = 1
+            
+# Max number of bones supported by HW skinning
+MAX_SKIN_MATRICES   = 64
+
+#--------------------
+# Classes
+#--------------------
+
+# Bounding box axes aligned
+class BoundingBox:
+    def __init__(self):
+        self.min = None # Vector((0.0, 0.0, 0.0))
+        self.max = None # Vector((0.0, 0.0, 0.0))
+
+    def merge(self, point):
+        if self.min is None:
+            self.min = point.copy()
+            self.max = point.copy()
+            return
+        if point.x < self.min.x:
+            self.min.x = point.x
+        if point.y < self.min.y:
+            self.min.y = point.y
+        if point.z < self.min.z:
+            self.min.z = point.z
+        if point.x > self.max.x:
+            self.max.x = point.x
+        if point.y > self.max.y:
+            self.max.y = point.y
+        if point.z > self.max.z:
+            self.max.z = point.z
+
+# Exception rasied when we add a vertex with more or less elements than
+# the vertex buffer.
+class MaskError(Exception):
+    pass
+
+# --- Model classes ---
+    
+class UrhoVertex:
+    def __init__(self, tVertex, uVertexBuffer):
+        # Note: cannot pass elementMask directly because it is immutable
+        mask = 0
+        # Only used by morphs, original vertex index in the not morphed vertex buffer
+        self.index = None 
+        # Vertex position: Vector((0.0, 0.0, 0.0)) of floats
+        self.pos = tVertex.pos
+        if tVertex.pos:
+            mask |= ELEMENT_POSITION
+        # Vertex normal: Vector((0.0, 0.0, 0.0)) of floats
+        self.normal = tVertex.normal
+        if tVertex.normal:
+            mask |= ELEMENT_NORMAL
+        # Vertex color: (0, 0, 0, 0) of unsigned bytes
+        self.color = tVertex.color       
+        if tVertex.color:
+            mask |= ELEMENT_COLOR
+        # Vertex UV texture coordinate: (0.0, 0.0) of floats
+        self.uv = tVertex.uv
+        if tVertex.uv:
+            mask |= ELEMENT_UV1
+        # Vertex tangent: Vector((0.0, 0.0, 0.0, 0.0)) of floats
+        self.tangent = tVertex.tangent
+        if tVertex.tangent:
+            mask |= ELEMENT_TANGENT
+        # TODO: move it out of here
+        # List of 4 tuples: bone index (unsigned byte), blend weight (float)
+        self.weights = []
+        if not tVertex.weights is None:
+            # Sort tuples (index, weight) by decreasing weight
+            sortedList = sorted(tVertex.weights, key = operator.itemgetter(1), reverse = True)
+            # Sum the first 4 weights
+            totalWeight = sum([t[1] for t in sortedList[:4]])
+            # Keep only the first 4 tuples, normalize weights, add at least 4 tuples
+            for i in range(4):
+                t = (0, 0.0)
+                if i < len(sortedList):
+                    t = sortedList[i]
+                    t = (t[0], t[1] / totalWeight)
+                self.weights.append(t) 
+            mask |= ELEMENT_BLEND
+
+        # Update buffer mask
+        if uVertexBuffer.elementMask is None:
+            uVertexBuffer.elementMask = mask
+        elif uVertexBuffer.elementMask != mask:
+            oldMask = uVertexBuffer.elementMask
+            uVertexBuffer.elementMask &= mask
+            raise MaskError("{:04X} AND {:04X} = {:04X}".format(oldMask, mask, uVertexBuffer.elementMask))
+    
+    # used by the function index() of lists
+    def __eq__(self, other):
+        return (self.pos == other.pos and self.normal == other.normal and 
+                self.color == other.color and self.uv == other.uv)
+
+    # id of this vertex (not unique)
+    def __hash__(self):
+        hashValue = 0
+        if self.pos:
+            hashValue ^= hash(self.pos.x) ^ hash(self.pos.y) ^ hash(self.pos.z)
+        if self.normal:
+            hashValue ^= hash(self.normal.x) ^ hash(self.normal.y) ^ hash(self.normal.z)
+        if self.uv:
+            hashValue ^= hash(self.uv.x) ^ hash(self.uv.y)
+        return hashValue
+
+    # used by moprh vertex calculations
+    def subtract(self, other, mask):
+        if mask & ELEMENT_POSITION:
+            self.pos -= other.pos
+        if mask & ELEMENT_NORMAL:
+            self.normal -= other.normal
+        if mask & ELEMENT_TANGENT:
+            self.tangent -= other.tangent
+            
+class UrhoVertexBuffer:
+    def __init__(self):
+        # Flags of the elements contained in every vertex of this buffer
+        self.elementMask = None
+        # Morph min index and max index in the list vertices TODO: check
+        self.morphMinIndex = None
+        self.morphMaxIndex = None
+        # List of UrhoVertex
+        self.vertices = []
+
+class UrhoIndexBuffer:
+    def __init__(self):
+        # Size of each index: 2 for 16 bits, 4 for 32 bits
+        self.indexSize = 0
+        # List of triples of indices (in the vertex buffer) to draw triangles
+        self.indexes = []
+    
+class UrhoLodLevel:
+    def __init__(self):
+        # Distance above which we draw this LOD
+        self.distance = 0.0
+        # How to draw triangles: TRIANGLE_LIST, LINE_LIST 
+        self.primitiveType = 0
+        # Index of the vertex buffer used by this LOD in the model list
+        self.vertexBuffer = 0
+        # Index of the index buffer used by this LOD in the model list
+        self.indexBuffer = 0
+        # Pointer in the index buffer where starts this LOD
+        self.startIndex = 0
+        # Length in the index buffer to complete draw this LOD
+        self.countIndex = 0
+
+class UrhoGeometry:
+    def __init__(self):
+        # If the bones in the skeleton are too many for the hardware skinning, we
+        # search for only the bones used by this geometry, then create a map from
+        # the new bone index to the old bone index (in the skeleton)
+        self.boneMap = []
+        # List of UrhoLodLevel
+        self.lodLevels = []
+        # Geometry center based on the position of each triangle of the first LOD
+        self.center = Vector((0.0, 0.0, 0.0))
+        
+class UrhoVertexMorph:
+    def __init__(self):
+         # Morph name
+        self.name = None
+        # Maps from 'vertex buffer index' to 'list of vertex', these are only the 
+        # vertices modified by the morph, not all the vertices in the buffer (each 
+        # morphed vertex has an index to the original vertex)
+        self.vertexBufferMap = {}
+
+class UrhoBone:
+    def __init__(self):
+        # Bone name
+        self.name = None
+        # Index of the parent bone in the model bones list
+        self.parentIndex = None
+        # Bone position in parent space
+        self.position = None
+        # Bone rotation in parent space
+        self.rotation = None
+        # Bone scale
+        self.scale = Vector((1.0, 1.0, 1.0))
+        # Bone transformation in skeleton space
+        self.matrix = None
+        # Inverse of the above
+        self.inverseMatrix = None
+        # Position in skeleton space
+        self.derivedPosition = None
+        # Collision sphere and/or box
+        self.collisionMask = 0
+        self.radius = None
+        self.boundingBox = BoundingBox()
+
+class UrhoModel:
+    def __init__(self):
+        # Model name
+        self.name = None
+        # List of UrhoVertexBuffer
+        self.vertexBuffers = []
+        # List of UrhoIndexBuffer
+        self.indexBuffers = []
+        # List of UrhoGeometry
+        self.geometries = []
+        # List of UrhoVertexMorph
+        self.morphs = []
+        # List of UrhoBone
+        self.bones = []
+        # Bounding box, containd each LOD of each geometry
+        self.boundingBox = BoundingBox()
+        
+# --- Animation classes ---
+
+class UrhoKeyframe:
+    def __init__(self, tKeyframe, uTrack):
+        # Note: cannot pass mask directly because it is immutable
+        mask = 0
+        # Time position in seconds: float
+        self.time = tKeyframe.time
+        # Position: Vector((0.0, 0.0, 0.0))
+        self.position = tKeyframe.position
+        if tKeyframe.position:
+            mask |= TRACK_POSITION
+        # Rotation: Quaternion()
+        self.rotation = tKeyframe.rotation
+        if tKeyframe.rotation:
+            mask |= TRACK_ROTATION
+        # Scale: Vector((0.0, 0.0, 0.0))
+        self.scale = tKeyframe.scale
+        if tKeyframe.scale:
+            mask |= TRACK_SCALE
+        # Update track mask    
+        if uTrack.mask is None:
+            uTrack.mask = mask
+        elif uTrack.mask != mask:
+            oldMask = uTrack.elementMask
+            uTrack.mask &= mask
+            raise MaskError("{:04X} AND {:04X} = {:04X}".format(oldMask, mask, uTrack.elementMask))
+            
+class UrhoTrack:
+    def __init__(self):
+        # Track name (practically same as the bone name that should be driven)
+        self.name = ""
+        # Mask of included animation data
+        self.mask = None
+        # Keyframes
+        self.keyframes = []
+        
+class UrhoAnimation:
+    def __init__(self):
+        # Animation name
+        self.name = ""
+        # Length in seconds: float
+        self.length = 0.0
+        # Tracks
+        self.tracks = []
+
+# --- Export options classes ---
+
+class UrhoExportData:
+    def __init__(self):
+        # List of UrhoModel
+        self.models = []
+        # List of UrhoAnimation
+        self.animations = []
+        
+class UrhoExportOptions:
+    def __init__(self):
+        self.splitSubMeshes = False
+                
+
+#--------------------
+# Writers
+#--------------------
+
+class BinaryFileWriter:
+
+    # Constructor.
+    def __init__(self):
+        # File stream.
+        self.file = None
+    
+    # Open file stream.
+    def open(self, filename):
+        self.file = open(filename, "wb")
+        return True
+
+    def close(self):
+        self.file.close()
+
+    # Writes an ASCII string without terminator
+    def writeAsciiStr(self, v):
+        self.file.write(bytes(v, "ascii"))
+
+    # Writes a 32 bits unsigned int
+    def writeUInt(self, v):
+        self.file.write(struct.pack("<I", v))
+
+    # Writes a 16 bits unsigned int
+    def writeUShort(self, v):
+        self.file.write(struct.pack("<H", v))
+
+    # Writes one 8 bits unsigned byte
+    def writeUByte(self, v):
+        self.file.write(struct.pack("<B", v))
+
+    # Writes four 32 bits floats .w .x .y .z
+    def writeQuaternion(self, v):
+        self.file.write(struct.pack("<4f", v.w, v.x, v.y, v.z))
+
+    # Writes three 32 bits floats .x .y .z
+    def writeVector(self, v):
+        self.file.write(struct.pack("<3f", v.x, v.y, v.z))
+
+    # Writes a 32 bits float
+    def writeFloat(self, v):
+        self.file.write(struct.pack("<f", v))
+
+        
+def UrhoWriteModel(model, filename):
+
+    if not model.vertexBuffers or not model.indexBuffers or not model.geometries:
+        log.error("No model data to export in {:s}".format(filename))
+        return
+
+    fw = BinaryFileWriter()
+    try:
+        fw.open(filename)
+    except Exception as e:
+        log.error("Cannot open file {:s} {:s}".format(filename, e))
+        return
+
+    # File Identifier
+    fw.writeAsciiStr("UMDL")
+    
+    # Number of vertex buffers
+    fw.writeUInt(len(model.vertexBuffers))
+    # For each vertex buffer
+    for buffer in model.vertexBuffers:
+        # Vertex count
+        fw.writeUInt(len(buffer.vertices))
+        # Vertex element mask (determines vertex size)
+        mask = buffer.elementMask
+        fw.writeUInt(mask)
+        # Morphable vertex range start index
+        fw.writeUInt(buffer.morphMinIndex)
+        # Morphable vertex count
+        if buffer.morphMaxIndex != 0:
+            fw.writeUInt(buffer.morphMaxIndex - buffer.morphMinIndex + 1)
+        else:
+            fw.writeUInt(0)
+        # Vertex data (vertex count * vertex size)
+        for vertex in buffer.vertices:
+            if mask & ELEMENT_POSITION:
+                fw.writeVector(vertex.pos)
+            if mask & ELEMENT_NORMAL:
+                fw.writeVector(vertex.normal)
+            if mask & ELEMENT_COLOR:
+                for c in vertex.color:
+                    fw.writeUByte(c)
+            if mask & ELEMENT_UV1:
+                for uv in vertex.uv:
+                    fw.writeFloat(uv)
+            if mask & ELEMENT_TANGENT:
+                fw.writeVector(vertex.tangent)
+                fw.writeFloat(vertex.tangent.w)
+            if mask & ELEMENT_BWEIGHTS:
+                for iw in vertex.weights:
+                    fw.writeFloat(iw[1])
+            if mask & ELEMENT_BINDICES:
+                for iw in vertex.weights:
+                    fw.writeUByte(iw[0])
+
+    # Number of index buffers
+    fw.writeUInt(len(model.indexBuffers))
+    # For each index buffer
+    for buffer in model.indexBuffers:
+        # Index count
+        fw.writeUInt(len(buffer.indexes))
+        # Index size (2 for 16-bit indices, 4 for 32-bit indices)
+        fw.writeUInt(buffer.indexSize)
+        # Index data (index count * index size)
+        for i in buffer.indexes:
+            if buffer.indexSize == 2:
+                fw.writeUShort(i)
+            else:
+                fw.writeUInt(i)
+
+    # Number of geometries
+    fw.writeUInt(len(model.geometries))
+    # For each geometry
+    for geometry in model.geometries:
+        # Number of bone mapping entries
+        fw.writeUInt(len(geometry.boneMap))
+        # For each bone
+        for bone in geometry.boneMap:
+            fw.writeUInt(bone)
+        # Number of LOD levels
+        fw.writeUInt(len(geometry.lodLevels))
+        # For each LOD level
+        for lod in geometry.lodLevels:
+            # LOD distance
+            fw.writeFloat(lod.distance)
+            # Primitive type (0 = triangle list, 1 = line list)
+            fw.writeUInt(lod.primitiveType)
+            # Vertex buffer index, starting from 0
+            fw.writeUInt(lod.vertexBuffer)
+            # Index buffer index, starting from 0
+            fw.writeUInt(lod.indexBuffer)
+            # Draw range: index start
+            fw.writeUInt(lod.startIndex)
+            # Draw range: index count
+            fw.writeUInt(lod.countIndex)
+
+    # Number of morphs
+    fw.writeUInt(len(model.morphs))
+    # For each morph
+    for morph in model.morphs:
+        # Name of morph
+        fw.writeAsciiStr(morph.name)
+        fw.writeUByte(0)
+        # Number of affected vertex buffers
+        fw.writeUInt(len(morph.vertexBufferMap))
+        # For each affected vertex buffers
+        for morphBufferIndex, morphBuffer in sorted(morph.vertexBufferMap.items()):
+            # Vertex buffer index, starting from 0
+            fw.writeUInt(morphBufferIndex)
+            # Vertex element mask for morph data
+            mask = morphBuffer.elementMask
+            fw.writeUInt(mask)
+            # Vertex count
+            fw.writeUInt(len(morphBuffer.vertices))
+            # For each vertex:
+            for vertex in morphBuffer.vertices:
+                # Moprh vertex index
+                fw.writeUInt(vertex.index)
+                # Moprh vertex Position
+                if mask & ELEMENT_POSITION:
+                    fw.writeVector(vertex.pos)
+                # Moprh vertex Normal
+                if mask & ELEMENT_NORMAL:
+                    fw.writeVector(vertex.normal)
+                # Moprh vertex Tangent
+                if mask & ELEMENT_TANGENT:
+                    fw.writeVector(vertex.tangent)
+                    fw.writeFloat(vertex.tangent.w)
+                    
+    # Number of bones (may be 0)
+    fw.writeUInt(len(model.bones))
+    # For each bone
+    for bone in model.bones:
+        # Bone name
+        fw.writeAsciiStr(bone.name)
+        fw.writeUByte(0)
+        # Parent bone index starting from 0
+        fw.writeUInt(bone.parentIndex)
+        # Initial position
+        fw.writeVector(bone.position)
+        # Initial rotation
+        fw.writeQuaternion(bone.rotation)
+        # Initial scale
+        fw.writeVector(bone.scale)
+        # 4x3 offset matrix for skinning
+        for row in bone.inverseMatrix[:3]:
+            for v in row:
+                fw.writeFloat(v)
+        # Bone collision info bitmask
+        fw.writeUByte(bone.collisionMask)
+        # Bone radius
+        if bone.collisionMask & BONE_BOUNDING_SPHERE:
+            fw.writeFloat(bone.radius)
+        # Bone bounding box minimum and maximum
+        if bone.collisionMask & BONE_BOUNDING_BOX:
+            fw.writeVector(bone.boundingBox.min)    
+            fw.writeVector(bone.boundingBox.max)    
+         
+    # Model bounding box minimum  
+    fw.writeVector(model.boundingBox.min)
+    # Model bounding box maximum
+    fw.writeVector(model.boundingBox.max)
+
+    # For each geometry
+    for geometry in model.geometries:
+        # Geometry center
+        fw.writeVector(geometry.center)
+    
+    fw.close()
+
+    
+def UrhoWriteAnimation(animation, filename):
+
+    if not animation.tracks:
+        log.error("No animation data to export in {:s}".format(filename))
+        return
+
+    fw = BinaryFileWriter()
+    try:
+        fw.open(filename)
+    except Exception as e:
+        log.error("Cannot open file {:s} {:s}".format(filename, e))
+        return
+
+    # File Identifier
+    fw.writeAsciiStr("UANI")
+    # Animation name
+    fw.writeAsciiStr(animation.name)
+    fw.writeUByte(0)
+    # Length in seconds
+    fw.writeFloat(animation.length)
+    
+    # Number of tracks
+    fw.writeUInt(len(animation.tracks))
+    # For each track
+    for track in animation.tracks:
+        # Track name (practically same as the bone name that should be driven)
+        fw.writeAsciiStr(track.name)
+        fw.writeUByte(0)
+        # Mask of included animation data
+        mask = track.mask
+        fw.writeUByte(track.mask)
+        
+        # Number of tracks
+        fw.writeUInt(len(track.keyframes))
+        # For each keyframe
+        for keyframe in track.keyframes:
+            # Time position in seconds: float
+            fw.writeFloat(keyframe.time)
+            # Keyframe position
+            if mask & TRACK_POSITION:
+                fw.writeVector(keyframe.position)
+            # Keyframe rotation
+            if mask & TRACK_ROTATION:
+                fw.writeQuaternion(keyframe.rotation)
+            # Keyframe scale
+            if mask & TRACK_SCALE:
+                fw.writeVector(keyframe.scale)
+
+    fw.close()
+
+def UrhoWriteMaterial(material, filename, useStandardDirs):
+
+    try:
+        file = open(filename, "w")
+    except Exception as e:
+        log.error("Cannot open file {:s} {:s}".format(filename, e))
+        return
+    
+    # TODO
+    material.specularColor = Vector((0.0, 0.0, 0.0, 1.0))
+    
+    imageRelPath = material.imageName
+    if useStandardDirs:
+        #imageRelPath = os.path.join("Textures", imageRelPath)
+        imageRelPath = "Textures/" + imageRelPath
+    
+    file.write("<material>\n"
+               "    <technique name=\"Techniques/Diff.xml\" />\n")
+    file.write("    <texture unit=\"diffuse\" name=\"{:s}\" />\n"
+               .format(imageRelPath) )
+    file.write("    <parameter name=\"MatSpecColor\" value=\"{:.1f} {:.1f} {:.1f} {:.1f}\" />\n"
+               .format(material.specularColor[0], material.specularColor[1], material.specularColor[2], material.specularColor[3]) )
+    file.write("</material>")
+
+    file.close()
+
+#---------------------------------------
+
+# NOTE: only different geometries use different buffers
+
+# NOTE: LODs must use the same vertex buffer, and so the same vertices. This means
+# normals and tangents are a bit off, but they are good infact they are approximations 
+# of the first LOD which uses those vertices.
+# Creating a LOD we search for the similar vertex.
+
+# NOTE: vertex buffers can have different mask (ex. skeleton weights)
+
+# NOTE: morph must have what geometry they refer to, or the vertex buffer or better
+# the index buffer as vertex buffer is in common.
+    
+# NOTE: a morph can affect more than one vertex buffer
+
+# NOTE: if a vertex buffer has blendweights then all its vertices must have it
+
+# NOTE: if we use index() we must have __EQ__ in the class.
+# NOTE: don't use index(), it's slow.
+
+#--------------------
+# Urho exporter
+#--------------------
+
+def UrhoExport(tData, uExportOptions, uExportData):
+    
+    uModel = UrhoModel()
+    uModel.name = tData.objectName
+    uExportData.models.append(uModel)    
+    
+    # For each bone
+    for boneName, bone in tData.bonesMap.items():
+        uBoneIndex = len(uModel.bones)
+        # Sanity check for the OrderedDict
+        assert bone.index == uBoneIndex
+        
+        uBone = UrhoBone()
+        uModel.bones.append(uBone)
+        
+        uBone.name = boneName
+        if bone.parentName:
+            # Child bone
+            uBone.parentIndex = tData.bonesMap[bone.parentName].index
+        else:
+            # Root bone
+            uBone.parentIndex = uBoneIndex
+        uBone.position = bone.bindPosition
+        uBone.rotation = bone.bindRotation
+        uBone.scale = bone.bindScale
+        uBone.matrix = bone.worldTransform
+        uBone.inverseMatrix = uBone.matrix.inverted()
+        uBone.derivedPosition = uBone.matrix.to_translation()        
+    
+    totalVertices = len(tData.verticesList) 
+    
+    # Search in geometries for the maximum number of vertices 
+    maxLodVertices = 0
+    for tGeometry in tData.geometriesList:
+        for tLodLevel in tGeometry.lodLevels:
+            vertexCount = len(tLodLevel.indexSet)
+            if vertexCount > maxLodVertices:
+                maxLodVertices = vertexCount
+    
+    # If one big buffer needs a 32 bits index but each geometry needs only a 16 bits
+    # index then try to use a different buffer for each geometry
+    useOneBuffer = True
+    if uExportOptions.splitSubMeshes or (totalVertices > 65535 and maxLodVertices <= 65535):
+        useOneBuffer = False
+
+    # Urho lod vertex buffer
+    vertexBuffer = None
+    # Urho lod index buffer
+    indexBuffer = None
+    # Model bounding box
+    minVertexPos = None
+    maxVertexPos = None
+    # Maps old vertex index to Urho vertex buffer index and Urho vertex index
+    modelIndexMap = {}
+
+    # For each geometry
+    for tGeometry in tData.geometriesList:
+        
+        uGeometry = UrhoGeometry()
+        uModel.geometries.append(uGeometry)
+
+        # Start value for geometry center (one for each geometry)
+        center = Vector((0.0, 0.0, 0.0))
+        
+        # For each LOD level
+        for i, tLodLevel in enumerate(tGeometry.lodLevels):
+            uLodLevel = UrhoLodLevel()
+            uGeometry.lodLevels.append(uLodLevel)
+            
+            if i == 0 and tLodLevel.distance != 0.0:
+                log.warning("First lod of object {:s} should have 0.0 distance".format(uModel.name))
+
+            uLodLevel.distance = tLodLevel.distance
+            uLodLevel.primitiveType = TRIANGLE_LIST
+
+            # If needed add a new vertex buffer (only for first lod of a geometry)
+            if vertexBuffer is None or (i == 0 and not useOneBuffer):
+                vertexBuffer = UrhoVertexBuffer()
+                uModel.vertexBuffers.append(vertexBuffer)
+                uVerticesMap = {}
+
+            # If needed add a new index buffer (only for first lod of a geometry)
+            if indexBuffer is None or (i == 0 and not useOneBuffer):
+                indexBuffer = UrhoIndexBuffer()
+                uModel.indexBuffers.append(indexBuffer)
+                uLodLevel.startIndex = 0
+            else:
+                uLodLevel.startIndex = len(indexBuffer.indexes)    
+
+            # Set how many indices the LOD level will use
+            uLodLevel.countIndex = len(tLodLevel.triangleList) * 3
+            # Set lod vertex and index buffers
+            uLodLevel.vertexBuffer = len(uModel.vertexBuffers) - 1
+            uLodLevel.indexBuffer = len(uModel.indexBuffers) - 1
+            
+            # Maps old vertex index to new vertex index in the new Urho buffer
+            indexMap = {}
+            
+            # Add vertices to the vertex buffer
+            for tVertexIndex in tLodLevel.indexSet:
+            
+                tVertex = tData.verticesList[tVertexIndex]
+
+                # Create a Urho vertex
+                try:
+                    uVertex = UrhoVertex(tVertex, vertexBuffer)
+                except MaskError as e:
+                    log.warning("Incompatible vertex element mask in object {:s} ({:s})".format(uModel.name, e))
+                                
+                # All this code do is "uVertexIndex = vertexBuffer.vertices.index(uVertex)", but we use
+                # a map to speed up.
+            
+                # Get an hash of the vertex (more vertices can have the same hash)
+                uVertexHash = hash(uVertex)
+            
+                try:
+                    # Get the list of vertices indices with the same hash
+                    uVerticesMapList = uVerticesMap[uVertexHash]
+                except KeyError:
+                    # If the hash is not mapped, create a new list (we should use sets but lists are faster)
+                    uVerticesMapList = []
+                    uVerticesMap[uVertexHash] = uVerticesMapList
+                
+                # For each index in the list, get the corresponding vertex and test if it is equal to tVertex.
+                # If Position, Normal and UV are the same, it must be the same vertex, get its index.
+                uVertexIndex = None
+                for ivl in uVerticesMapList:
+                    if vertexBuffer.vertices[ivl] == uVertex:
+                        uVertexIndex = ivl
+                        break
+
+                # If we cannot find it, the vertex is new, add it to the list, and its index to the map list
+                if uVertexIndex is None:
+                    uVertexIndex = len(vertexBuffer.vertices)
+                    vertexBuffer.vertices.append(uVertex)
+                    uVerticesMapList.append(uVertexIndex)
+                    if i != 0:
+                        log.warning("LOD {:d} of object {:s} has new vertices.".format(i, uModel.name))
+                
+                # Populate the 'old tVertex index' to 'new uVertex index' map
+                if not tVertexIndex in indexMap:
+                    indexMap[tVertexIndex] = uVertexIndex
+                elif indexMap[tVertexIndex] != uVertexIndex:
+                    log.error("Conflict in vertex index map of object {:s}".format(uModel.name))
+                
+                '''    
+                # Limit weights count to 4 and normalize them
+                if (vertexBuffer.elementMask & ELEMENT_BLEND) == ELEMENT_BLEND:
+                    # Sort tuples (index, weight) by decreasing weight
+                    sortedList = sorted(uVertex.weights, key = operator.itemgetter(1), reverse = True)
+                    # Cleat the vertex weights list and delete the old tuples (maybe)
+                    uVertex.weights[:] = []
+                    # Sum the first 4 weights
+                    totalWeight = sum([t[1] for t in sortedList[:4]])
+                    # Keep only the first 4 tuples, map index, normalize weights, add at least 4 tuples
+                    for i in range(4):
+                        t = (0, 0.0)
+                        if i < len(sortedList):
+                            t = sortedList[i]
+                            t = (t[0], t[1] / totalWeight)
+                        uVertex.weights.append(t) 
+                '''
+                
+                # Update the model bounding box (common to all geometries)
+                if vertexBuffer.elementMask & ELEMENT_POSITION:
+                    uModel.boundingBox.merge(uVertex.pos)
+
+            # Add the local vertex map to the global map
+            for oldIndex, newIndex in indexMap.items():
+                # We create a map: Map[old index] = Set( Tuple(new buffer index, new vertex index) )
+                # Search if this vertex index was already mapped, get its Set or add a new one.
+                # We need a Set because a vertex can be copied in more than one vertex buffer.
+                try:
+                    vbviSet = modelIndexMap[oldIndex]
+                except KeyError:
+                    vbviSet = set()
+                    modelIndexMap[oldIndex] = vbviSet
+                # Add a tuple to the Set: new buffer index, new vertex index
+                vbvi = (uLodLevel.vertexBuffer, newIndex)
+                vbviSet.add(vbvi)
+                
+            # Add indices to the index buffer
+            centerCount = 0
+            for triangle in tLodLevel.triangleList:
+                for tVertexIndex in triangle:
+                    uVertexIndex = indexMap[tVertexIndex]
+                    indexBuffer.indexes.append(uVertexIndex)
+                    # Update geometry center (only for the first LOD)
+                    if (i == 0) and (vertexBuffer.elementMask & ELEMENT_POSITION):
+                        centerCount += 1
+                        center += vertexBuffer.vertices[uVertexIndex].pos;
+
+            # Update geometry center (only for the first LOD)
+            if i == 0 and centerCount:
+                uGeometry.center = center / centerCount;
+                        
+            # If this geometry has bone weights but the number of total bones is over the limit 
+            # then let's hope our geometry uses only a subset of the total bones within the limit.
+            # If this is true then we can remap the original bone index, which can be over the 
+            # limit, to a local, in this geometry, bone index within the limit.
+            if (len(uModel.bones) > MAX_SKIN_MATRICES and 
+               (vertexBuffer.elementMask & ELEMENT_BLEND) == ELEMENT_BLEND):
+                # For each vertex in the buffer
+                for vertex in vertexBuffer.vertices:
+                    for i, (boneIndex, weight) in enumerate(vertex.weights):
+                        # Search if the bone is already present in the map
+                        try:
+                            newBoneIndex = uGeometry.boneMap.index(boneIndex)
+                        except ValueError:
+                            # New bone, add it in the map
+                            newBoneIndex = len(uGeometry.boneMap)
+                            if newBoneIndex < MAX_SKIN_MATRICES:
+                                uGeometry.boneMap.append(boneIndex)
+                            else:
+                                log.error("Too many bones in object {:s} geometry {:d}.".format(uModel.name, i))
+                                newBoneIndex = 0
+                                weight = 0.0
+                        # Change from the global bone index to the local bone index
+                        vertex.weights[i] = (newBoneIndex, weight)
+
+    if tData.geometriesList and uModel.boundingBox.min is None:
+        uModel.boundingBox.min = Vector((0.0, 0.0, 0.0))
+        uModel.boundingBox.max = Vector((0.0, 0.0, 0.0))
+        log.warning("Vertices of object {:s} have no position.".format(uModel.name))
+
+    # Set index size for indexes buffers
+    for uIndexBuffer in uModel.indexBuffers:
+        if len(uIndexBuffer.indexes) > 65535:
+            # 32 bits indexes
+            uIndexBuffer.indexSize = 4
+        else:
+            # 16 bits indexes
+            uIndexBuffer.indexSize = 2
+
+    # Update bones bounding sphere and box
+    # For each vertex buffer
+    for uVertexBuffer in uModel.vertexBuffers:
+        # Skip if the buffer doesn't have bone weights
+        if (uVertexBuffer.elementMask & ELEMENT_BLEND) != ELEMENT_BLEND:
+            continue
+        # For each vertex in the buffer
+        for uVertex in uVertexBuffer.vertices:
+            vertexPos = uVertex.pos
+            for boneIndex, weight in uVertex.weights:
+                # The 0.33 threshold check is to avoid including vertices in the bone hitbox 
+                # to which the bone contributes only a little. It is rather arbitrary. (Lasse)
+                if weight > 0.33:
+                    uBone = uModel.bones[boneIndex]
+                    # Bone head position (in model space)
+                    bonePos = uBone.derivedPosition
+                    # Distance between vertex and bone head
+                    distance = (bonePos - vertexPos).length
+                    # Search for the maximum distance
+                    if uBone.radius is None or distance > uBone.radius:
+                        uBone.collisionMask |= BONE_BOUNDING_SPHERE
+                        uBone.radius = distance
+                    # Calculate the vertex position in bone space
+                    boneVertexPos = uBone.inverseMatrix * vertexPos
+                    # Update the bone boundingBox
+                    uBone.collisionMask |= BONE_BOUNDING_BOX
+                    uBone.boundingBox.merge(boneVertexPos)
+
+    
+    for tMorph in tData.morphsList:
+        uMorph = UrhoVertexMorph()
+        uMorph.name = tMorph.name
+        uModel.morphs.append(uMorph)
+        
+        # For each vertex affected by the morph
+        for tVertexIndex, tMorphVertex in tMorph.vertexMap.items():
+            # Get the correspondent Urho vertex buffer and vertex index (there can be more than one)
+            vbviSet = modelIndexMap[tVertexIndex]
+            # For each corresponding vertex buffer
+            for uVertexBufferIndex, uVertexIndex in vbviSet:
+                # Search for the vertex buffer in the morph, if not present add it
+                try:
+                    uMorphVertexBuffer = uMorph.vertexBufferMap[uVertexBufferIndex]
+                except KeyError:
+                    uMorphVertexBuffer = UrhoVertexBuffer()
+                    uMorph.vertexBufferMap[uVertexBufferIndex] = uMorphVertexBuffer
+                
+                # Create the morphed vertex
+                try:
+                    uMorphVertex = UrhoVertex(tMorphVertex, uMorphVertexBuffer)
+                except MaskError as e:
+                    log.warning("Incompatible vertex element mask in morph {:s} of object {:s} ({:s})".format(uMorph.name, uModel.name, e))
+
+                # Get the original vertex
+                uVertexBuffer = uModel.vertexBuffers[uVertexBufferIndex]
+                uVertex = uVertexBuffer.vertices[uVertexIndex]
+                
+                # Calculate morph values (pos, normal, tangent) relative to the original vertex
+                uMorphVertex.subtract(uVertex, uMorphVertexBuffer.elementMask)
+                    
+                # Add the vertex to the morph buffer
+                uMorphVertex.index = uVertexIndex
+                uMorphVertexBuffer.vertices.append(uMorphVertex)
+
+                # Update min and max morphed vertex index in the vertex buffer
+                if uVertexBuffer.morphMinIndex is None:
+                    uVertexBuffer.morphMinIndex = uVertexIndex
+                    uVertexBuffer.morphMaxIndex = uVertexIndex
+                elif uVertexIndex < uVertexBuffer.morphMinIndex:
+                    uVertexBuffer.morphMinIndex = uVertexIndex
+                elif uVertexIndex > uVertexBuffer.morphMaxIndex:
+                    uVertexBuffer.morphMaxIndex = uVertexIndex
+
+    # Set to zero min and max morphed vertex index of buffers with no morphs
+    for i, uVertexBuffer in enumerate(uModel.vertexBuffers):
+        if uVertexBuffer.morphMinIndex is None:
+            uVertexBuffer.morphMinIndex = 0
+            uVertexBuffer.morphMaxIndex = 0
+
+            
+    uAnimations = uExportData.animations
+    for tAnimation in tData.animationsList:
+        uAnimation = UrhoAnimation()
+        uAnimation.name = tAnimation.name
+        uAnimation.length = None
+        
+        for tTrack in tAnimation.tracks:
+            uTrack = UrhoTrack()
+            uTrack.name = tTrack.name
+            uTrack.mask = None
+            
+            for tFrame in tTrack.frames:
+                try:
+                    uKeyframe = UrhoKeyframe(tFrame, uTrack)
+                except MaskError as e:
+                    log.warning("Incompatible element mask in track {:s} of animation {:s} ({:s})".format(uTrack.name, uAnimation.name, e))
+                uTrack.keyframes.append(uKeyframe)
+
+            # Make sure keyframes are sorted from beginning to end
+            uTrack.keyframes.sort(key = operator.attrgetter('time'))
+
+            # Add only tracks with keyframes
+            if uTrack.keyframes and uTrack.mask:
+                uAnimation.tracks.append(uTrack)
+                # Update animation length
+                length = uTrack.keyframes[-1].time
+                if uAnimation.length is None or uAnimation.length < length:
+                    uAnimation.length = length
+        
+        # Add only animations with tracks
+        if uAnimation.tracks:
+            uAnimations.append(uAnimation)
+    
+                

+ 121 - 0
Source/Extras/BlenderExporter/io_mesh_urho/testing.py

@@ -0,0 +1,121 @@
+
+PRINTMASK_COORD     = 0x0001
+PRINTMASK_NORMAL    = 0x0002
+PRINTMASK_UV        = 0x0004
+PRINTMASK_TANGENT   = 0x0008
+PRINTMASK_WEIGHT    = 0x0010
+
+def PrintUrhoData(data, mask):
+    print()
+    for model in data.models:
+        print("Model: " + model.name)        
+        for ivb,vb in enumerate(model.vertexBuffers):
+            print(" Vertex buffer {:d}".format(ivb) )
+            print("  Mask: 0x{:X}".format(vb.elementMask) )
+            for iv,v in enumerate(vb.vertices):
+                print("  Vertex {:d}".format(iv) )
+                if mask & PRINTMASK_COORD:
+                    print("    Coord: {:+.3f} {:+.3f} {:+.3f}".format(v.pos.x, v.pos.y, v.pos.z) )
+                if mask & PRINTMASK_NORMAL:
+                    print("   Normal: {:+.3f} {:+.3f} {:+.3f}".format(v.normal.x, v.normal.y, v.normal.z) )
+                if mask & PRINTMASK_UV:
+                    print("        Uv: {:+.3f} {:+.3f}".format(v.uv[0], v.uv[1]) )
+                if mask & PRINTMASK_TANGENT:
+                    print("  Tangent: {:+.3f} {:+.3f} {:+.3f} {:+.3f}".format(v.tangent.x, v.tangent.y, v.tangent.z, v.tangent.w) )
+                if mask & PRINTMASK_WEIGHT:
+                    print("   Weights:", end='')
+                    for w in v.weights:
+                        print("  {:d} {:.3f}".format(w[0],w[1]), end='' )
+                    print()
+                #if iv > 20:
+                #    break
+        for iib,ib in enumerate(model.indexBuffers):
+            print(" Index buffer {:d}".format(iib) )
+            for iix,ix in enumerate(ib.indexes):
+                if iix and (iix % 12) == 0:
+                    print()
+                if (iix % 3) == 0:
+                    print("|", end='' )
+                print(" {:3d}".format(ix), end='' )
+            print()
+        for ib, bone in enumerate(model.bones):
+            print(" Bone {:d} {:s}".format(ib, bone.name) )
+            print("   parent index: {:d}".format(bone.parentIndex) )
+            print("       position: " +str(bone.position) )
+            print("       rotation: " +str(bone.rotation) )
+            print("          scale: " +str(bone.scale) )
+            print("  collisionMask: " +str(bone.collisionMask) )
+            print("         radius: " +str(bone.radius) )
+            print("  inverseMatrix:\n" +str(bone.inverseMatrix) )
+        for morph in model.morphs:
+            print(" Morph {:s}".format(morph.name) )
+            for ivb, vb in morph.vertexBufferMap.items():
+                print("  Vertex buffer {:d}".format(ivb) )
+                print("  Mask: 0x{:X}".format(vb.elementMask) )
+                for iv,v in enumerate(vb.vertices):
+                    print("  Vertex {:d}".format(iv) )
+                    if mask & PRINTMASK_COORD:
+                        print("    Coord: {:+.3f} {:+.3f} {:+.3f}".format(v.pos.x, v.pos.y, v.pos.z) )
+                    if mask & PRINTMASK_NORMAL:
+                        print("   Normal: {:+.3f} {:+.3f} {:+.3f}".format(v.normal.x, v.normal.y, v.normal.z) )
+                    if mask & PRINTMASK_UV:
+                        print("        Uv: {:+.3f} {:+.3f}".format(v.uv[0], v.uv[1]) )
+                    if mask & PRINTMASK_TANGENT:
+                        print("  Tangent: {:+.3f} {:+.3f} {:+.3f} {:+.3f}".format(v.tangent.x, v.tangent.y, v.tangent.z, v.tangent.w) )
+
+    print()
+
+
+def PrintVerts(mesh):        
+    print("Mesh: " + mesh.name)
+    print(" Vertices: ", len(mesh.vertices))
+    for i, vt in enumerate(mesh.vertices):
+        print("{:3d}  coords: ".format(vt.index) + str(vt.co))
+        print("    normals: " +str(vt.normal))
+        if i > 12: break
+
+def PrintAll(tData):    
+    print('Vertexes:')
+    for i,v in enumerate(tData.verticesList):
+        print(i)
+        print(v)
+        if i > 20:
+            break
+
+    print('\nMorphs:')
+    for i,v in enumerate(tData.morphsList):
+        print(i)
+        print(v)
+
+    print('\nMaterials:')
+    for i,v in enumerate(tData.materialsList):
+        print(i)
+        print(v)
+
+    print('\nGeometries:')
+    for i,v in enumerate(tData.geometriesList):
+        print(i)
+        print(v)
+
+    print('\nSkeleton:')
+    for (k,v) in sorted(tData.bonesMap.items()):
+        print(k)
+        print(v)
+                
+def PrintMesh(mesh):        
+    print("Mesh: " + mesh.name)
+    print(" vertices: ", len(mesh.vertices))
+    print(" faces: " + str(len(mesh.polygons)))
+    for vt in mesh.vertices:
+        print(" Vertex ", vt.index)
+        print("  coords: " + str(vt.co))
+        print("  normals: " +str(vt.normal))
+    for i,fc in enumerate(mesh.polygons):
+        print(" Polygon ", i)
+        print("  normal: " +str(fc.normal))
+        print("  indexes: ", end="")
+        for v in fc.vertices:
+            print(v, " ", end="")
+        print()
+
+    

+ 7 - 2
Source/Extras/Readme.txt

@@ -1,12 +1,17 @@
+BlenderExporter
+
+- Contributed by reattiva. Read installation instructions from the Readme.txt
+  file within the subdirectory
+
 OgreBatchConverter
 
-- Contributed by Carlo Carollo. Converts multiple Ogre .mesh.xml files (also from 
+- Contributed by Carlo Carollo. Converts multiple Ogre .mesh.xml files (also from
   subdirectories) by invoking the OgreImporter tool. Use the CMake option
   ENABLE_EXTRAS to include in the build.
 
 OgreMaxscriptExport
 
-- Contributed by Vladimir Pobedinsky. A modified version of the Maxscript 
+- Contributed by Vladimir Pobedinsky. A modified version of the Maxscript
   Exporter from the Ogre SDK that will import Ogre .mesh.xml files (for feeding
   into OgreImporter) and materials in Urho3D .xml format.