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
 - Firegorilla
 - Magic.Lixin
 - Magic.Lixin
 - amadeus_osa
 - amadeus_osa
+- reattiva
 - skaiware
 - skaiware
 
 
 Urho3D is greatly inspired by OGRE (http://www.ogre3d.org/) and Horde3D (http://www.horde3d.org/). Additional inspiration & research used:
 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
 - Firegorilla
 - Magic.Lixin
 - Magic.Lixin
 - amadeus_osa
 - amadeus_osa
+- reattiva
 - skaiware
 - skaiware
 
 
 Urho3D is greatly inspired by OGRE (http://www.ogre3d.org) and Horde3D
 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
 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
   subdirectories) by invoking the OgreImporter tool. Use the CMake option
   ENABLE_EXTRAS to include in the build.
   ENABLE_EXTRAS to include in the build.
 
 
 OgreMaxscriptExport
 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
   Exporter from the Ogre SDK that will import Ogre .mesh.xml files (for feeding
   into OgreImporter) and materials in Urho3D .xml format.
   into OgreImporter) and materials in Urho3D .xml format.