diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py
new file mode 100755
index 0000000000000000000000000000000000000000..0e864cad3466e29d4ed6845f98a0966c3231c76a
--- /dev/null
+++ b/io_scene_gltf2/__init__.py
@@ -0,0 +1,579 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Imports
+#
+
+import os
+import bpy
+from bpy_extras.io_utils import ImportHelper, ExportHelper
+from bpy.types import Operator, AddonPreferences
+
+from .io.com.gltf2_io_debug import print_console, Log
+from .io.imp.gltf2_io_gltf import glTFImporter
+from .blender.imp.gltf2_blender_gltf import BlenderGlTF
+
+from bpy.props import (CollectionProperty,
+                       StringProperty,
+                       BoolProperty,
+                       EnumProperty,
+                       FloatProperty,
+                       IntProperty)
+
+#
+# Globals
+#
+
+bl_info = {
+    'name': 'glTF 2.0 format',
+    'author': 'Julien Duroure, Norbert Nopper, Urs Hanselmann & Moritz Becher',
+    "version": (0, 0, 1),
+    'blender': (2, 80, 0),
+    'location': 'File > Import-Export',
+    'description': 'Import-Export as glTF 2.0',
+    'warning': '',
+    'wiki_url': ''
+                '',
+    'support': 'COMMUNITY',
+    'category': 'Import-Export'}
+
+
+#
+#  Functions / Classes.
+#
+
+
+class GLTF2ExportSettings(bpy.types.Operator):
+    """Save the export settings on export (saved in .blend).
+Toggle off to clear settings"""
+    bl_label = "Save Settings"
+    bl_idname = "scene.gltf2_export_settings_set"
+
+    def execute(self, context):
+        operator = context.active_operator
+        operator.will_save_settings = not operator.will_save_settings
+        if not operator.will_save_settings:
+            # clear settings
+            context.scene.pop(operator.scene_key)
+        return {"FINISHED"}
+
+
+class ExportGLTF2_Base:
+
+    # TODO: refactor to avoid boilerplate
+
+    export_copyright: StringProperty(
+        name='Copyright',
+        description='',
+        default=''
+    )
+
+    export_embed_buffers: BoolProperty(
+        name='Embed buffers',
+        description='',
+        default=False
+    )
+
+    export_embed_images: BoolProperty(
+        name='Embed images',
+        description='',
+        default=False
+    )
+
+    export_strip: BoolProperty(
+        name='Strip delimiters',
+        description='',
+        default=False
+    )
+
+    export_indices: EnumProperty(
+        name='Maximum indices',
+        items=(('UNSIGNED_BYTE', 'Unsigned Byte', ''),
+               ('UNSIGNED_SHORT', 'Unsigned Short', ''),
+               ('UNSIGNED_INT', 'Unsigned Integer', '')),
+        default='UNSIGNED_INT'
+    )
+
+    export_force_indices: BoolProperty(
+        name='Force maximum indices',
+        description='',
+        default=False
+    )
+
+    export_texcoords: BoolProperty(
+        name='Export texture coordinates',
+        description='',
+        default=True
+    )
+
+    export_normals: BoolProperty(
+        name='Export normals',
+        description='',
+        default=True
+    )
+
+    export_tangents: BoolProperty(
+        name='Export tangents',
+        description='',
+        default=True
+    )
+
+    export_materials: BoolProperty(
+        name='Export materials',
+        description='',
+        default=True
+    )
+
+    export_colors: BoolProperty(
+        name='Export colors',
+        description='',
+        default=True
+    )
+
+    export_cameras: BoolProperty(
+        name='Export cameras',
+        description='',
+        default=False
+    )
+
+    export_camera_infinite: BoolProperty(
+        name='Infinite perspective Camera',
+        description='',
+        default=False
+    )
+
+    export_selected: BoolProperty(
+        name='Export selected only',
+        description='',
+        default=False
+    )
+
+    export_layers: BoolProperty(
+        name='Export all layers',
+        description='',
+        default=True
+    )
+
+    export_extras: BoolProperty(
+        name='Export extras',
+        description='',
+        default=False
+    )
+
+    export_yup: BoolProperty(
+        name='Convert Z up to Y up',
+        description='',
+        default=True
+    )
+
+    export_apply: BoolProperty(
+        name='Apply modifiers',
+        description='',
+        default=False
+    )
+
+    export_animations: BoolProperty(
+        name='Export animations',
+        description='',
+        default=True
+    )
+
+    export_frame_range: BoolProperty(
+        name='Export within playback range',
+        description='',
+        default=True
+    )
+
+    export_frame_step: IntProperty(
+        name='Frame step size',
+        description='Step size (in frames) for animation export.',
+        default=1,
+        min=1,
+        max=120
+    )
+
+    export_move_keyframes: BoolProperty(
+        name='Keyframes start with 0',
+        description='',
+        default=True
+    )
+
+    export_force_sampling: BoolProperty(
+        name='Force sample animations',
+        description='',
+        default=False
+    )
+
+    export_current_frame: BoolProperty(
+        name='Export current frame',
+        description='',
+        default=True
+    )
+
+    export_skins: BoolProperty(
+        name='Export skinning',
+        description='',
+        default=True
+    )
+
+    export_bake_skins: BoolProperty(
+        name='Bake skinning constraints',
+        description='',
+        default=False
+    )
+
+    export_morph: BoolProperty(
+        name='Export morphing',
+        description='',
+        default=True
+    )
+
+    export_morph_normal: BoolProperty(
+        name='Export morphing normals',
+        description='',
+        default=True
+    )
+
+    export_morph_tangent: BoolProperty(
+        name='Export morphing tangents',
+        description='',
+        default=True
+    )
+
+    export_lights: BoolProperty(
+        name='Export KHR_lights_punctual',
+        description='',
+        default=False
+    )
+
+    export_texture_transform: BoolProperty(
+        name='Export KHR_texture_transform',
+        description='',
+        default=False
+    )
+
+    export_displacement: BoolProperty(
+        name='Export KHR_materials_displacement',
+        description='',
+        default=False
+    )
+
+    will_save_settings: BoolProperty(default=False)
+
+    # Custom scene property for saving settings
+    scene_key = "glTF2ExportSettings"
+
+    #
+
+    def invoke(self, context, event):
+        settings = context.scene.get(self.scene_key)
+        self.will_save_settings = False
+        if settings:
+            try:
+                for (k, v) in settings.items():
+                    setattr(self, k, v)
+                self.will_save_settings = True
+
+            except AttributeError:
+                self.report({"ERROR"}, "Loading export settings failed. Removed corrupted settings")
+                del context.scene[self.scene_key]
+
+        return ExportHelper.invoke(self, context, event)
+
+    def save_settings(self, context):
+        # find all export_ props
+        all_props = self.properties
+        export_props = {x: all_props.get(x) for x in dir(all_props)
+                        if x.startswith("export_") and all_props.get(x) is not None}
+
+        context.scene[self.scene_key] = export_props
+
+    def execute(self, context):
+        from .blender.exp import gltf2_blender_export
+
+        if self.will_save_settings:
+            self.save_settings(context)
+
+        # All custom export settings are stored in this container.
+        export_settings = {}
+
+        export_settings['gltf_filepath'] = bpy.path.ensure_ext(self.filepath, self.filename_ext)
+        export_settings['gltf_filedirectory'] = os.path.dirname(export_settings['gltf_filepath']) + '/'
+
+        export_settings['gltf_format'] = self.export_format
+        export_settings['gltf_copyright'] = self.export_copyright
+        export_settings['gltf_embed_buffers'] = self.export_embed_buffers
+        export_settings['gltf_embed_images'] = self.export_embed_images
+        export_settings['gltf_strip'] = self.export_strip
+        export_settings['gltf_indices'] = self.export_indices
+        export_settings['gltf_force_indices'] = self.export_force_indices
+        export_settings['gltf_texcoords'] = self.export_texcoords
+        export_settings['gltf_normals'] = self.export_normals
+        export_settings['gltf_tangents'] = self.export_tangents and self.export_normals
+        export_settings['gltf_materials'] = self.export_materials
+        export_settings['gltf_colors'] = self.export_colors
+        export_settings['gltf_cameras'] = self.export_cameras
+        if self.export_cameras:
+            export_settings['gltf_camera_infinite'] = self.export_camera_infinite
+        else:
+            export_settings['gltf_camera_infinite'] = False
+        export_settings['gltf_selected'] = self.export_selected
+        export_settings['gltf_layers'] = self.export_layers
+        export_settings['gltf_extras'] = self.export_extras
+        export_settings['gltf_yup'] = self.export_yup
+        export_settings['gltf_apply'] = self.export_apply
+        export_settings['gltf_animations'] = self.export_animations
+        if self.export_animations:
+            export_settings['gltf_current_frame'] = False
+            export_settings['gltf_frame_range'] = self.export_frame_range
+            export_settings['gltf_move_keyframes'] = self.export_move_keyframes
+            export_settings['gltf_force_sampling'] = self.export_force_sampling
+        else:
+            export_settings['gltf_current_frame'] = self.export_current_frame
+            export_settings['gltf_frame_range'] = False
+            export_settings['gltf_move_keyframes'] = False
+            export_settings['gltf_force_sampling'] = False
+        export_settings['gltf_skins'] = self.export_skins
+        if self.export_skins:
+            export_settings['gltf_bake_skins'] = self.export_bake_skins
+        else:
+            export_settings['gltf_bake_skins'] = False
+        export_settings['gltf_frame_step'] = self.export_frame_step
+        export_settings['gltf_morph'] = self.export_morph
+        if self.export_morph:
+            export_settings['gltf_morph_normal'] = self.export_morph_normal
+        else:
+            export_settings['gltf_morph_normal'] = False
+        if self.export_morph and self.export_morph_normal:
+            export_settings['gltf_morph_tangent'] = self.export_morph_tangent
+        else:
+            export_settings['gltf_morph_tangent'] = False
+
+        export_settings['gltf_lights'] = self.export_lights
+        export_settings['gltf_texture_transform'] = self.export_texture_transform
+        export_settings['gltf_displacement'] = self.export_displacement
+
+        export_settings['gltf_binary'] = bytearray()
+        export_settings['gltf_binaryfilename'] = os.path.splitext(os.path.basename(self.filepath))[0] + '.bin'
+
+        return gltf2_blender_export.save(self, context, export_settings)
+
+    def draw(self, context):
+        layout = self.layout
+
+        #
+
+        col = layout.box().column()
+        col.label(text='Embedding:')  # , icon='PACKAGE')
+        col.prop(self, 'export_copyright')
+        if self.export_format == 'ASCII':
+            col.prop(self, 'export_embed_buffers')
+            col.prop(self, 'export_embed_images')
+            col.prop(self, 'export_strip')
+
+        col = layout.box().column()
+        col.label(text='Nodes:')  # , icon='OOPS')
+        col.prop(self, 'export_selected')
+        col.prop(self, 'export_layers')
+        col.prop(self, 'export_extras')
+        col.prop(self, 'export_yup')
+
+        col = layout.box().column()
+        col.label(text='Meshes:')  # , icon='MESH_DATA')
+        col.prop(self, 'export_apply')
+        col.prop(self, 'export_indices')
+        col.prop(self, 'export_force_indices')
+
+        col = layout.box().column()
+        col.label(text='Attributes:')  # , icon='SURFACE_DATA')
+        col.prop(self, 'export_texcoords')
+        col.prop(self, 'export_normals')
+        if self.export_normals:
+            col.prop(self, 'export_tangents')
+        col.prop(self, 'export_colors')
+
+        col = layout.box().column()
+        col.label(text='Objects:')  # , icon='OBJECT_DATA')
+        col.prop(self, 'export_cameras')
+        if self.export_cameras:
+            col.prop(self, 'export_camera_infinite')
+
+        col = layout.box().column()
+        col.label(text='Materials:')  # , icon='MATERIAL_DATA')
+        col.prop(self, 'export_materials')
+        col.prop(self, 'export_texture_transform')
+
+        col = layout.box().column()
+        col.label(text='Animation:')  # , icon='OUTLINER_DATA_POSE')
+        col.prop(self, 'export_animations')
+        if self.export_animations:
+            col.prop(self, 'export_frame_range')
+            col.prop(self, 'export_frame_step')
+            col.prop(self, 'export_move_keyframes')
+            col.prop(self, 'export_force_sampling')
+        else:
+            col.prop(self, 'export_current_frame')
+        col.prop(self, 'export_skins')
+        if self.export_skins:
+            col.prop(self, 'export_bake_skins')
+        col.prop(self, 'export_morph')
+        if self.export_morph:
+            col.prop(self, 'export_morph_normal')
+            if self.export_morph_normal:
+                col.prop(self, 'export_morph_tangent')
+
+        addon_prefs = context.user_preferences.addons[__name__].preferences
+        if addon_prefs.experimental:
+            col = layout.box().column()
+            col.label(text='Experimental:')  # , icon='RADIO')
+            col.prop(self, 'export_lights')
+            col.prop(self, 'export_displacement')
+
+        row = layout.row()
+        row.operator(
+            GLTF2ExportSettings.bl_idname,
+            text=GLTF2ExportSettings.bl_label,
+            icon="%s" % "PINNED" if self.will_save_settings else "UNPINNED")
+
+# TODO: refactor operators to single operator for both cases
+
+class ExportGLTF2_GLTF(bpy.types.Operator, ExportGLTF2_Base, ExportHelper):
+    """Export scene as glTF 2.0 file"""
+    bl_idname = 'export_scene.gltf'
+    bl_label = 'Export glTF 2.0'
+
+    filename_ext = '.gltf'
+    filter_glob: StringProperty(default='*.gltf', options={'HIDDEN'})
+
+    export_format = 'ASCII'
+
+
+class ExportGLTF2_GLB(bpy.types.Operator, ExportGLTF2_Base, ExportHelper):
+    """Export scene as glTF 2.0 file"""
+    bl_idname = 'export_scene.glb'
+    bl_label = 'Export glTF 2.0 binary'
+
+    filename_ext = '.glb'
+    filter_glob: StringProperty(default='*.glb', options={'HIDDEN'})
+
+    export_format = 'BINARY'
+
+
+def menu_func_export_gltf(self, context):
+    self.layout.operator(ExportGLTF2_GLTF.bl_idname, text='glTF 2.0 (.gltf)')
+
+
+def menu_func_export_glb(self, context):
+    self.layout.operator(ExportGLTF2_GLB.bl_idname, text='glTF 2.0 (.glb)')
+
+
+class ExportGLTF2_AddonPreferences(AddonPreferences):
+    bl_idname = __name__
+
+    experimental: BoolProperty(name='Enable experimental glTF export settings', default=False)
+
+    def draw(self, context):
+        layout = self.layout
+        layout.prop(self, "experimental")
+
+
+class ImportglTF2(Operator, ImportHelper):
+    bl_idname = 'import_scene.gltf'
+    bl_label = "glTF 2.0 (.gltf/.glb)"
+
+    filter_glob: StringProperty(default="*.gltf;*.glb", options={'HIDDEN'})
+
+    loglevel: EnumProperty(items=Log.get_levels(), name="Log Level", default=Log.default())
+
+    import_pack_images: BoolProperty(
+        name='Pack images',
+        description='',
+        default=True
+    )
+
+    import_shading: EnumProperty(
+        name="Shading",
+        items=(("NORMALS", "Use Normal Data", ""),
+               ("FLAT", "Flat Shading", ""),
+               ("SMOOTH", "Smooth Shading", "")),
+        default="NORMALS")
+
+    def draw(self, context):
+        layout = self.layout
+
+        layout.prop(self, 'loglevel')
+        layout.prop(self, 'import_pack_images')
+        layout.prop(self, 'import_shading')
+
+    def execute(self, context):
+        return self.import_gltf2(context)
+
+    def import_gltf2(self, context):
+        import_settings = self.as_keywords()
+
+        self.gltf_importer = glTFImporter(self.filepath, import_settings)
+        success, txt = self.gltf_importer.read()
+        if not success:
+            self.report({'ERROR'}, txt)
+            return {'CANCELLED'}
+        success, txt = self.gltf_importer.checks()
+        if not success:
+            self.report({'ERROR'}, txt)
+            return {'CANCELLED'}
+        self.gltf_importer.log.critical("Data are loaded, start creating Blender stuff")
+        BlenderGlTF.create(self.gltf_importer)
+        self.gltf_importer.log.critical("glTF import is now finished")
+        self.gltf_importer.log.removeHandler(self.gltf_importer.log_handler)
+
+        # Switch to newly created main scene
+        bpy.context.window.scene = bpy.data.scenes[self.gltf_importer.blender_scene]
+
+        return {'FINISHED'}
+
+
+def menu_func_import(self, context):
+    self.layout.operator(ImportglTF2.bl_idname, text=ImportglTF2.bl_label)
+
+
+classes = (
+    GLTF2ExportSettings,
+    ExportGLTF2_GLTF,
+    ExportGLTF2_GLB,
+    ExportGLTF2_AddonPreferences,
+    ImportglTF2
+)
+
+
+def register():
+    for c in classes:
+        bpy.utils.register_class(c)
+    # bpy.utils.register_module(__name__)
+
+    # add to the export / import menu
+    bpy.types.TOPBAR_MT_file_export.append(menu_func_export_gltf)
+    bpy.types.TOPBAR_MT_file_export.append(menu_func_export_glb)
+    bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
+
+
+def unregister():
+    for c in classes:
+        bpy.utils.unregister_class(c)
+    # bpy.utils.unregister_module(__name__)
+
+    # remove from the export / import menu
+    bpy.types.TOPBAR_MT_file_export.remove(menu_func_export_gltf)
+    bpy.types.TOPBAR_MT_file_export.remove(menu_func_export_glb)
+    bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
+
diff --git a/io_scene_gltf2/blender/com/gltf2_blender_conversion.py b/io_scene_gltf2/blender/com/gltf2_blender_conversion.py
new file mode 100755
index 0000000000000000000000000000000000000000..95fa292ded0dbc104abffddc1f97a21d1efe7b13
--- /dev/null
+++ b/io_scene_gltf2/blender/com/gltf2_blender_conversion.py
@@ -0,0 +1,42 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from mathutils import Matrix, Quaternion
+
+def matrix_gltf_to_blender(mat_input):
+    """Matrix from glTF format to Blender format."""
+    mat = Matrix([mat_input[0:4], mat_input[4:8], mat_input[8:12], mat_input[12:16]])
+    mat.transpose()
+    return mat
+
+def loc_gltf_to_blender(loc):
+    """Location."""
+    return loc
+
+def scale_gltf_to_blender(scale):
+    """Scaling."""
+    return scale
+
+def quaternion_gltf_to_blender(q):
+    """Quaternion from glTF to Blender."""
+    return Quaternion([q[3], q[0], q[1], q[2]])
+
+def scale_to_matrix(scale):
+    """Scale to matrix."""
+    mat = Matrix()
+    for i in range(3):
+        mat[i][i] = scale[i]
+
+    return mat
+
diff --git a/io_scene_gltf2/blender/com/gltf2_blender_data_path.py b/io_scene_gltf2/blender/com/gltf2_blender_data_path.py
new file mode 100755
index 0000000000000000000000000000000000000000..c5ce40250375a7ea228d67d8853774f2f9c5938f
--- /dev/null
+++ b/io_scene_gltf2/blender/com/gltf2_blender_data_path.py
@@ -0,0 +1,28 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+def get_target_property_name(data_path: str) -> str:
+    """Retrieve target property."""
+    return data_path.rsplit('.', 1)[-1]
+
+
+def get_target_object_path(data_path: str) -> str:
+    """Retrieve target object data path without property"""
+    path_split = data_path.rsplit('.', 1)
+    self_targeting = len(path_split) < 2
+    if self_targeting:
+        return ""
+    return path_split[0]
+
diff --git a/io_scene_gltf2/blender/com/gltf2_blender_image.py b/io_scene_gltf2/blender/com/gltf2_blender_image.py
new file mode 100755
index 0000000000000000000000000000000000000000..7564070d556eef71b1d2c3c315410d481f9834b1
--- /dev/null
+++ b/io_scene_gltf2/blender/com/gltf2_blender_image.py
@@ -0,0 +1,32 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Imports
+#
+
+from ...io.com.gltf2_io_image import create_img_from_pixels
+
+
+def create_img_from_blender_image(blender_image):
+    """
+    Create a new image object using the given blender image.
+
+    Returns the created image object.
+    """
+    if blender_image is None:
+        return None
+
+    return create_img_from_pixels(blender_image.size[0], blender_image.size[1], blender_image.pixels[:])
+
diff --git a/io_scene_gltf2/blender/com/gltf2_blender_image_util.py b/io_scene_gltf2/blender/com/gltf2_blender_image_util.py
new file mode 100755
index 0000000000000000000000000000000000000000..e2563a52e065b202490ccb4b770fceb20d89a38e
--- /dev/null
+++ b/io_scene_gltf2/blender/com/gltf2_blender_image_util.py
@@ -0,0 +1,121 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import shutil
+import bpy
+import zlib
+import struct
+from io_scene_gltf2.blender.exp import gltf2_blender_get
+
+
+def create_image_file(context, blender_image, dst_path, file_format):
+    """Create JPEG or PNG file from a given Blender image."""
+    # Check, if source image exists e.g. does not exist if image is packed.
+    file_exists = 1
+    try:
+        src_path = bpy.path.abspath(blender_image.filepath, library=blender_image.library)
+        file = open(src_path)
+    except IOError:
+        file_exists = 0
+    else:
+        file.close()
+
+    if file_exists == 0:
+        # Image does not exist on disk ...
+        blender_image.filepath = dst_path
+        # ... so save it.
+        blender_image.save()
+
+    elif file_format == blender_image.file_format:
+        # Copy source image to destination, keeping original format.
+
+        src_path = bpy.path.abspath(blender_image.filepath, library=blender_image.library)
+
+        # Required for comapre.
+        src_path = src_path.replace('\\', '/')
+        dst_path = dst_path.replace('\\', '/')
+
+        # Check that source and destination path are not the same using os.path.abspath
+        # because bpy.path.abspath seems to not always return an absolute path
+        if os.path.abspath(dst_path) != os.path.abspath(src_path):
+            shutil.copyfile(src_path, dst_path)
+
+    else:
+        # Render a new image to destination, converting to target format.
+
+        # TODO: Reusing the existing scene means settings like exposure are applied on export,
+        # which we don't want, but I'm not sure how to create a new Scene object through the
+        # Python API. See: https://github.com/KhronosGroup/glTF-Blender-Exporter/issues/184.
+
+        tmp_file_format = context.scene.render.image_settings.file_format
+        tmp_color_depth = context.scene.render.image_settings.color_depth
+
+        context.scene.render.image_settings.file_format = file_format
+        context.scene.render.image_settings.color_depth = '8'
+        blender_image.save_render(dst_path, context.scene)
+
+        context.scene.render.image_settings.file_format = tmp_file_format
+        context.scene.render.image_settings.color_depth = tmp_color_depth
+
+
+def create_image_data(context, export_settings, blender_image, file_format):
+    """Create JPEG or PNG byte array from a given Blender image."""
+    if blender_image is None:
+        return None
+
+    if file_format == 'PNG':
+        return _create_png_data(blender_image)
+    else:
+        return _create_jpg_data(context, export_settings, blender_image)
+
+
+def _create_jpg_data(context, export_settings, blender_image):
+    """Create a JPEG byte array from a given Blender image."""
+    uri = gltf2_blender_get.get_image_uri(export_settings, blender_image)
+    path = export_settings['gltf_filedirectory'] + uri
+
+    create_image_file(context, blender_image, path, 'JPEG')
+
+    jpg_data = open(path, 'rb').read()
+    os.remove(path)
+
+    return jpg_data
+
+
+def _create_png_data(blender_image):
+    """Create a PNG byte array from a given Blender image."""
+    width, height = blender_image.size
+
+    buf = bytearray([int(channel * 255.0) for channel in blender_image.pixels])
+
+    #
+    # Taken from 'blender-thumbnailer.py' in Blender.
+    #
+
+    # reverse the vertical line order and add null bytes at the start
+    width_byte_4 = width * 4
+    raw_data = b"".join(
+        b'\x00' + buf[span:span + width_byte_4] for span in range((height - 1) * width * 4, -1, - width_byte_4))
+
+    def png_pack(png_tag, data):
+        chunk_head = png_tag + data
+        return struct.pack("!I", len(data)) + chunk_head + struct.pack("!I", 0xFFFFFFFF & zlib.crc32(chunk_head))
+
+    return b"".join([
+        b'\x89PNG\r\n\x1a\n',
+        png_pack(b'IHDR', struct.pack("!2I5B", width, height, 8, 6, 0, 0, 0)),
+        png_pack(b'IDAT', zlib.compress(raw_data, 9)),
+        png_pack(b'IEND', b'')])
+
diff --git a/io_scene_gltf2/blender/com/gltf2_blender_json.py b/io_scene_gltf2/blender/com/gltf2_blender_json.py
new file mode 100755
index 0000000000000000000000000000000000000000..fbf833c1dff45a973ddfc4ba5c62050efc3f7589
--- /dev/null
+++ b/io_scene_gltf2/blender/com/gltf2_blender_json.py
@@ -0,0 +1,38 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import json
+import bpy
+
+
+class BlenderJSONEncoder(json.JSONEncoder):
+    """Blender JSON Encoder."""
+
+    def default(self, obj):
+        if isinstance(obj, bpy.types.ID):
+            return dict(
+                name=obj.name,
+                type=obj.__class__.__name__
+            )
+        return super(BlenderJSONEncoder, self).default(obj)
+
+
+def is_json_convertible(data):
+    """Test, if a data set can be expressed as JSON."""
+    try:
+        json.dumps(data, cls=BlenderJSONEncoder)
+        return True
+    except:
+        return False
+
diff --git a/io_scene_gltf2/blender/com/gltf2_blender_material_helpers.py b/io_scene_gltf2/blender/com/gltf2_blender_material_helpers.py
new file mode 100755
index 0000000000000000000000000000000000000000..05f359544a4e816f3550584ef07a94e32a12dd6a
--- /dev/null
+++ b/io_scene_gltf2/blender/com/gltf2_blender_material_helpers.py
@@ -0,0 +1,59 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+def get_output_node(node_tree):
+    """Retrive output node."""
+    output = [node for node in node_tree.nodes if node.type == 'OUTPUT_MATERIAL'][0]
+    return output
+
+
+def get_output_surface_input(node_tree):
+    """Retrieve surface input of output node."""
+    output_node = get_output_node(node_tree)
+    return output_node.inputs['Surface']
+
+
+def get_diffuse_texture(node_tree):
+    """Retrieve diffuse texture node."""
+    for node in node_tree.nodes:
+        print(node.name)
+        if node.label == 'BASE COLOR':
+            return node
+
+    return None
+
+
+def get_preoutput_node_output(node_tree):
+    """Retrieve node just before output node."""
+    output_node = get_output_node(node_tree)
+    preoutput_node = output_node.inputs['Surface'].links[0].from_node
+
+    # Pre output node is Principled BSDF or any BSDF => BSDF
+    if 'BSDF' in preoutput_node.type:
+        return preoutput_node.outputs['BSDF']
+    elif 'SHADER' in preoutput_node.type:
+        return preoutput_node.outputs['Shader']
+    else:
+        print(preoutput_node.type)
+
+
+def get_base_color_node(node_tree):
+    """Returns the last node of the diffuse block."""
+    for node in node_tree.nodes:
+        if node.label == 'BASE COLOR':
+            return node
+
+    return None
+
diff --git a/io_scene_gltf2/blender/com/gltf2_blender_math.py b/io_scene_gltf2/blender/com/gltf2_blender_math.py
new file mode 100755
index 0000000000000000000000000000000000000000..26eb639681245a306505308426734353e0356500
--- /dev/null
+++ b/io_scene_gltf2/blender/com/gltf2_blender_math.py
@@ -0,0 +1,159 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+import typing
+import math
+from mathutils import Matrix, Vector, Quaternion, Euler
+
+from io_scene_gltf2.blender.com.gltf2_blender_data_path import get_target_property_name
+
+
+def multiply(a, b):
+    """Multiplication."""
+    return a @ b
+
+
+def list_to_mathutils(values: typing.List[float], data_path: str) -> typing.Union[Vector, Quaternion, Euler]:
+    """Transform a list to blender py object."""
+    target = get_target_property_name(data_path)
+
+    if target == 'location':
+        return Vector(values)
+    elif target == 'rotation_axis_angle':
+        angle = values[0]
+        axis = values[1:]
+        return Quaternion(axis, math.radians(angle))
+    elif target == 'rotation_euler':
+        return Euler(values).to_quaternion()
+    elif target == 'rotation_quaternion':
+        return Quaternion(values)
+    elif target == 'scale':
+        return Vector(values)
+    elif target == 'value':
+        return values
+
+    return values
+
+
+def mathutils_to_gltf(x: typing.Union[Vector, Quaternion]) -> typing.List[float]:
+    """Transform a py object to glTF list."""
+    if isinstance(x, Vector):
+        return list(x)
+    if isinstance(x, Quaternion):
+        # Blender has w-first quaternion notation
+        return [x[1], x[2], x[3], x[0]]
+    else:
+        return list(x)
+
+
+def to_yup() -> Matrix:
+    """Transform to Yup."""
+    return Matrix(
+        ((1.0, 0.0, 0.0, 0.0),
+         (0.0, 0.0, 1.0, 0.0),
+         (0.0, -1.0, 0.0, 0.0),
+         (0.0, 0.0, 0.0, 1.0))
+    )
+
+
+to_zup = to_yup
+
+
+def swizzle_yup(v: typing.Union[Vector, Quaternion], data_path: str) -> typing.Union[Vector, Quaternion]:
+    """Manage Yup."""
+    target = get_target_property_name(data_path)
+    swizzle_func = {
+        "location": swizzle_yup_location,
+        "rotation_axis_angle": swizzle_yup_rotation,
+        "rotation_euler": swizzle_yup_rotation,
+        "rotation_quaternion": swizzle_yup_rotation,
+        "scale": swizzle_yup_scale,
+        "value": swizzle_yup_value
+    }.get(target)
+
+    if swizzle_func is None:
+        raise RuntimeError("Cannot transform values at {}".format(data_path))
+
+    return swizzle_func(v)
+
+
+def swizzle_yup_location(loc: Vector) -> Vector:
+    """Manage Yup location."""
+    return Vector((loc[0], loc[2], -loc[1]))
+
+
+def swizzle_yup_rotation(rot: Quaternion) -> Quaternion:
+    """Manage Yup rotation."""
+    return Quaternion((rot[0], rot[1], rot[3], -rot[2]))
+
+
+def swizzle_yup_scale(scale: Vector) -> Vector:
+    """Manage Yup scale."""
+    return Vector((scale[0], scale[2], scale[1]))
+
+
+def swizzle_yup_value(value: typing.Any) -> typing.Any:
+    """Manage Yup value."""
+    return value
+
+
+def transform(v: typing.Union[Vector, Quaternion], data_path: str, transform: Matrix = Matrix.Identity(4)) -> typing \
+        .Union[Vector, Quaternion]:
+    """Manage transformations."""
+    target = get_target_property_name(data_path)
+    transform_func = {
+        "location": transform_location,
+        "rotation_axis_angle": transform_rotation,
+        "rotation_euler": transform_rotation,
+        "rotation_quaternion": transform_rotation,
+        "scale": transform_scale,
+        "value": transform_value
+    }.get(target)
+
+    if transform_func is None:
+        raise RuntimeError("Cannot transform values at {}".format(data_path))
+
+    return transform_func(v, transform)
+
+
+def transform_location(location: Vector, transform: Matrix = Matrix.Identity(4)) -> Vector:
+    """Transform location."""
+    m = Matrix.Translation(location)
+    m = multiply(transform, m)
+    return m.to_translation()
+
+
+def transform_rotation(rotation: Quaternion, transform: Matrix = Matrix.Identity(4)) -> Quaternion:
+    """Transform rotation."""
+    m = rotation.to_matrix().to_4x4()
+    m = multiply(transform, m)
+    return m.to_quaternion()
+
+
+def transform_scale(scale: Vector, transform: Matrix = Matrix.Identity(4)) -> Vector:
+    """Transform scale."""
+    m = Matrix.Identity(4)
+    m[0][0] = scale.x
+    m[1][1] = scale.y
+    m[2][2] = scale.z
+    m = multiply(transform, m)
+
+    return m.to_scale()
+
+
+def transform_value(value: Vector, _: Matrix = Matrix.Identity(4)) -> Vector:
+    """Transform value."""
+    return value
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_animate.py b/io_scene_gltf2/blender/exp/gltf2_blender_animate.py
new file mode 100755
index 0000000000000000000000000000000000000000..e4b1148726b062b9c04fe6c77781eb6ca01ba0e5
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_animate.py
@@ -0,0 +1,638 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Imports
+#
+
+import bpy
+from . import gltf2_blender_export_keys
+from . import gltf2_blender_extract
+from mathutils import Matrix, Quaternion, Euler
+
+
+#
+# Globals
+#
+
+JOINT_NODE = 'JOINT'
+
+NEEDS_CONVERSION = 'CONVERSION_NEEDED'
+CUBIC_INTERPOLATION = 'CUBICSPLINE'
+LINEAR_INTERPOLATION = 'LINEAR'
+STEP_INTERPOLATION = 'STEP'
+BEZIER_INTERPOLATION = 'BEZIER'
+CONSTANT_INTERPOLATION = 'CONSTANT'
+
+
+#
+# Functions
+#
+
+def animate_get_interpolation(export_settings, blender_fcurve_list):
+    """
+    Retrieve the glTF interpolation, depending on a fcurve list.
+
+    Blender allows mixing and more variations of interpolations.
+    In such a case, a conversion is needed.
+    """
+    if export_settings[gltf2_blender_export_keys.FORCE_SAMPLING]:
+        return NEEDS_CONVERSION
+
+    #
+
+    interpolation = None
+
+    keyframe_count = None
+
+    for blender_fcurve in blender_fcurve_list:
+        if blender_fcurve is None:
+            continue
+
+        #
+
+        current_keyframe_count = len(blender_fcurve.keyframe_points)
+
+        if keyframe_count is None:
+            keyframe_count = current_keyframe_count
+
+        if current_keyframe_count > 0 > blender_fcurve.keyframe_points[0].co[0]:
+            return NEEDS_CONVERSION
+
+        if keyframe_count != current_keyframe_count:
+            return NEEDS_CONVERSION
+
+        #
+
+        for blender_keyframe in blender_fcurve.keyframe_points:
+            is_bezier = blender_keyframe.interpolation == BEZIER_INTERPOLATION
+            is_linear = blender_keyframe.interpolation == LINEAR_INTERPOLATION
+            is_constant = blender_keyframe.interpolation == CONSTANT_INTERPOLATION
+
+            if interpolation is None:
+                if is_bezier:
+                    interpolation = CUBIC_INTERPOLATION
+                elif is_linear:
+                    interpolation = LINEAR_INTERPOLATION
+                elif is_constant:
+                    interpolation = STEP_INTERPOLATION
+                else:
+                    interpolation = NEEDS_CONVERSION
+                    return interpolation
+            else:
+                if is_bezier and interpolation != CUBIC_INTERPOLATION:
+                    interpolation = NEEDS_CONVERSION
+                    return interpolation
+                elif is_linear and interpolation != LINEAR_INTERPOLATION:
+                    interpolation = NEEDS_CONVERSION
+                    return interpolation
+                elif is_constant and interpolation != STEP_INTERPOLATION:
+                    interpolation = NEEDS_CONVERSION
+                    return interpolation
+                elif not is_bezier and not is_linear and not is_constant:
+                    interpolation = NEEDS_CONVERSION
+                    return interpolation
+
+    if interpolation is None:
+        interpolation = NEEDS_CONVERSION
+
+    return interpolation
+
+
+def animate_convert_rotation_axis_angle(axis_angle):
+    """Convert an axis angle to a quaternion rotation."""
+    q = Quaternion((axis_angle[1], axis_angle[2], axis_angle[3]), axis_angle[0])
+
+    return [q.x, q.y, q.z, q.w]
+
+
+def animate_convert_rotation_euler(euler, rotation_mode):
+    """Convert an euler angle to a quaternion rotation."""
+    rotation = Euler((euler[0], euler[1], euler[2]), rotation_mode).to_quaternion()
+
+    return [rotation.x, rotation.y, rotation.z, rotation.w]
+
+
+def animate_convert_keys(key_list):
+    """Convert Blender key frames to glTF time keys depending on the applied frames per second."""
+    times = []
+
+    for key in key_list:
+        times.append(key / bpy.context.scene.render.fps)
+
+    return times
+
+
+def animate_gather_keys(export_settings, fcurve_list, interpolation):
+    """
+    Merge and sort several key frames to one set.
+
+    If an interpolation conversion is needed, the sample key frames are created as well.
+    """
+    keys = []
+
+    frame_start = bpy.context.scene.frame_start
+    frame_end = bpy.context.scene.frame_end
+
+    if interpolation == NEEDS_CONVERSION:
+        start = None
+        end = None
+
+        for blender_fcurve in fcurve_list:
+            if blender_fcurve is None:
+                continue
+
+            if start is None:
+                start = blender_fcurve.range()[0]
+            else:
+                start = min(start, blender_fcurve.range()[0])
+
+            if end is None:
+                end = blender_fcurve.range()[1]
+            else:
+                end = max(end, blender_fcurve.range()[1])
+
+            #
+
+            add_epsilon_keyframe = False
+            for blender_keyframe in blender_fcurve.keyframe_points:
+                if add_epsilon_keyframe:
+                    key = blender_keyframe.co[0] - 0.001
+
+                    if key not in keys:
+                        keys.append(key)
+
+                    add_epsilon_keyframe = False
+
+                if blender_keyframe.interpolation == CONSTANT_INTERPOLATION:
+                    add_epsilon_keyframe = True
+
+            if add_epsilon_keyframe:
+                key = end - 0.001
+
+                if key not in keys:
+                    keys.append(key)
+
+        key = start
+        while key <= end:
+            if not export_settings[gltf2_blender_export_keys.FRAME_RANGE] or (frame_start <= key <= frame_end):
+                keys.append(key)
+            key += export_settings[gltf2_blender_export_keys.FRAME_STEP]
+
+        keys.sort()
+
+    else:
+        for blender_fcurve in fcurve_list:
+            if blender_fcurve is None:
+                continue
+
+            for blender_keyframe in blender_fcurve.keyframe_points:
+                key = blender_keyframe.co[0]
+                if not export_settings[gltf2_blender_export_keys.FRAME_RANGE] or (frame_start <= key <= frame_end):
+                    if key not in keys:
+                        keys.append(key)
+
+        keys.sort()
+
+    return keys
+
+
+def animate_location(export_settings, location, interpolation, node_type, node_name, action_name, matrix_correction,
+                     matrix_basis):
+    """Calculate/gather the key value pairs for location transformations."""
+    joint_cache = export_settings[gltf2_blender_export_keys.JOINT_CACHE][action_name]
+    if not joint_cache.get(node_name):
+        joint_cache[node_name] = {}
+
+    keys = animate_gather_keys(export_settings, location, interpolation)
+
+    times = animate_convert_keys(keys)
+
+    result = {}
+    result_in_tangent = {}
+    result_out_tangent = {}
+
+    keyframe_index = 0
+    for timeIndex, time in enumerate(times):
+        translation = [0.0, 0.0, 0.0]
+        in_tangent = [0.0, 0.0, 0.0]
+        out_tangent = [0.0, 0.0, 0.0]
+
+        if node_type == JOINT_NODE:
+            if joint_cache[node_name].get(keys[keyframe_index]):
+                translation, tmp_rotation, tmp_scale = joint_cache[node_name][keys[keyframe_index]]
+            else:
+                bpy.context.scene.frame_set(keys[keyframe_index])
+
+                matrix = matrix_correction * matrix_basis
+
+                translation, tmp_rotation, tmp_scale = matrix.decompose()
+
+                joint_cache[node_name][keys[keyframe_index]] = [translation, tmp_rotation, tmp_scale]
+        else:
+            channel_index = 0
+            for blender_fcurve in location:
+
+                if blender_fcurve is not None:
+
+                    if interpolation == CUBIC_INTERPOLATION:
+                        blender_key_frame = blender_fcurve.keyframe_points[keyframe_index]
+
+                        translation[channel_index] = blender_key_frame.co[1]
+
+                        if timeIndex == 0:
+                            in_tangent_value = 0.0
+                        else:
+                            factor = 3.0 / (time - times[timeIndex - 1])
+                            in_tangent_value = (blender_key_frame.co[1] - blender_key_frame.handle_left[1]) * factor
+
+                        if timeIndex == len(times) - 1:
+                            out_tangent_value = 0.0
+                        else:
+                            factor = 3.0 / (times[timeIndex + 1] - time)
+                            out_tangent_value = (blender_key_frame.handle_right[1] - blender_key_frame.co[1]) * factor
+
+                        in_tangent[channel_index] = in_tangent_value
+                        out_tangent[channel_index] = out_tangent_value
+                    else:
+                        value = blender_fcurve.evaluate(keys[keyframe_index])
+
+                        translation[channel_index] = value
+
+                channel_index += 1
+
+            # handle parent inverse
+            matrix = Matrix.Translation(translation)
+            matrix = matrix_correction * matrix
+            translation = matrix.to_translation()
+
+            translation = gltf2_blender_extract.convert_swizzle_location(translation, export_settings)
+            in_tangent = gltf2_blender_extract.convert_swizzle_location(in_tangent, export_settings)
+            out_tangent = gltf2_blender_extract.convert_swizzle_location(out_tangent, export_settings)
+
+        result[time] = translation
+        result_in_tangent[time] = in_tangent
+        result_out_tangent[time] = out_tangent
+
+        keyframe_index += 1
+
+    return result, result_in_tangent, result_out_tangent
+
+
+def animate_rotation_axis_angle(export_settings, rotation_axis_angle, interpolation, node_type, node_name, action_name,
+                                matrix_correction, matrix_basis):
+    """Calculate/gather the key value pairs for axis angle transformations."""
+    joint_cache = export_settings[gltf2_blender_export_keys.JOINT_CACHE][action_name]
+    if not joint_cache.get(node_name):
+        joint_cache[node_name] = {}
+
+    keys = animate_gather_keys(export_settings, rotation_axis_angle, interpolation)
+
+    times = animate_convert_keys(keys)
+
+    result = {}
+
+    keyframe_index = 0
+    for time in times:
+        axis_angle_rotation = [1.0, 0.0, 0.0, 0.0]
+
+        if node_type == JOINT_NODE:
+            if joint_cache[node_name].get(keys[keyframe_index]):
+                tmp_location, rotation, tmp_scale = joint_cache[node_name][keys[keyframe_index]]
+            else:
+                bpy.context.scene.frame_set(keys[keyframe_index])
+
+                matrix = matrix_correction * matrix_basis
+
+                tmp_location, rotation, tmp_scale = matrix.decompose()
+
+                joint_cache[node_name][keys[keyframe_index]] = [tmp_location, rotation, tmp_scale]
+        else:
+            channel_index = 0
+            for blender_fcurve in rotation_axis_angle:
+                if blender_fcurve is not None:
+                    value = blender_fcurve.evaluate(keys[keyframe_index])
+
+                    axis_angle_rotation[channel_index] = value
+
+                channel_index += 1
+
+            rotation = animate_convert_rotation_axis_angle(axis_angle_rotation)
+
+            # handle parent inverse
+            rotation = Quaternion((rotation[3], rotation[0], rotation[1], rotation[2]))
+            matrix = rotation.to_matrix().to_4x4()
+            matrix = matrix_correction * matrix
+            rotation = matrix.to_quaternion()
+
+            # Bring back to internal Quaternion notation.
+            rotation = gltf2_blender_extract.convert_swizzle_rotation(
+                [rotation[0], rotation[1], rotation[2], rotation[3]], export_settings)
+
+        # Bring back to glTF Quaternion notation.
+        rotation = [rotation[1], rotation[2], rotation[3], rotation[0]]
+
+        result[time] = rotation
+
+        keyframe_index += 1
+
+    return result
+
+
+def animate_rotation_euler(export_settings, rotation_euler, rotation_mode, interpolation, node_type, node_name,
+                           action_name, matrix_correction, matrix_basis):
+    """Calculate/gather the key value pairs for euler angle transformations."""
+    joint_cache = export_settings[gltf2_blender_export_keys.JOINT_CACHE][action_name]
+    if not joint_cache.get(node_name):
+        joint_cache[node_name] = {}
+
+    keys = animate_gather_keys(export_settings, rotation_euler, interpolation)
+
+    times = animate_convert_keys(keys)
+
+    result = {}
+
+    keyframe_index = 0
+    for time in times:
+        euler_rotation = [0.0, 0.0, 0.0]
+
+        if node_type == JOINT_NODE:
+            if joint_cache[node_name].get(keys[keyframe_index]):
+                tmp_location, rotation, tmp_scale = joint_cache[node_name][keys[keyframe_index]]
+            else:
+                bpy.context.scene.frame_set(keys[keyframe_index])
+
+                matrix = matrix_correction * matrix_basis
+
+                tmp_location, rotation, tmp_scale = matrix.decompose()
+
+                joint_cache[node_name][keys[keyframe_index]] = [tmp_location, rotation, tmp_scale]
+        else:
+            channel_index = 0
+            for blender_fcurve in rotation_euler:
+                if blender_fcurve is not None:
+                    value = blender_fcurve.evaluate(keys[keyframe_index])
+
+                    euler_rotation[channel_index] = value
+
+                channel_index += 1
+
+            rotation = animate_convert_rotation_euler(euler_rotation, rotation_mode)
+
+            # handle parent inverse
+            rotation = Quaternion((rotation[3], rotation[0], rotation[1], rotation[2]))
+            matrix = rotation.to_matrix().to_4x4()
+            matrix = matrix_correction * matrix
+            rotation = matrix.to_quaternion()
+
+            # Bring back to internal Quaternion notation.
+            rotation = gltf2_blender_extract.convert_swizzle_rotation(
+                [rotation[0], rotation[1], rotation[2], rotation[3]], export_settings)
+
+        # Bring back to glTF Quaternion notation.
+        rotation = [rotation[1], rotation[2], rotation[3], rotation[0]]
+
+        result[time] = rotation
+
+        keyframe_index += 1
+
+    return result
+
+
+def animate_rotation_quaternion(export_settings, rotation_quaternion, interpolation, node_type, node_name, action_name,
+                                matrix_correction, matrix_basis):
+    """Calculate/gather the key value pairs for quaternion transformations."""
+    joint_cache = export_settings[gltf2_blender_export_keys.JOINT_CACHE][action_name]
+    if not joint_cache.get(node_name):
+        joint_cache[node_name] = {}
+
+    keys = animate_gather_keys(export_settings, rotation_quaternion, interpolation)
+
+    times = animate_convert_keys(keys)
+
+    result = {}
+    result_in_tangent = {}
+    result_out_tangent = {}
+
+    keyframe_index = 0
+    for timeIndex, time in enumerate(times):
+        rotation = [1.0, 0.0, 0.0, 0.0]
+        in_tangent = [1.0, 0.0, 0.0, 0.0]
+        out_tangent = [1.0, 0.0, 0.0, 0.0]
+
+        if node_type == JOINT_NODE:
+            if joint_cache[node_name].get(keys[keyframe_index]):
+                tmp_location, rotation, tmp_scale = joint_cache[node_name][keys[keyframe_index]]
+            else:
+                bpy.context.scene.frame_set(keys[keyframe_index])
+
+                matrix = matrix_correction * matrix_basis
+
+                tmp_location, rotation, tmp_scale = matrix.decompose()
+
+                joint_cache[node_name][keys[keyframe_index]] = [tmp_location, rotation, tmp_scale]
+        else:
+            channel_index = 0
+            for blender_fcurve in rotation_quaternion:
+
+                if blender_fcurve is not None:
+                    if interpolation == CUBIC_INTERPOLATION:
+                        blender_key_frame = blender_fcurve.keyframe_points[keyframe_index]
+
+                        rotation[channel_index] = blender_key_frame.co[1]
+
+                        if timeIndex == 0:
+                            in_tangent_value = 0.0
+                        else:
+                            factor = 3.0 / (time - times[timeIndex - 1])
+                            in_tangent_value = (blender_key_frame.co[1] - blender_key_frame.handle_left[1]) * factor
+
+                        if timeIndex == len(times) - 1:
+                            out_tangent_value = 0.0
+                        else:
+                            factor = 3.0 / (times[timeIndex + 1] - time)
+                            out_tangent_value = (blender_key_frame.handle_right[1] - blender_key_frame.co[1]) * factor
+
+                        in_tangent[channel_index] = in_tangent_value
+                        out_tangent[channel_index] = out_tangent_value
+                    else:
+                        value = blender_fcurve.evaluate(keys[keyframe_index])
+
+                        rotation[channel_index] = value
+
+                channel_index += 1
+
+            rotation = Quaternion((rotation[0], rotation[1], rotation[2], rotation[3]))
+            in_tangent = gltf2_blender_extract.convert_swizzle_rotation(in_tangent, export_settings)
+            out_tangent = gltf2_blender_extract.convert_swizzle_rotation(out_tangent, export_settings)
+
+            # handle parent inverse
+            matrix = rotation.to_matrix().to_4x4()
+            matrix = matrix_correction * matrix
+            rotation = matrix.to_quaternion()
+
+            # Bring back to internal Quaternion notation.
+            rotation = gltf2_blender_extract.convert_swizzle_rotation(
+                [rotation[0], rotation[1], rotation[2], rotation[3]], export_settings)
+
+        # Bring to glTF Quaternion notation.
+        rotation = [rotation[1], rotation[2], rotation[3], rotation[0]]
+        in_tangent = [in_tangent[1], in_tangent[2], in_tangent[3], in_tangent[0]]
+        out_tangent = [out_tangent[1], out_tangent[2], out_tangent[3], out_tangent[0]]
+
+        result[time] = rotation
+        result_in_tangent[time] = in_tangent
+        result_out_tangent[time] = out_tangent
+
+        keyframe_index += 1
+
+    return result, result_in_tangent, result_out_tangent
+
+
+def animate_scale(export_settings, scale, interpolation, node_type, node_name, action_name, matrix_correction,
+                  matrix_basis):
+    """Calculate/gather the key value pairs for scale transformations."""
+    joint_cache = export_settings[gltf2_blender_export_keys.JOINT_CACHE][action_name]
+    if not joint_cache.get(node_name):
+        joint_cache[node_name] = {}
+
+    keys = animate_gather_keys(export_settings, scale, interpolation)
+
+    times = animate_convert_keys(keys)
+
+    result = {}
+    result_in_tangent = {}
+    result_out_tangent = {}
+
+    keyframe_index = 0
+    for timeIndex, time in enumerate(times):
+        scale_data = [1.0, 1.0, 1.0]
+        in_tangent = [0.0, 0.0, 0.0]
+        out_tangent = [0.0, 0.0, 0.0]
+
+        if node_type == JOINT_NODE:
+            if joint_cache[node_name].get(keys[keyframe_index]):
+                tmp_location, tmp_rotation, scale_data = joint_cache[node_name][keys[keyframe_index]]
+            else:
+                bpy.context.scene.frame_set(keys[keyframe_index])
+
+                matrix = matrix_correction * matrix_basis
+
+                tmp_location, tmp_rotation, scale_data = matrix.decompose()
+
+                joint_cache[node_name][keys[keyframe_index]] = [tmp_location, tmp_rotation, scale_data]
+        else:
+            channel_index = 0
+            for blender_fcurve in scale:
+
+                if blender_fcurve is not None:
+                    if interpolation == CUBIC_INTERPOLATION:
+                        blender_key_frame = blender_fcurve.keyframe_points[keyframe_index]
+
+                        scale_data[channel_index] = blender_key_frame.co[1]
+
+                        if timeIndex == 0:
+                            in_tangent_value = 0.0
+                        else:
+                            factor = 3.0 / (time - times[timeIndex - 1])
+                            in_tangent_value = (blender_key_frame.co[1] - blender_key_frame.handle_left[1]) * factor
+
+                        if timeIndex == len(times) - 1:
+                            out_tangent_value = 0.0
+                        else:
+                            factor = 3.0 / (times[timeIndex + 1] - time)
+                            out_tangent_value = (blender_key_frame.handle_right[1] - blender_key_frame.co[1]) * factor
+
+                        in_tangent[channel_index] = in_tangent_value
+                        out_tangent[channel_index] = out_tangent_value
+                    else:
+                        value = blender_fcurve.evaluate(keys[keyframe_index])
+
+                        scale_data[channel_index] = value
+
+                channel_index += 1
+
+            scale_data = gltf2_blender_extract.convert_swizzle_scale(scale_data, export_settings)
+            in_tangent = gltf2_blender_extract.convert_swizzle_scale(in_tangent, export_settings)
+            out_tangent = gltf2_blender_extract.convert_swizzle_scale(out_tangent, export_settings)
+
+            # handle parent inverse
+            matrix = Matrix()
+            matrix[0][0] = scale_data.x
+            matrix[1][1] = scale_data.y
+            matrix[2][2] = scale_data.z
+            matrix = matrix_correction * matrix
+            scale_data = matrix.to_scale()
+
+        result[time] = scale_data
+        result_in_tangent[time] = in_tangent
+        result_out_tangent[time] = out_tangent
+
+        keyframe_index += 1
+
+    return result, result_in_tangent, result_out_tangent
+
+
+def animate_value(export_settings, value_parameter, interpolation,
+                  node_type, node_name, matrix_correction, matrix_basis):
+    """Calculate/gather the key value pairs for scalar anaimations."""
+    keys = animate_gather_keys(export_settings, value_parameter, interpolation)
+
+    times = animate_convert_keys(keys)
+
+    result = {}
+    result_in_tangent = {}
+    result_out_tangent = {}
+
+    keyframe_index = 0
+    for timeIndex, time in enumerate(times):
+        value_data = []
+        in_tangent = []
+        out_tangent = []
+
+        for blender_fcurve in value_parameter:
+
+            if blender_fcurve is not None:
+                if interpolation == CUBIC_INTERPOLATION:
+                    blender_key_frame = blender_fcurve.keyframe_points[keyframe_index]
+
+                    value_data.append(blender_key_frame.co[1])
+
+                    if timeIndex == 0:
+                        in_tangent_value = 0.0
+                    else:
+                        factor = 3.0 / (time - times[timeIndex - 1])
+                        in_tangent_value = (blender_key_frame.co[1] - blender_key_frame.handle_left[1]) * factor
+
+                    if timeIndex == len(times) - 1:
+                        out_tangent_value = 0.0
+                    else:
+                        factor = 3.0 / (times[timeIndex + 1] - time)
+                        out_tangent_value = (blender_key_frame.handle_right[1] - blender_key_frame.co[1]) * factor
+
+                    in_tangent.append(in_tangent_value)
+                    out_tangent.append(out_tangent_value)
+                else:
+                    value = blender_fcurve.evaluate(keys[keyframe_index])
+
+                    value_data.append(value)
+
+        result[time] = value_data
+        result_in_tangent[time] = in_tangent
+        result_out_tangent[time] = out_tangent
+
+        keyframe_index += 1
+
+    return result, result_in_tangent, result_out_tangent
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_export.py b/io_scene_gltf2/blender/exp/gltf2_blender_export.py
new file mode 100755
index 0000000000000000000000000000000000000000..ae2db26bb21c159c2eea2153dc6a6e0dcd9ac1b5
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_export.py
@@ -0,0 +1,100 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+import sys
+import traceback
+
+from . import gltf2_blender_export_keys
+from . import gltf2_blender_gather
+from .gltf2_blender_gltf2_exporter import GlTF2Exporter
+from ..com import gltf2_blender_json
+from ...io.exp import gltf2_io_export
+from ...io.com.gltf2_io_debug import print_console, print_newline
+
+
+def save(operator,
+         context,
+         export_settings):
+    """Start the glTF 2.0 export and saves to content either to a .gltf or .glb file."""
+    print_console('INFO', 'Starting glTF 2.0 export')
+    context.window_manager.progress_begin(0, 100)
+    context.window_manager.progress_update(0)
+
+    if not export_settings[gltf2_blender_export_keys.COPYRIGHT]:
+        export_settings[gltf2_blender_export_keys.COPYRIGHT] = None
+
+    scenes, animations = gltf2_blender_gather.gather_gltf2(export_settings)
+    exporter = GlTF2Exporter(copyright=export_settings[gltf2_blender_export_keys.COPYRIGHT])
+    for scene in scenes:
+        exporter.add_scene(scene)
+    for animation in animations:
+        exporter.add_animation(animation)
+
+    buffer = bytes()
+    if export_settings[gltf2_blender_export_keys.FORMAT] == 'ASCII':
+        # .gltf
+        if export_settings[gltf2_blender_export_keys.EMBED_BUFFERS]:
+            exporter.finalize_buffer(export_settings[gltf2_blender_export_keys.FILE_DIRECTORY])
+        else:
+            exporter.finalize_buffer(export_settings[gltf2_blender_export_keys.FILE_DIRECTORY],
+                                     export_settings[gltf2_blender_export_keys.BINARY_FILENAME])
+    else:
+        # .glb
+        buffer = exporter.finalize_buffer(export_settings[gltf2_blender_export_keys.FILE_DIRECTORY], is_glb=True)
+    exporter.finalize_images(export_settings[gltf2_blender_export_keys.FILE_DIRECTORY])
+    glTF = exporter.glTF
+
+    #
+
+    # TODO: move to custom JSON encoder
+    def dict_strip(obj):
+        o = obj
+        if isinstance(obj, dict):
+            o = {}
+            for k, v in obj.items():
+                if v is None:
+                    continue
+                elif isinstance(v, list) and len(v) == 0:
+                    continue
+                o[k] = dict_strip(v)
+        elif isinstance(obj, list):
+            o = []
+            for v in obj:
+                o.append(dict_strip(v))
+        elif isinstance(obj, float):
+            # force floats to int, if they are integers (prevent INTEGER_WRITTEN_AS_FLOAT validator warnings)
+            if int(obj) == obj:
+                return int(obj)
+        return o
+
+    try:
+        gltf2_io_export.save_gltf(dict_strip(glTF.to_dict()), export_settings, gltf2_blender_json.BlenderJSONEncoder,
+                                  buffer)
+    except AssertionError as e:
+        _, _, tb = sys.exc_info()
+        traceback.print_tb(tb)  # Fixed format
+        tb_info = traceback.extract_tb(tb)
+        for tbi in tb_info:
+            filename, line, func, text = tbi
+            print_console('ERROR', 'An error occurred on line {} in statement {}'.format(line, text))
+        print_console('ERROR', str(e))
+        raise e
+
+    print_console('INFO', 'Finished glTF 2.0 export')
+    context.window_manager.progress_end()
+    print_newline()
+
+    return {'FINISHED'}
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_export_keys.py b/io_scene_gltf2/blender/exp/gltf2_blender_export_keys.py
new file mode 100755
index 0000000000000000000000000000000000000000..9e47645a107f7076c99bc460d543808fe3c6dc9e
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_export_keys.py
@@ -0,0 +1,66 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+FILTERED_VERTEX_GROUPS = 'filtered_vertex_groups'
+FILTERED_MESHES = 'filtered_meshes'
+FILTERED_IMAGES = 'filtered_images'
+FILTERED_IMAGES_USE_ALPHA = 'filtered_images_use_alpha'
+FILTERED_MERGED_IMAGES = 'filtered_merged_images'
+FILTERED_TEXTURES = 'filtered_textures'
+FILTERED_MATERIALS = 'filtered_materials'
+FILTERED_LIGHTS = 'filtered_lights'
+TEMPORARY_MESHES = 'temporary_meshes'
+FILTERED_OBJECTS = 'filtered_objects'
+FILTERED_CAMERAS = 'filtered_cameras'
+
+APPLY = 'gltf_apply'
+LAYERS = 'gltf_layers'
+SELECTED = 'gltf_selected'
+SKINS = 'gltf_skins'
+DISPLACEMENT = 'gltf_displacement'
+FORCE_SAMPLING = 'gltf_force_sampling'
+FRAME_RANGE = 'gltf_frame_range'
+FRAME_STEP = 'gltf_frame_step'
+JOINT_CACHE = 'gltf_joint_cache'
+COPYRIGHT = 'gltf_copyright'
+FORMAT = 'gltf_format'
+FILE_DIRECTORY = 'gltf_filedirectory'
+BINARY_FILENAME = 'gltf_binaryfilename'
+YUP = 'gltf_yup'
+MORPH = 'gltf_morph'
+INDICES = 'gltf_indices'
+CAMERA_INFINITE = 'gltf_camera_infinite'
+BAKE_SKINS = 'gltf_bake_skins'
+TEX_COORDS = 'gltf_texcoords'
+COLORS = 'gltf_colors'
+NORMALS = 'gltf_normals'
+TANGENTS = 'gltf_tangents'
+FORCE_INDICES = 'gltf_force_indices'
+MORPH_TANGENT = 'gltf_morph_tangent'
+MORPH_NORMAL = 'gltf_morph_normal'
+MOVE_KEYFRAMES = 'gltf_move_keyframes'
+MATERIALS = 'gltf_materials'
+EXTRAS = 'gltf_extras'
+CAMERAS = 'gltf_cameras'
+LIGHTS = 'gltf_lights'
+ANIMATIONS = 'gltf_animations'
+EMBED_IMAGES = 'gltf_embed_images'
+BINARY = 'gltf_binary'
+EMBED_BUFFERS = 'gltf_embed_buffers'
+TEXTURE_TRANSFORM = 'gltf_texture_transform'
+USE_NO_COLOR = 'gltf_use_no_color'
+
+METALLIC_ROUGHNESS_IMAGE = "metallic_roughness_image"
+GROUP_INDEX = 'group_index'
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_extract.py b/io_scene_gltf2/blender/exp/gltf2_blender_extract.py
new file mode 100755
index 0000000000000000000000000000000000000000..a52201297ee841d36945619aa2a576e24dc68734
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_extract.py
@@ -0,0 +1,1117 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Imports
+#
+
+from mathutils import Vector, Quaternion
+from mathutils.geometry import tessellate_polygon
+
+from . import gltf2_blender_export_keys
+from ...io.com.gltf2_io_debug import print_console
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_skins
+
+#
+# Globals
+#
+
+INDICES_ID = 'indices'
+MATERIAL_ID = 'material'
+ATTRIBUTES_ID = 'attributes'
+
+COLOR_PREFIX = 'COLOR_'
+MORPH_TANGENT_PREFIX = 'MORPH_TANGENT_'
+MORPH_NORMAL_PREFIX = 'MORPH_NORMAL_'
+MORPH_POSITION_PREFIX = 'MORPH_POSITION_'
+TEXCOORD_PREFIX = 'TEXCOORD_'
+WEIGHTS_PREFIX = 'WEIGHTS_'
+JOINTS_PREFIX = 'JOINTS_'
+
+TANGENT_ATTRIBUTE = 'TANGENT'
+NORMAL_ATTRIBUTE = 'NORMAL'
+POSITION_ATTRIBUTE = 'POSITION'
+
+GLTF_MAX_COLORS = 2
+
+
+#
+# Classes
+#
+
+class ShapeKey:
+    def __init__(self, shape_key, vertex_normals, polygon_normals):
+        self.shape_key = shape_key
+        self.vertex_normals = vertex_normals
+        self.polygon_normals = polygon_normals
+
+
+#
+# Functions
+#
+
+def convert_swizzle_location(loc, export_settings):
+    """Convert a location from Blender coordinate system to glTF coordinate system."""
+    if export_settings[gltf2_blender_export_keys.YUP]:
+        return Vector((loc[0], loc[2], -loc[1]))
+    else:
+        return Vector((loc[0], loc[1], loc[2]))
+
+
+def convert_swizzle_tangent(tan, export_settings):
+    """Convert a tangent from Blender coordinate system to glTF coordinate system."""
+    if tan[0] == 0.0 and tan[1] == 0.0 and tan[2] == 0.0:
+        print_console('WARNING', 'Tangent has zero length.')
+
+    if export_settings[gltf2_blender_export_keys.YUP]:
+        return Vector((tan[0], tan[2], -tan[1], 1.0))
+    else:
+        return Vector((tan[0], tan[1], tan[2], 1.0))
+
+
+def convert_swizzle_rotation(rot, export_settings):
+    """
+    Convert a quaternion rotation from Blender coordinate system to glTF coordinate system.
+
+    'w' is still at first position.
+    """
+    if export_settings[gltf2_blender_export_keys.YUP]:
+        return Quaternion((rot[0], rot[1], rot[3], -rot[2]))
+    else:
+        return Quaternion((rot[0], rot[1], rot[2], rot[3]))
+
+
+def convert_swizzle_scale(scale, export_settings):
+    """Convert a scale from Blender coordinate system to glTF coordinate system."""
+    if export_settings[gltf2_blender_export_keys.YUP]:
+        return Vector((scale[0], scale[2], scale[1]))
+    else:
+        return Vector((scale[0], scale[1], scale[2]))
+
+
+def decompose_transition(matrix, context, export_settings):
+    translation, rotation, scale = matrix.decompose()
+    """Decompose a matrix depending if it is associated to a joint or node."""
+    if context == 'NODE':
+        translation = convert_swizzle_location(translation, export_settings)
+        rotation = convert_swizzle_rotation(rotation, export_settings)
+        scale = convert_swizzle_scale(scale, export_settings)
+
+    # Put w at the end.
+    rotation = Quaternion((rotation[1], rotation[2], rotation[3], rotation[0]))
+
+    return translation, rotation, scale
+
+
+def color_srgb_to_scene_linear(c):
+    """
+    Convert from sRGB to scene linear color space.
+
+    Source: Cycles addon implementation, node_color.h.
+    """
+    if c < 0.04045:
+        return 0.0 if c < 0.0 else c * (1.0 / 12.92)
+    else:
+        return pow((c + 0.055) * (1.0 / 1.055), 2.4)
+
+
+def extract_primitive_floor(a, indices, use_tangents):
+    """Shift indices, that the first one starts with 0. It is assumed, that the indices are packed."""
+    attributes = {
+        POSITION_ATTRIBUTE: [],
+        NORMAL_ATTRIBUTE: []
+    }
+
+    if use_tangents:
+        attributes[TANGENT_ATTRIBUTE] = []
+
+    result_primitive = {
+        MATERIAL_ID: a[MATERIAL_ID],
+        INDICES_ID: [],
+        ATTRIBUTES_ID: attributes
+    }
+
+    source_attributes = a[ATTRIBUTES_ID]
+
+    #
+
+    tex_coord_index = 0
+    process_tex_coord = True
+    while process_tex_coord:
+        tex_coord_id = TEXCOORD_PREFIX + str(tex_coord_index)
+
+        if source_attributes.get(tex_coord_id) is not None:
+            attributes[tex_coord_id] = []
+            tex_coord_index += 1
+        else:
+            process_tex_coord = False
+
+    tex_coord_max = tex_coord_index
+
+    #
+
+    color_index = 0
+    process_color = True
+    while process_color:
+        color_id = COLOR_PREFIX + str(color_index)
+
+        if source_attributes.get(color_id) is not None:
+            attributes[color_id] = []
+            color_index += 1
+        else:
+            process_color = False
+
+    color_max = color_index
+
+    #
+
+    bone_index = 0
+    process_bone = True
+    while process_bone:
+        joint_id = JOINTS_PREFIX + str(bone_index)
+        weight_id = WEIGHTS_PREFIX + str(bone_index)
+
+        if source_attributes.get(joint_id) is not None:
+            attributes[joint_id] = []
+            attributes[weight_id] = []
+            bone_index += 1
+        else:
+            process_bone = False
+
+    bone_max = bone_index
+
+    #
+
+    morph_index = 0
+    process_morph = True
+    while process_morph:
+        morph_position_id = MORPH_POSITION_PREFIX + str(morph_index)
+        morph_normal_id = MORPH_NORMAL_PREFIX + str(morph_index)
+        morph_tangent_id = MORPH_TANGENT_PREFIX + str(morph_index)
+
+        if source_attributes.get(morph_position_id) is not None:
+            attributes[morph_position_id] = []
+            attributes[morph_normal_id] = []
+            if use_tangents:
+                attributes[morph_tangent_id] = []
+            morph_index += 1
+        else:
+            process_morph = False
+
+    morph_max = morph_index
+
+    #
+
+    min_index = min(indices)
+    max_index = max(indices)
+
+    for old_index in indices:
+        result_primitive[INDICES_ID].append(old_index - min_index)
+
+    for old_index in range(min_index, max_index + 1):
+        for vi in range(0, 3):
+            attributes[POSITION_ATTRIBUTE].append(source_attributes[POSITION_ATTRIBUTE][old_index * 3 + vi])
+            attributes[NORMAL_ATTRIBUTE].append(source_attributes[NORMAL_ATTRIBUTE][old_index * 3 + vi])
+
+        if use_tangents:
+            for vi in range(0, 4):
+                attributes[TANGENT_ATTRIBUTE].append(source_attributes[TANGENT_ATTRIBUTE][old_index * 4 + vi])
+
+        for tex_coord_index in range(0, tex_coord_max):
+            tex_coord_id = TEXCOORD_PREFIX + str(tex_coord_index)
+            for vi in range(0, 2):
+                attributes[tex_coord_id].append(source_attributes[tex_coord_id][old_index * 2 + vi])
+
+        for color_index in range(0, color_max):
+            color_id = COLOR_PREFIX + str(color_index)
+            for vi in range(0, 4):
+                attributes[color_id].append(source_attributes[color_id][old_index * 4 + vi])
+
+        for bone_index in range(0, bone_max):
+            joint_id = JOINTS_PREFIX + str(bone_index)
+            weight_id = WEIGHTS_PREFIX + str(bone_index)
+            for vi in range(0, 4):
+                attributes[joint_id].append(source_attributes[joint_id][old_index * 4 + vi])
+                attributes[weight_id].append(source_attributes[weight_id][old_index * 4 + vi])
+
+        for morph_index in range(0, morph_max):
+            morph_position_id = MORPH_POSITION_PREFIX + str(morph_index)
+            morph_normal_id = MORPH_NORMAL_PREFIX + str(morph_index)
+            morph_tangent_id = MORPH_TANGENT_PREFIX + str(morph_index)
+            for vi in range(0, 3):
+                attributes[morph_position_id].append(source_attributes[morph_position_id][old_index * 3 + vi])
+                attributes[morph_normal_id].append(source_attributes[morph_normal_id][old_index * 3 + vi])
+            if use_tangents:
+                for vi in range(0, 4):
+                    attributes[morph_tangent_id].append(source_attributes[morph_tangent_id][old_index * 4 + vi])
+
+    return result_primitive
+
+
+def extract_primitive_pack(a, indices, use_tangents):
+    """Pack indices, that the first one starts with 0. Current indices can have gaps."""
+    attributes = {
+        POSITION_ATTRIBUTE: [],
+        NORMAL_ATTRIBUTE: []
+    }
+
+    if use_tangents:
+        attributes[TANGENT_ATTRIBUTE] = []
+
+    result_primitive = {
+        MATERIAL_ID: a[MATERIAL_ID],
+        INDICES_ID: [],
+        ATTRIBUTES_ID: attributes
+    }
+
+    source_attributes = a[ATTRIBUTES_ID]
+
+    #
+
+    tex_coord_index = 0
+    process_tex_coord = True
+    while process_tex_coord:
+        tex_coord_id = TEXCOORD_PREFIX + str(tex_coord_index)
+
+        if source_attributes.get(tex_coord_id) is not None:
+            attributes[tex_coord_id] = []
+            tex_coord_index += 1
+        else:
+            process_tex_coord = False
+
+    tex_coord_max = tex_coord_index
+
+    #
+
+    color_index = 0
+    process_color = True
+    while process_color:
+        color_id = COLOR_PREFIX + str(color_index)
+
+        if source_attributes.get(color_id) is not None:
+            attributes[color_id] = []
+            color_index += 1
+        else:
+            process_color = False
+
+    color_max = color_index
+
+    #
+
+    bone_index = 0
+    process_bone = True
+    while process_bone:
+        joint_id = JOINTS_PREFIX + str(bone_index)
+        weight_id = WEIGHTS_PREFIX + str(bone_index)
+
+        if source_attributes.get(joint_id) is not None:
+            attributes[joint_id] = []
+            attributes[weight_id] = []
+            bone_index += 1
+        else:
+            process_bone = False
+
+    bone_max = bone_index
+
+    #
+
+    morph_index = 0
+    process_morph = True
+    while process_morph:
+        morph_position_id = MORPH_POSITION_PREFIX + str(morph_index)
+        morph_normal_id = MORPH_NORMAL_PREFIX + str(morph_index)
+        morph_tangent_id = MORPH_TANGENT_PREFIX + str(morph_index)
+
+        if source_attributes.get(morph_position_id) is not None:
+            attributes[morph_position_id] = []
+            attributes[morph_normal_id] = []
+            if use_tangents:
+                attributes[morph_tangent_id] = []
+            morph_index += 1
+        else:
+            process_morph = False
+
+    morph_max = morph_index
+
+    #
+
+    old_to_new_indices = {}
+    new_to_old_indices = {}
+
+    new_index = 0
+    for old_index in indices:
+        if old_to_new_indices.get(old_index) is None:
+            old_to_new_indices[old_index] = new_index
+            new_to_old_indices[new_index] = old_index
+            new_index += 1
+
+        result_primitive[INDICES_ID].append(old_to_new_indices[old_index])
+
+    end_new_index = new_index
+
+    for new_index in range(0, end_new_index):
+        old_index = new_to_old_indices[new_index]
+
+        for vi in range(0, 3):
+            attributes[POSITION_ATTRIBUTE].append(source_attributes[POSITION_ATTRIBUTE][old_index * 3 + vi])
+            attributes[NORMAL_ATTRIBUTE].append(source_attributes[NORMAL_ATTRIBUTE][old_index * 3 + vi])
+
+        if use_tangents:
+            for vi in range(0, 4):
+                attributes[TANGENT_ATTRIBUTE].append(source_attributes[TANGENT_ATTRIBUTE][old_index * 4 + vi])
+
+        for tex_coord_index in range(0, tex_coord_max):
+            tex_coord_id = TEXCOORD_PREFIX + str(tex_coord_index)
+            for vi in range(0, 2):
+                attributes[tex_coord_id].append(source_attributes[tex_coord_id][old_index * 2 + vi])
+
+        for color_index in range(0, color_max):
+            color_id = COLOR_PREFIX + str(color_index)
+            for vi in range(0, 4):
+                attributes[color_id].append(source_attributes[color_id][old_index * 4 + vi])
+
+        for bone_index in range(0, bone_max):
+            joint_id = JOINTS_PREFIX + str(bone_index)
+            weight_id = WEIGHTS_PREFIX + str(bone_index)
+            for vi in range(0, 4):
+                attributes[joint_id].append(source_attributes[joint_id][old_index * 4 + vi])
+                attributes[weight_id].append(source_attributes[weight_id][old_index * 4 + vi])
+
+        for morph_index in range(0, morph_max):
+            morph_position_id = MORPH_POSITION_PREFIX + str(morph_index)
+            morph_normal_id = MORPH_NORMAL_PREFIX + str(morph_index)
+            morph_tangent_id = MORPH_TANGENT_PREFIX + str(morph_index)
+            for vi in range(0, 3):
+                attributes[morph_position_id].append(source_attributes[morph_position_id][old_index * 3 + vi])
+                attributes[morph_normal_id].append(source_attributes[morph_normal_id][old_index * 3 + vi])
+            if use_tangents:
+                for vi in range(0, 4):
+                    attributes[morph_tangent_id].append(source_attributes[morph_tangent_id][old_index * 4 + vi])
+
+    return result_primitive
+
+
+def extract_primitives(glTF, blender_mesh, blender_vertex_groups, modifiers, export_settings):
+    """
+    Extract primitives from a mesh. Polygons are triangulated and sorted by material.
+
+    Furthermore, primitives are split up, if the indices range is exceeded.
+    Finally, triangles are also split up/duplicated, if face normals are used instead of vertex normals.
+    """
+    print_console('INFO', 'Extracting primitive')
+
+    use_tangents = False
+    if blender_mesh.uv_layers.active and len(blender_mesh.uv_layers) > 0:
+        try:
+            blender_mesh.calc_tangents()
+            use_tangents = True
+        except Exception:
+            print_console('WARNING', 'Could not calculate tangents. Please try to triangulate the mesh first.')
+
+    #
+
+    material_map = {}
+
+    #
+    # Gathering position, normal and tex_coords.
+    #
+    no_material_attributes = {
+        POSITION_ATTRIBUTE: [],
+        NORMAL_ATTRIBUTE: []
+    }
+
+    if use_tangents:
+        no_material_attributes[TANGENT_ATTRIBUTE] = []
+
+    #
+    # Directory of materials with its primitive.
+    #
+    no_material_primitives = {
+        MATERIAL_ID: '',
+        INDICES_ID: [],
+        ATTRIBUTES_ID: no_material_attributes
+    }
+
+    material_name_to_primitives = {'': no_material_primitives}
+
+    #
+
+    vertex_index_to_new_indices = {}
+
+    material_map[''] = vertex_index_to_new_indices
+
+    #
+    # Create primitive for each material.
+    #
+    for blender_material in blender_mesh.materials:
+        if blender_material is None:
+            continue
+
+        attributes = {
+            POSITION_ATTRIBUTE: [],
+            NORMAL_ATTRIBUTE: []
+        }
+
+        if use_tangents:
+            attributes[TANGENT_ATTRIBUTE] = []
+
+        primitive = {
+            MATERIAL_ID: blender_material.name,
+            INDICES_ID: [],
+            ATTRIBUTES_ID: attributes
+        }
+
+        material_name_to_primitives[blender_material.name] = primitive
+
+        #
+
+        vertex_index_to_new_indices = {}
+
+        material_map[blender_material.name] = vertex_index_to_new_indices
+
+    tex_coord_max = 0
+    if blender_mesh.uv_layers.active:
+        tex_coord_max = len(blender_mesh.uv_layers)
+
+    #
+
+    vertex_colors = {}
+
+    color_index = 0
+    for vertex_color in blender_mesh.vertex_colors:
+        vertex_color_name = COLOR_PREFIX + str(color_index)
+        vertex_colors[vertex_color_name] = vertex_color
+
+        color_index += 1
+        if color_index >= GLTF_MAX_COLORS:
+            break
+    color_max = color_index
+
+    #
+
+    bone_max = 0
+    for blender_polygon in blender_mesh.polygons:
+        for loop_index in blender_polygon.loop_indices:
+            vertex_index = blender_mesh.loops[loop_index].vertex_index
+            bones_count = len(blender_mesh.vertices[vertex_index].groups)
+            if bones_count > 0:
+                if bones_count % 4 == 0:
+                    bones_count -= 1
+                bone_max = max(bone_max, bones_count // 4 + 1)
+
+    #
+
+    morph_max = 0
+
+    blender_shape_keys = []
+
+    if blender_mesh.shape_keys is not None:
+        morph_max = len(blender_mesh.shape_keys.key_blocks) - 1
+
+        for blender_shape_key in blender_mesh.shape_keys.key_blocks:
+            if blender_shape_key != blender_shape_key.relative_key:
+                blender_shape_keys.append(ShapeKey(
+                    blender_shape_key,
+                    blender_shape_key.normals_vertex_get(),  # calculate vertex normals for this shape key
+                    blender_shape_key.normals_polygon_get()))  # calculate polygon normals for this shape key
+
+    #
+    # Convert polygon to primitive indices and eliminate invalid ones. Assign to material.
+    #
+    for blender_polygon in blender_mesh.polygons:
+        export_color = True
+
+        #
+
+        if blender_polygon.material_index < 0 or blender_polygon.material_index >= len(blender_mesh.materials) or \
+                blender_mesh.materials[blender_polygon.material_index] is None:
+            primitive = material_name_to_primitives['']
+            vertex_index_to_new_indices = material_map['']
+        else:
+            primitive = material_name_to_primitives[blender_mesh.materials[blender_polygon.material_index].name]
+            vertex_index_to_new_indices = material_map[blender_mesh.materials[blender_polygon.material_index].name]
+        #
+
+        attributes = primitive[ATTRIBUTES_ID]
+
+        face_normal = blender_polygon.normal
+        face_tangent = Vector((0.0, 0.0, 0.0))
+        face_bitangent = Vector((0.0, 0.0, 0.0))
+        if use_tangents:
+            for loop_index in blender_polygon.loop_indices:
+                temp_vertex = blender_mesh.loops[loop_index]
+                face_tangent += temp_vertex.tangent
+                face_bitangent += temp_vertex.bitangent
+
+            face_tangent.normalize()
+            face_bitangent.normalize()
+
+        #
+
+        indices = primitive[INDICES_ID]
+
+        loop_index_list = []
+
+        if len(blender_polygon.loop_indices) == 3:
+            loop_index_list.extend(blender_polygon.loop_indices)
+        elif len(blender_polygon.loop_indices) > 3:
+            # Triangulation of polygon. Using internal function, as non-convex polygons could exist.
+            polyline = []
+
+            for loop_index in blender_polygon.loop_indices:
+                vertex_index = blender_mesh.loops[loop_index].vertex_index
+                v = blender_mesh.vertices[vertex_index].co
+                polyline.append(Vector((v[0], v[1], v[2])))
+
+            triangles = tessellate_polygon((polyline,))
+
+            for triangle in triangles:
+                loop_index_list.append(blender_polygon.loop_indices[triangle[0]])
+                loop_index_list.append(blender_polygon.loop_indices[triangle[2]])
+                loop_index_list.append(blender_polygon.loop_indices[triangle[1]])
+        else:
+            continue
+
+        for loop_index in loop_index_list:
+            vertex_index = blender_mesh.loops[loop_index].vertex_index
+
+            if vertex_index_to_new_indices.get(vertex_index) is None:
+                vertex_index_to_new_indices[vertex_index] = []
+
+            #
+
+            v = None
+            n = None
+            t = None
+            b = None
+            uvs = []
+            colors = []
+            joints = []
+            weights = []
+
+            target_positions = []
+            target_normals = []
+            target_tangents = []
+
+            vertex = blender_mesh.vertices[vertex_index]
+
+            v = convert_swizzle_location(vertex.co, export_settings)
+            if blender_polygon.use_smooth:
+                n = convert_swizzle_location(vertex.normal, export_settings)
+                if use_tangents:
+                    t = convert_swizzle_tangent(blender_mesh.loops[loop_index].tangent, export_settings)
+                    b = convert_swizzle_location(blender_mesh.loops[loop_index].bitangent, export_settings)
+            else:
+                n = convert_swizzle_location(face_normal, export_settings)
+                if use_tangents:
+                    t = convert_swizzle_tangent(face_tangent, export_settings)
+                    b = convert_swizzle_location(face_bitangent, export_settings)
+
+            if use_tangents:
+                tv = Vector((t[0], t[1], t[2]))
+                bv = Vector((b[0], b[1], b[2]))
+                nv = Vector((n[0], n[1], n[2]))
+
+                if (nv.cross(tv)).dot(bv) < 0.0:
+                    t[3] = -1.0
+
+            if blender_mesh.uv_layers.active:
+                for tex_coord_index in range(0, tex_coord_max):
+                    uv = blender_mesh.uv_layers[tex_coord_index].data[loop_index].uv
+                    uvs.append([uv.x, 1.0 - uv.y])
+
+            #
+
+            if color_max > 0 and export_color:
+                for color_index in range(0, color_max):
+                    color_name = COLOR_PREFIX + str(color_index)
+                    color = vertex_colors[color_name].data[loop_index].color
+                    colors.append([
+                        color_srgb_to_scene_linear(color[0]),
+                        color_srgb_to_scene_linear(color[1]),
+                        color_srgb_to_scene_linear(color[2]),
+                        1.0
+                    ])
+
+            #
+
+            bone_count = 0
+
+            if vertex.groups is not None and len(vertex.groups) > 0 and export_settings[gltf2_blender_export_keys.SKINS]:
+                joint = []
+                weight = []
+                for group_element in vertex.groups:
+
+                    if len(joint) == 4:
+                        bone_count += 1
+                        joints.append(joint)
+                        weights.append(weight)
+                        joint = []
+                        weight = []
+
+                    #
+
+                    vertex_group_index = group_element.group
+                    vertex_group_name = blender_vertex_groups[vertex_group_index].name
+
+                    #
+
+                    joint_index = 0
+                    modifiers_dict = {m.type: m for m in modifiers}
+                    if "ARMATURE" in modifiers_dict:
+                        armature = modifiers_dict["ARMATURE"].object
+                        skin = gltf2_blender_gather_skins.gather_skin(armature, export_settings)
+                        for index, j in enumerate(skin.joints):
+                            if j.name == vertex_group_name:
+                                joint_index = index
+
+                    joint_weight = group_element.weight
+
+                    #
+                    joint.append(joint_index)
+                    weight.append(joint_weight)
+
+                if len(joint) > 0:
+                    bone_count += 1
+
+                    for fill in range(0, 4 - len(joint)):
+                        joint.append(0)
+                        weight.append(0.0)
+
+                    joints.append(joint)
+                    weights.append(weight)
+
+            for fill in range(0, bone_max - bone_count):
+                joints.append([0, 0, 0, 0])
+                weights.append([0.0, 0.0, 0.0, 0.0])
+
+            #
+
+            if morph_max > 0 and export_settings[gltf2_blender_export_keys.MORPH]:
+                for morph_index in range(0, morph_max):
+                    blender_shape_key = blender_shape_keys[morph_index]
+
+                    v_morph = convert_swizzle_location(blender_shape_key.shape_key.data[vertex_index].co,
+                                                       export_settings)
+
+                    # Store delta.
+                    v_morph -= v
+
+                    target_positions.append(v_morph)
+
+                    #
+
+                    n_morph = None
+
+                    if blender_polygon.use_smooth:
+                        temp_normals = blender_shape_key.vertex_normals
+                        n_morph = (temp_normals[vertex_index * 3 + 0], temp_normals[vertex_index * 3 + 1],
+                                   temp_normals[vertex_index * 3 + 2])
+                    else:
+                        temp_normals = blender_shape_key.polygon_normals
+                        n_morph = (
+                            temp_normals[blender_polygon.index * 3 + 0], temp_normals[blender_polygon.index * 3 + 1],
+                            temp_normals[blender_polygon.index * 3 + 2])
+
+                    n_morph = convert_swizzle_location(n_morph, export_settings)
+
+                    # Store delta.
+                    n_morph -= n
+
+                    target_normals.append(n_morph)
+
+                    #
+
+                    if use_tangents:
+                        rotation = n_morph.rotation_difference(n)
+
+                        t_morph = Vector((t[0], t[1], t[2]))
+
+                        t_morph.rotate(rotation)
+
+                        target_tangents.append(t_morph)
+
+            #
+            #
+
+            create = True
+
+            for current_new_index in vertex_index_to_new_indices[vertex_index]:
+                found = True
+
+                for i in range(0, 3):
+                    if attributes[POSITION_ATTRIBUTE][current_new_index * 3 + i] != v[i]:
+                        found = False
+                        break
+
+                    if attributes[NORMAL_ATTRIBUTE][current_new_index * 3 + i] != n[i]:
+                        found = False
+                        break
+
+                if use_tangents:
+                    for i in range(0, 4):
+                        if attributes[TANGENT_ATTRIBUTE][current_new_index * 4 + i] != t[i]:
+                            found = False
+                            break
+
+                if not found:
+                    continue
+
+                for tex_coord_index in range(0, tex_coord_max):
+                    uv = uvs[tex_coord_index]
+
+                    tex_coord_id = TEXCOORD_PREFIX + str(tex_coord_index)
+                    for i in range(0, 2):
+                        if attributes[tex_coord_id][current_new_index * 2 + i] != uv[i]:
+                            found = False
+                            break
+
+                if export_color:
+                    for color_index in range(0, color_max):
+                        color = colors[color_index]
+
+                        color_id = COLOR_PREFIX + str(color_index)
+                        for i in range(0, 3):
+                            # Alpha is always 1.0 - see above.
+                            current_color = attributes[color_id][current_new_index * 4 + i]
+                            if color_srgb_to_scene_linear(current_color) != color[i]:
+                                found = False
+                                break
+
+                if export_settings[gltf2_blender_export_keys.SKINS]:
+                    for bone_index in range(0, bone_max):
+                        joint = joints[bone_index]
+                        weight = weights[bone_index]
+
+                        joint_id = JOINTS_PREFIX + str(bone_index)
+                        weight_id = WEIGHTS_PREFIX + str(bone_index)
+                        for i in range(0, 4):
+                            if attributes[joint_id][current_new_index * 4 + i] != joint[i]:
+                                found = False
+                                break
+                            if attributes[weight_id][current_new_index * 4 + i] != weight[i]:
+                                found = False
+                                break
+
+                if export_settings[gltf2_blender_export_keys.MORPH]:
+                    for morph_index in range(0, morph_max):
+                        target_position = target_positions[morph_index]
+                        target_normal = target_normals[morph_index]
+                        if use_tangents:
+                            target_tangent = target_tangents[morph_index]
+
+                        target_position_id = MORPH_POSITION_PREFIX + str(morph_index)
+                        target_normal_id = MORPH_NORMAL_PREFIX + str(morph_index)
+                        target_tangent_id = MORPH_TANGENT_PREFIX + str(morph_index)
+                        for i in range(0, 3):
+                            if attributes[target_position_id][current_new_index * 3 + i] != target_position[i]:
+                                found = False
+                                break
+                            if attributes[target_normal_id][current_new_index * 3 + i] != target_normal[i]:
+                                found = False
+                                break
+                            if use_tangents:
+                                if attributes[target_tangent_id][current_new_index * 3 + i] != target_tangent[i]:
+                                    found = False
+                                    break
+
+                if found:
+                    indices.append(current_new_index)
+
+                    create = False
+                    break
+
+            if not create:
+                continue
+
+            new_index = 0
+
+            if primitive.get('max_index') is not None:
+                new_index = primitive['max_index'] + 1
+
+            primitive['max_index'] = new_index
+
+            vertex_index_to_new_indices[vertex_index].append(new_index)
+
+            #
+            #
+
+            indices.append(new_index)
+
+            #
+
+            attributes[POSITION_ATTRIBUTE].extend(v)
+            attributes[NORMAL_ATTRIBUTE].extend(n)
+            if use_tangents:
+                attributes[TANGENT_ATTRIBUTE].extend(t)
+
+            if blender_mesh.uv_layers.active:
+                for tex_coord_index in range(0, tex_coord_max):
+                    tex_coord_id = TEXCOORD_PREFIX + str(tex_coord_index)
+
+                    if attributes.get(tex_coord_id) is None:
+                        attributes[tex_coord_id] = []
+
+                    attributes[tex_coord_id].extend(uvs[tex_coord_index])
+
+            if export_color:
+                for color_index in range(0, color_max):
+                    color_id = COLOR_PREFIX + str(color_index)
+
+                    if attributes.get(color_id) is None:
+                        attributes[color_id] = []
+
+                    attributes[color_id].extend(colors[color_index])
+
+            if export_settings[gltf2_blender_export_keys.SKINS]:
+                for bone_index in range(0, bone_max):
+                    joint_id = JOINTS_PREFIX + str(bone_index)
+
+                    if attributes.get(joint_id) is None:
+                        attributes[joint_id] = []
+
+                    attributes[joint_id].extend(joints[bone_index])
+
+                    weight_id = WEIGHTS_PREFIX + str(bone_index)
+
+                    if attributes.get(weight_id) is None:
+                        attributes[weight_id] = []
+
+                    attributes[weight_id].extend(weights[bone_index])
+
+            if export_settings[gltf2_blender_export_keys.MORPH]:
+                for morph_index in range(0, morph_max):
+                    target_position_id = MORPH_POSITION_PREFIX + str(morph_index)
+
+                    if attributes.get(target_position_id) is None:
+                        attributes[target_position_id] = []
+
+                    attributes[target_position_id].extend(target_positions[morph_index])
+
+                    target_normal_id = MORPH_NORMAL_PREFIX + str(morph_index)
+
+                    if attributes.get(target_normal_id) is None:
+                        attributes[target_normal_id] = []
+
+                    attributes[target_normal_id].extend(target_normals[morph_index])
+
+                    if use_tangents:
+                        target_tangent_id = MORPH_TANGENT_PREFIX + str(morph_index)
+
+                        if attributes.get(target_tangent_id) is None:
+                            attributes[target_tangent_id] = []
+
+                        attributes[target_tangent_id].extend(target_tangents[morph_index])
+
+    #
+    # Add primitive plus split them if needed.
+    #
+
+    result_primitives = []
+
+    for material_name, primitive in material_name_to_primitives.items():
+        export_color = True
+
+        #
+
+        indices = primitive[INDICES_ID]
+
+        if len(indices) == 0:
+            continue
+
+        position = primitive[ATTRIBUTES_ID][POSITION_ATTRIBUTE]
+        normal = primitive[ATTRIBUTES_ID][NORMAL_ATTRIBUTE]
+        if use_tangents:
+            tangent = primitive[ATTRIBUTES_ID][TANGENT_ATTRIBUTE]
+        tex_coords = []
+        for tex_coord_index in range(0, tex_coord_max):
+            tex_coords.append(primitive[ATTRIBUTES_ID][TEXCOORD_PREFIX + str(tex_coord_index)])
+        colors = []
+        if export_color:
+            for color_index in range(0, color_max):
+                tex_coords.append(primitive[ATTRIBUTES_ID][COLOR_PREFIX + str(color_index)])
+        joints = []
+        weights = []
+        if export_settings[gltf2_blender_export_keys.SKINS]:
+            for bone_index in range(0, bone_max):
+                joints.append(primitive[ATTRIBUTES_ID][JOINTS_PREFIX + str(bone_index)])
+                weights.append(primitive[ATTRIBUTES_ID][WEIGHTS_PREFIX + str(bone_index)])
+
+        target_positions = []
+        target_normals = []
+        target_tangents = []
+        if export_settings[gltf2_blender_export_keys.MORPH]:
+            for morph_index in range(0, morph_max):
+                target_positions.append(primitive[ATTRIBUTES_ID][MORPH_POSITION_PREFIX + str(morph_index)])
+                target_normals.append(primitive[ATTRIBUTES_ID][MORPH_NORMAL_PREFIX + str(morph_index)])
+                if use_tangents:
+                    target_tangents.append(primitive[ATTRIBUTES_ID][MORPH_TANGENT_PREFIX + str(morph_index)])
+
+        #
+
+        count = len(indices)
+
+        if count == 0:
+            continue
+
+        max_index = max(indices)
+
+        #
+
+        range_indices = 65536
+        if export_settings[gltf2_blender_export_keys.INDICES] == 'UNSIGNED_BYTE':
+            range_indices = 256
+        elif export_settings[gltf2_blender_export_keys.INDICES] == 'UNSIGNED_INT':
+            range_indices = 4294967296
+
+        #
+
+        if max_index >= range_indices:
+            #
+            # Spliting result_primitives.
+            #
+
+            # At start, all indicees are pending.
+            pending_attributes = {
+                POSITION_ATTRIBUTE: [],
+                NORMAL_ATTRIBUTE: []
+            }
+
+            if use_tangents:
+                pending_attributes[TANGENT_ATTRIBUTE] = []
+
+            pending_primitive = {
+                MATERIAL_ID: material_name,
+                INDICES_ID: [],
+                ATTRIBUTES_ID: pending_attributes
+            }
+
+            pending_primitive[INDICES_ID].extend(indices)
+
+            pending_attributes[POSITION_ATTRIBUTE].extend(position)
+            pending_attributes[NORMAL_ATTRIBUTE].extend(normal)
+            if use_tangents:
+                pending_attributes[TANGENT_ATTRIBUTE].extend(tangent)
+            tex_coord_index = 0
+            for tex_coord in tex_coords:
+                pending_attributes[TEXCOORD_PREFIX + str(tex_coord_index)] = tex_coord
+                tex_coord_index += 1
+            if export_color:
+                color_index = 0
+                for color in colors:
+                    pending_attributes[COLOR_PREFIX + str(color_index)] = color
+                    color_index += 1
+            if export_settings[gltf2_blender_export_keys.SKINS]:
+                joint_index = 0
+                for joint in joints:
+                    pending_attributes[JOINTS_PREFIX + str(joint_index)] = joint
+                    joint_index += 1
+                weight_index = 0
+                for weight in weights:
+                    pending_attributes[WEIGHTS_PREFIX + str(weight_index)] = weight
+                    weight_index += 1
+            if export_settings[gltf2_blender_export_keys.MORPH]:
+                morph_index = 0
+                for target_position in target_positions:
+                    pending_attributes[MORPH_POSITION_PREFIX + str(morph_index)] = target_position
+                    morph_index += 1
+                morph_index = 0
+                for target_normal in target_normals:
+                    pending_attributes[MORPH_NORMAL_PREFIX + str(morph_index)] = target_normal
+                    morph_index += 1
+                if use_tangents:
+                    morph_index = 0
+                    for target_tangent in target_tangents:
+                        pending_attributes[MORPH_TANGENT_PREFIX + str(morph_index)] = target_tangent
+                        morph_index += 1
+
+            pending_indices = pending_primitive[INDICES_ID]
+
+            # Continue until all are processed.
+            while len(pending_indices) > 0:
+
+                process_indices = pending_primitive[INDICES_ID]
+
+                pending_indices = []
+
+                #
+                #
+
+                all_local_indices = []
+
+                for i in range(0, (max(process_indices) // range_indices) + 1):
+                    all_local_indices.append([])
+
+                #
+                #
+
+                # For all faces ...
+                for face_index in range(0, len(process_indices), 3):
+
+                    written = False
+
+                    face_min_index = min(process_indices[face_index + 0], process_indices[face_index + 1],
+                                         process_indices[face_index + 2])
+                    face_max_index = max(process_indices[face_index + 0], process_indices[face_index + 1],
+                                         process_indices[face_index + 2])
+
+                    # ... check if it can be but in a range of maximum indices.
+                    for i in range(0, (max(process_indices) // range_indices) + 1):
+                        offset = i * range_indices
+
+                        # Yes, so store the primitive with its indices.
+                        if face_min_index >= offset and face_max_index < offset + range_indices:
+                            all_local_indices[i].extend(
+                                [process_indices[face_index + 0], process_indices[face_index + 1],
+                                 process_indices[face_index + 2]])
+
+                            written = True
+                            break
+
+                    # If not written, the triangel face has indices from different ranges.
+                    if not written:
+                        pending_indices.extend([process_indices[face_index + 0], process_indices[face_index + 1],
+                                                process_indices[face_index + 2]])
+
+                # Only add result_primitives, which do have indices in it.
+                for local_indices in all_local_indices:
+                    if len(local_indices) > 0:
+                        current_primitive = extract_primitive_floor(pending_primitive, local_indices, use_tangents)
+
+                        result_primitives.append(current_primitive)
+
+                        print_console('DEBUG', 'Adding primitive with splitting. Indices: ' + str(
+                            len(current_primitive[INDICES_ID])) + ' Vertices: ' + str(
+                            len(current_primitive[ATTRIBUTES_ID][POSITION_ATTRIBUTE]) // 3))
+
+                # Process primitive faces having indices in several ranges.
+                if len(pending_indices) > 0:
+                    pending_primitive = extract_primitive_pack(pending_primitive, pending_indices, use_tangents)
+
+                    print_console('DEBUG', 'Creating temporary primitive for splitting')
+
+        else:
+            #
+            # No splitting needed.
+            #
+            result_primitives.append(primitive)
+
+            print_console('DEBUG', 'Adding primitive without splitting. Indices: ' + str(
+                len(primitive[INDICES_ID])) + ' Vertices: ' + str(
+                len(primitive[ATTRIBUTES_ID][POSITION_ATTRIBUTE]) // 3))
+
+    print_console('INFO', 'Primitives created: ' + str(len(result_primitives)))
+
+    return result_primitives
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_filter.py b/io_scene_gltf2/blender/exp/gltf2_blender_filter.py
new file mode 100755
index 0000000000000000000000000000000000000000..ed3ee055eb24c554086ce24bf4aecab6fa68a327
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_filter.py
@@ -0,0 +1,455 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Imports
+#
+
+import bpy
+from . import gltf2_blender_export_keys
+from . import gltf2_blender_get
+from ...io.com.gltf2_io_debug import print_console
+from ..com.gltf2_blender_image import create_img_from_blender_image
+from ...io.com import gltf2_io_image
+
+#
+# Globals
+#
+
+PREVIEW = 'PREVIEW'
+GLOSSINESS = 'glTF Specular Glossiness'
+ROUGHNESS = 'glTF Metallic Roughness'
+
+
+#
+# Functions
+#
+
+def filter_merge_image(export_settings, blender_image):
+    metallic_channel = gltf2_blender_get.get_image_material_usage_to_socket(blender_image, "Metallic")
+    roughness_channel = gltf2_blender_get.get_image_material_usage_to_socket(blender_image, "Roughness")
+
+    if metallic_channel < 0 and roughness_channel < 0:
+        return False
+
+    output = export_settings[gltf2_blender_export_keys.METALLIC_ROUGHNESS_IMAGE]
+    if export_settings.get(export_keys.METALLIC_ROUGHNESS_IMAGE) is None:
+        width = blender_image.image.size[0]
+        height = blender_image.image.size[1]
+        output = gltf2_io_image.create_img(width, height, r=1.0, g=1.0, b=1.0, a=1.0)
+
+    source = create_img_from_blender_image(blender_image.image)
+
+    if metallic_channel >= 0:
+        gltf2_io_image.copy_img_channel(output, dst_channel=2, src_image=source, src_channel=metallic_channel)
+        output.name = blender_image.image.name + output.name
+    if roughness_channel >= 0:
+        gltf2_io_image.copy_img_channel(output, dst_channel=1, src_image=source, src_channel=roughness_channel)
+        if metallic_channel < 0:
+            output.name = output.name + blender_image.image.name
+    return True
+
+
+def filter_used_materials():
+    """Gather and return all unfiltered, valid Blender materials."""
+    materials = []
+
+    for blender_material in bpy.data.materials:
+        if blender_material.node_tree and blender_material.use_nodes:
+            for currentNode in blender_material.node_tree.nodes:
+                if isinstance(currentNode, bpy.types.ShaderNodeGroup):
+                    if currentNode.node_tree.name.startswith(ROUGHNESS):
+                        materials.append(blender_material)
+                    elif currentNode.node_tree.name.startswith(GLOSSINESS):
+                        materials.append(blender_material)
+                elif isinstance(currentNode, bpy.types.ShaderNodeBsdfPrincipled):
+                    materials.append(blender_material)
+        else:
+            materials.append(blender_material)
+
+    return materials
+
+
+def filter_apply(export_settings):
+    """
+    Gathers and filters the objects and assets to export.
+
+    Also filters out invalid, deleted and not exportable elements.
+    """
+    filtered_objects = []
+    implicit_filtered_objects = []
+
+    for blender_object in bpy.data.objects:
+
+        if blender_object.users == 0:
+            continue
+
+        if export_settings[gltf2_blender_export_keys.SELECTED] and not blender_object.select:
+            continue
+
+        if not export_settings[gltf2_blender_export_keys.LAYERS] and not blender_object.layers[0]:
+            continue
+
+        filtered_objects.append(blender_object)
+
+        if export_settings[gltf2_blender_export_keys.SELECTED] or not export_settings[gltf2_blender_export_keys.LAYERS]:
+            current_parent = blender_object.parent
+            while current_parent:
+                if current_parent not in implicit_filtered_objects:
+                    implicit_filtered_objects.append(current_parent)
+
+                current_parent = current_parent.parent
+
+    export_settings[gltf2_blender_export_keys.FILTERED_OBJECTS] = filtered_objects
+
+    # Meshes
+
+    filtered_meshes = {}
+    filtered_vertex_groups = {}
+    temporary_meshes = []
+
+    for blender_mesh in bpy.data.meshes:
+
+        if blender_mesh.users == 0:
+            continue
+
+        current_blender_mesh = blender_mesh
+
+        current_blender_object = None
+
+        skip = True
+
+        for blender_object in filtered_objects:
+
+            current_blender_object = blender_object
+
+            if current_blender_object.type != 'MESH':
+                continue
+
+            if current_blender_object.data == current_blender_mesh:
+
+                skip = False
+
+                use_auto_smooth = current_blender_mesh.use_auto_smooth
+
+                if use_auto_smooth:
+
+                    if current_blender_mesh.shape_keys is None:
+                        current_blender_object = current_blender_object.copy()
+                    else:
+                        use_auto_smooth = False
+
+                        print_console('WARNING',
+                                      'Auto smooth and shape keys cannot be exported in parallel. '
+                                      'Falling back to non auto smooth.')
+
+                if export_settings[gltf2_blender_export_keys.APPLY] or use_auto_smooth:
+                    # TODO: maybe add to new exporter
+                    if not export_settings[gltf2_blender_export_keys.APPLY]:
+                        current_blender_object.modifiers.clear()
+
+                    if use_auto_smooth:
+                        blender_modifier = current_blender_object.modifiers.new('Temporary_Auto_Smooth', 'EDGE_SPLIT')
+
+                        blender_modifier.split_angle = current_blender_mesh.auto_smooth_angle
+                        blender_modifier.use_edge_angle = not current_blender_mesh.has_custom_normals
+
+                    current_blender_mesh = current_blender_object.to_mesh(bpy.context.scene, True, PREVIEW)
+                    temporary_meshes.append(current_blender_mesh)
+
+                break
+
+        if skip:
+            continue
+
+        filtered_meshes[blender_mesh.name] = current_blender_mesh
+        filtered_vertex_groups[blender_mesh.name] = current_blender_object.vertex_groups
+
+    # Curves
+
+    for blender_curve in bpy.data.curves:
+
+        if blender_curve.users == 0:
+            continue
+
+        current_blender_curve = blender_curve
+
+        current_blender_mesh = None
+
+        current_blender_object = None
+
+        skip = True
+
+        for blender_object in filtered_objects:
+
+            current_blender_object = blender_object
+
+            if current_blender_object.type not in ('CURVE', 'FONT'):
+                continue
+
+            if current_blender_object.data == current_blender_curve:
+
+                skip = False
+
+                current_blender_object = current_blender_object.copy()
+
+                if not export_settings[gltf2_blender_export_keys.APPLY]:
+                    current_blender_object.modifiers.clear()
+
+                current_blender_mesh = current_blender_object.to_mesh(bpy.context.scene, True, PREVIEW)
+                temporary_meshes.append(current_blender_mesh)
+
+                break
+
+        if skip:
+            continue
+
+        filtered_meshes[blender_curve.name] = current_blender_mesh
+        filtered_vertex_groups[blender_curve.name] = current_blender_object.vertex_groups
+
+    #
+
+    export_settings[gltf2_blender_export_keys.FILTERED_MESHES] = filtered_meshes
+    export_settings[gltf2_blender_export_keys.FILTERED_VERTEX_GROUPS] = filtered_vertex_groups
+    export_settings[gltf2_blender_export_keys.TEMPORARY_MESHES] = temporary_meshes
+
+    #
+
+    filtered_materials = []
+
+    for blender_material in filter_used_materials():
+
+        if blender_material.users == 0:
+            continue
+
+        for mesh_name, blender_mesh in filtered_meshes.items():
+            for compare_blender_material in blender_mesh.materials:
+                if compare_blender_material == blender_material and blender_material not in filtered_materials:
+                    filtered_materials.append(blender_material)
+
+        #
+
+        for blender_object in filtered_objects:
+            if blender_object.material_slots:
+                for blender_material_slot in blender_object.material_slots:
+                    if blender_material_slot.link == 'DATA':
+                        continue
+
+                    if blender_material_slot.material not in filtered_materials:
+                        filtered_materials.append(blender_material_slot.material)
+
+    export_settings[gltf2_blender_export_keys.FILTERED_MATERIALS] = filtered_materials
+
+    #
+
+    filtered_textures = []
+    filtered_merged_textures = []
+
+    temp_filtered_texture_names = []
+
+    for blender_material in filtered_materials:
+        if blender_material.node_tree and blender_material.use_nodes:
+
+            per_material_textures = []
+
+            for blender_node in blender_material.node_tree.nodes:
+
+                if is_valid_node(blender_node) and blender_node not in filtered_textures:
+                    add_node = False
+                    add_merged_node = False
+                    for blender_socket in blender_node.outputs:
+                        if blender_socket.is_linked:
+                            for blender_link in blender_socket.links:
+                                if isinstance(blender_link.to_node, bpy.types.ShaderNodeGroup):
+                                    is_roughness = blender_link.to_node.node_tree.name.startswith(ROUGHNESS)
+                                    is_glossiness = blender_link.to_node.node_tree.name.startswith(GLOSSINESS)
+                                    if is_roughness or is_glossiness:
+                                        add_node = True
+                                        break
+                                elif isinstance(blender_link.to_node, bpy.types.ShaderNodeBsdfPrincipled):
+                                    add_node = True
+                                    break
+                                elif isinstance(blender_link.to_node, bpy.types.ShaderNodeNormalMap):
+                                    add_node = True
+                                    break
+                                elif isinstance(blender_link.to_node, bpy.types.ShaderNodeSeparateRGB):
+                                    add_merged_node = True
+                                    break
+
+                        if add_node or add_merged_node:
+                            break
+
+                    if add_node:
+                        filtered_textures.append(blender_node)
+                        # TODO: Add displacement texture, as not stored in node tree.
+
+                    if add_merged_node:
+                        if len(per_material_textures) == 0:
+                            filtered_merged_textures.append(per_material_textures)
+
+                        per_material_textures.append(blender_node)
+
+        else:
+
+            for blender_texture_slot in blender_material.texture_slots:
+
+                if is_valid_texture_slot(blender_texture_slot) and \
+                        blender_texture_slot not in filtered_textures and \
+                        blender_texture_slot.name not in temp_filtered_texture_names:
+                    accept = False
+
+                    if blender_texture_slot.use_map_color_diffuse:
+                        accept = True
+
+                    if blender_texture_slot.use_map_ambient:
+                        accept = True
+                    if blender_texture_slot.use_map_emit:
+                        accept = True
+                    if blender_texture_slot.use_map_normal:
+                        accept = True
+
+                    if export_settings[gltf2_blender_export_keys.DISPLACEMENT]:
+                        if blender_texture_slot.use_map_displacement:
+                            accept = True
+
+                    if accept:
+                        filtered_textures.append(blender_texture_slot)
+                        temp_filtered_texture_names.append(blender_texture_slot.name)
+
+    export_settings[gltf2_blender_export_keys.FILTERED_TEXTURES] = filtered_textures
+
+    #
+
+    filtered_images = []
+    filtered_merged_images = []
+    filtered_images_use_alpha = {}
+
+    for blender_texture in filtered_textures:
+
+        if isinstance(blender_texture, bpy.types.ShaderNodeTexImage):
+            if is_valid_image(blender_texture.image) and blender_texture.image not in filtered_images:
+                filtered_images.append(blender_texture.image)
+                alpha_socket = blender_texture.outputs.get('Alpha')
+                if alpha_socket is not None and alpha_socket.is_linked:
+                    filtered_images_use_alpha[blender_texture.image.name] = True
+
+        else:
+            if is_valid_image(blender_texture.texture.image) and blender_texture.texture.image not in filtered_images:
+                filtered_images.append(blender_texture.texture.image)
+                if blender_texture.use_map_alpha:
+                    filtered_images_use_alpha[blender_texture.texture.image.name] = True
+
+    #
+
+    for per_material_textures in filtered_merged_textures:
+
+        export_settings[gltf2_blender_export_keys.METALLIC_ROUGHNESS_IMAGE] = None
+
+        for blender_texture in per_material_textures:
+
+            if isinstance(blender_texture, bpy.types.ShaderNodeTexImage):
+                if is_valid_image(blender_texture.image) and blender_texture.image not in filtered_images:
+                    filter_merge_image(export_settings, blender_texture)
+
+        img = export_settings.get(export_keys.METALLIC_ROUGHNESS_IMAGE)
+        if img is not None:
+            filtered_merged_images.append(img)
+            export_settings[gltf2_blender_export_keys.FILTERED_TEXTURES].append(img)
+
+    export_settings[gltf2_blender_export_keys.FILTERED_MERGED_IMAGES] = filtered_merged_images
+    export_settings[gltf2_blender_export_keys.FILTERED_IMAGES] = filtered_images
+    export_settings[gltf2_blender_export_keys.FILTERED_IMAGES_USE_ALPHA] = filtered_images_use_alpha
+
+    #
+
+    filtered_cameras = []
+
+    for blender_camera in bpy.data.cameras:
+
+        if blender_camera.users == 0:
+            continue
+
+        if export_settings[gltf2_blender_export_keys.SELECTED]:
+            if blender_camera not in filtered_objects:
+                continue
+
+        filtered_cameras.append(blender_camera)
+
+    export_settings[gltf2_blender_export_keys.FILTERED_CAMERAS] = filtered_cameras
+
+    #
+    #
+
+    filtered_lights = []
+
+    for blender_light in bpy.data.lamps:
+
+        if blender_light.users == 0:
+            continue
+
+        if export_settings[gltf2_blender_export_keys.SELECTED]:
+            if blender_light not in filtered_objects:
+                continue
+
+        if blender_light.type == 'HEMI':
+            continue
+
+        filtered_lights.append(blender_light)
+
+    export_settings[gltf2_blender_export_keys.FILTERED_LIGHTS] = filtered_lights
+
+    #
+    #
+
+    for implicit_object in implicit_filtered_objects:
+        if implicit_object not in filtered_objects:
+            filtered_objects.append(implicit_object)
+
+    #
+    #
+    #
+
+    group_index = {}
+
+    if export_settings[gltf2_blender_export_keys.SKINS]:
+        for blender_object in filtered_objects:
+            if blender_object.type != 'ARMATURE' or len(blender_object.pose.bones) == 0:
+                continue
+            for blender_bone in blender_object.pose.bones:
+                group_index[blender_bone.name] = len(group_index)
+
+    export_settings[gltf2_blender_export_keys.GROUP_INDEX] = group_index
+
+
+def is_valid_node(blender_node):
+    return isinstance(blender_node, bpy.types.ShaderNodeTexImage) and is_valid_image(blender_node.image)
+
+
+def is_valid_image(image):
+    return image is not None and \
+        image.users != 0 and \
+        image.size[0] > 0 and \
+        image.size[1] > 0
+
+
+def is_valid_texture_slot(blender_texture_slot):
+    return blender_texture_slot is not None and \
+        blender_texture_slot.texture and \
+        blender_texture_slot.texture.users != 0 and \
+        blender_texture_slot.texture.type == 'IMAGE' and \
+        blender_texture_slot.texture.image is not None and \
+        blender_texture_slot.texture.image.users != 0 and \
+        blender_texture_slot.texture.image.size[0] > 0 and \
+        blender_texture_slot.texture.image.size[1] > 0
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather.py
new file mode 100755
index 0000000000000000000000000000000000000000..6f4f3b1e74aee6f2f6f9f7188663e2fadbd6cb57
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather.py
@@ -0,0 +1,63 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_nodes
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_animations
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+
+
+def gather_gltf2(export_settings):
+    """
+    Gather glTF properties from the current state of blender.
+
+    :return: list of scene graphs to be added to the glTF export
+    """
+    scenes = []
+    animations = []  # unfortunately animations in gltf2 are just as 'root' as scenes.
+    for blender_scene in bpy.data.scenes:
+        scenes.append(__gather_scene(blender_scene, export_settings))
+        animations += __gather_animations(blender_scene, export_settings)
+
+    return scenes, animations
+
+
+@cached
+def __gather_scene(blender_scene, export_settings):
+    scene = gltf2_io.Scene(
+        extensions=None,
+        extras=None,
+        name=blender_scene.name,
+        nodes=[]
+    )
+
+    for blender_object in blender_scene.objects:
+        if blender_object.parent is None:
+            node = gltf2_blender_gather_nodes.gather_node(blender_object, export_settings)
+            if node is not None:
+                scene.nodes.append(node)
+
+    # TODO: lights
+
+    return scene
+
+
+def __gather_animations(blender_scene, export_settings):
+    animations = []
+    for blender_object in blender_scene.objects:
+        animations += gltf2_blender_gather_animations.gather_animations(blender_object, export_settings)
+    return animations
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channel_target.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channel_target.py
new file mode 100755
index 0000000000000000000000000000000000000000..fbe183239fcca6ebd0e62721918c5c19a80464ed
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channel_target.py
@@ -0,0 +1,82 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import bpy
+import typing
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_nodes
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_joints
+
+
+@cached
+def gather_animation_channel_target(channels: typing.Tuple[bpy.types.FCurve],
+                                    blender_object: bpy.types.Object,
+                                    export_settings
+                                    ) -> gltf2_io.AnimationChannelTarget:
+    return gltf2_io.AnimationChannelTarget(
+        extensions=__gather_extensions(channels, blender_object, export_settings),
+        extras=__gather_extras(channels, blender_object, export_settings),
+        node=__gather_node(channels, blender_object, export_settings),
+        path=__gather_path(channels, blender_object, export_settings)
+    )
+
+
+def __gather_extensions(channels: typing.Tuple[bpy.types.FCurve],
+                        blender_object: bpy.types.Object,
+                        export_settings
+                        ) -> typing.Any:
+    return None
+
+
+def __gather_extras(channels: typing.Tuple[bpy.types.FCurve],
+                    blender_object: bpy.types.Object,
+                    export_settings
+                    ) -> typing.Any:
+    return None
+
+
+def __gather_node(channels: typing.Tuple[bpy.types.FCurve],
+                  blender_object: bpy.types.Object,
+                  export_settings
+                  ) -> gltf2_io.Node:
+    if blender_object.type == "ARMATURE":
+        # TODO: get joint from fcurve data_path and gather_joint
+        blender_bone = blender_object.path_resolve(channels[0].data_path.rsplit('.', 1)[0])
+        if isinstance(blender_bone, bpy.types.PoseBone):
+            return gltf2_blender_gather_joints.gather_joint(blender_bone, export_settings)
+
+    return gltf2_blender_gather_nodes.gather_node(blender_object, export_settings)
+
+
+def __gather_path(channels: typing.Tuple[bpy.types.FCurve],
+                  blender_object: bpy.types.Object,
+                  export_settings
+                  ) -> str:
+    target = channels[0].data_path.split('.')[-1]
+    path = {
+        "location": "translation",
+        "rotation_axis_angle": "rotation",
+        "rotation_euler": "rotation",
+        "rotation_quaternion": "rotation",
+        "scale": "scale",
+        "value": "weights"
+    }.get(target)
+
+    if target is None:
+        raise RuntimeError("Cannot export an animation with {} target".format(target))
+
+    return path
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channels.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channels.py
new file mode 100755
index 0000000000000000000000000000000000000000..808c970ddb9fe3f11e111eb383402c398b6a3632
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channels.py
@@ -0,0 +1,131 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+import typing
+
+from ..com.gltf2_blender_data_path import get_target_object_path, get_target_property_name
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.io.com import gltf2_io_debug
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_animation_samplers
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_animation_channel_target
+
+
+@cached
+def gather_animation_channels(blender_action: bpy.types.Action,
+                              blender_object: bpy.types.Object,
+                              export_settings
+                              ) -> typing.List[gltf2_io.AnimationChannel]:
+    channels = []
+
+    for channel_group in __get_channel_groups(blender_action, blender_object):
+        channel = __gather_animation_channel(channel_group, blender_object, export_settings)
+        if channel is not None:
+            channels.append(channel)
+
+    return channels
+
+
+def __gather_animation_channel(channels: typing.Tuple[bpy.types.FCurve],
+                               blender_object: bpy.types.Object,
+                               export_settings
+                               ) -> typing.Union[gltf2_io.AnimationChannel, None]:
+    if not __filter_animation_channel(channels, blender_object, export_settings):
+        return None
+
+    return gltf2_io.AnimationChannel(
+        extensions=__gather_extensions(channels, blender_object, export_settings),
+        extras=__gather_extras(channels, blender_object, export_settings),
+        sampler=__gather_sampler(channels, blender_object, export_settings),
+        target=__gather_target(channels, blender_object, export_settings)
+    )
+
+
+def __filter_animation_channel(channels: typing.Tuple[bpy.types.FCurve],
+                               blender_object: bpy.types.Object,
+                               export_settings
+                               ) -> bool:
+    return True
+
+
+def __gather_extensions(channels: typing.Tuple[bpy.types.FCurve],
+                        blender_object: bpy.types.Object,
+                        export_settings
+                        ) -> typing.Any:
+    return None
+
+
+def __gather_extras(channels: typing.Tuple[bpy.types.FCurve],
+                    blender_object: bpy.types.Object,
+                    export_settings
+                    ) -> typing.Any:
+    return None
+
+
+def __gather_sampler(channels: typing.Tuple[bpy.types.FCurve],
+                     blender_object: bpy.types.Object,
+                     export_settings
+                     ) -> gltf2_io.AnimationSampler:
+    return gltf2_blender_gather_animation_samplers.gather_animation_sampler(
+        channels,
+        blender_object,
+        export_settings
+    )
+
+
+def __gather_target(channels: typing.Tuple[bpy.types.FCurve],
+                    blender_object: bpy.types.Object,
+                    export_settings
+                    ) -> gltf2_io.AnimationChannelTarget:
+    return gltf2_blender_gather_animation_channel_target.gather_animation_channel_target(
+        channels, blender_object, export_settings)
+
+
+def __get_channel_groups(blender_action: bpy.types.Action, blender_object: bpy.types.Object):
+    targets = {}
+    for fcurve in blender_action.fcurves:
+        target_property = get_target_property_name(fcurve.data_path)
+        object_path = get_target_object_path(fcurve.data_path)
+
+        # find the object affected by this action
+        if not object_path:
+            target = blender_object
+        else:
+            try:
+                target = blender_object.path_resolve(object_path)
+            except ValueError:
+                # if the object is a mesh and the action target path can not be resolved, we know that this is a morph
+                # animation.
+                if blender_object.type == "MESH":
+                    # if you need the specific shape key for some reason, this is it:
+                    # shape_key = blender_object.data.shape_keys.path_resolve(object_path)
+                    target = blender_object.data.shape_keys
+                else:
+                    gltf2_io_debug.print_console("WARNING", "Can not export animations with target {}".format(object_path))
+                    continue
+
+        # group channels by target object and affected property of the target
+        target_properties = targets.get(target, {})
+        channels = target_properties.get(target_property, [])
+        channels.append(fcurve)
+        target_properties[target_property] = channels
+        targets[target] = target_properties
+
+    groups = []
+    for p in targets.values():
+        groups += list(p.values())
+
+    return map(tuple, groups)
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_sampler_keyframes.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_sampler_keyframes.py
new file mode 100755
index 0000000000000000000000000000000000000000..7562d8c24dfadcb334b20180998977e74eac0b5f
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_sampler_keyframes.py
@@ -0,0 +1,198 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+import mathutils
+import typing
+
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.blender.com import gltf2_blender_math
+from . import gltf2_blender_export_keys
+from io_scene_gltf2.io.com import gltf2_io_debug
+
+
+class Keyframe:
+    def __init__(self, channels: typing.Tuple[bpy.types.FCurve], time: float):
+        self.seconds = time / bpy.context.scene.render.fps
+        self.__target = channels[0].data_path.split('.')[-1]
+        self.__indices = [c.array_index for c in channels]
+
+        # Data holders for virtual properties
+        self.__value = None
+        self.__in_tangent = None
+        self.__out_tangent = None
+
+    def __get_target_len(self):
+        length = {
+            "location": 3,
+            "rotation_axis_angle": 4,
+            "rotation_euler": 3,
+            "rotation_quaternion": 4,
+            "scale": 3,
+            "value": 1
+        }.get(self.__target)
+
+        if length is None:
+            raise RuntimeError("Unknown target type {}".format(self.__target))
+
+        return length
+
+    def __set_indexed(self, value):
+        # 'value' targets don't use keyframe.array_index
+        if self.__target == "value":
+            return value
+        # Sometimes blender animations only reference a subset of components of a data target. Keyframe should always
+        # contain a complete Vector/ Quaternion --> use the array_index value of the keyframe to set components in such
+        # structures
+        result = [0.0] * self.__get_target_len()
+        for i, v in zip(self.__indices, value):
+            result[i] = v
+        result = gltf2_blender_math.list_to_mathutils(result, self.__target)
+        return result
+
+    @property
+    def value(self) -> typing.Union[mathutils.Vector, mathutils.Euler, mathutils.Quaternion, typing.List[float]]:
+        return self.__value
+
+    @value.setter
+    def value(self, value: typing.List[float]):
+        self.__value = self.__set_indexed(value)
+
+    @property
+    def in_tangent(self) -> typing.Union[mathutils.Vector, mathutils.Euler, mathutils.Quaternion, typing.List[float]]:
+        return self.__in_tangent
+
+    @in_tangent.setter
+    def in_tangent(self, value: typing.List[float]):
+        self.__in_tangent = self.__set_indexed(value)
+
+    @property
+    def out_tangent(self) -> typing.Union[mathutils.Vector, mathutils.Euler, mathutils.Quaternion, typing.List[float]]:
+        return self.__in_tangent
+
+    @out_tangent.setter
+    def out_tangent(self, value: typing.List[float]):
+        self.__out_tangent = self.__set_indexed(value)
+
+
+# cache for performance reasons
+@cached
+def gather_keyframes(channels: typing.Tuple[bpy.types.FCurve], export_settings) \
+        -> typing.List[Keyframe]:
+    """Convert the blender action groups' fcurves to keyframes for use in glTF."""
+    # Find the start and end of the whole action group
+    ranges = [channel.range() for channel in channels]
+
+    start = min([channel.range()[0] for channel in channels])
+    end = max([channel.range()[1] for channel in channels])
+
+    keyframes = []
+    if needs_baking(channels, export_settings):
+        # Bake the animation, by evaluating it at a high frequency
+        # TODO: maybe baking can also be done with FCurve.convert_to_samples
+        time = start
+        # TODO: make user controllable
+        step = 1.0 / bpy.context.scene.render.fps
+        while time <= end:
+            key = Keyframe(channels, time)
+            key.value = [c.evaluate(time) for c in channels]
+            keyframes.append(key)
+            time += step
+    else:
+        # Just use the keyframes as they are specified in blender
+        times = [keyframe.co[0] for keyframe in channels[0].keyframe_points]
+        for i, time in enumerate(times):
+            key = Keyframe(channels, time)
+            # key.value = [c.keyframe_points[i].co[0] for c in action_group.channels]
+            key.value = [c.evaluate(time) for c in channels]
+
+            # compute tangents for cubic spline interpolation
+            if channels[0].keyframe_points[0].interpolation == "BEZIER":
+                # Construct the in tangent
+                if time == times[0]:
+                    # start in-tangent has zero length
+                    key.in_tangent = [0.0 for _ in channels]
+                else:
+                    # otherwise construct an in tangent from the keyframes control points
+
+                    key.in_tangent = [
+                        3.0 * (c.keyframe_points[i].co[1] - c.keyframe_points[i].handle_left[1]
+                               ) / (time - times[i - 1])
+                        for c in channels
+                    ]
+                # Construct the out tangent
+                if time == times[-1]:
+                    # end out-tangent has zero length
+                    key.out_tangent = [0.0 for _ in channels]
+                else:
+                    # otherwise construct an out tangent from the keyframes control points
+                    key.out_tangent = [
+                        3.0 * (c.keyframe_points[i].handle_right[1] - c.keyframe_points[i].co[1]
+                               ) / (times[i + 1] - time)
+                        for c in channels
+                    ]
+            keyframes.append(key)
+
+    return keyframes
+
+
+def needs_baking(channels: typing.Tuple[bpy.types.FCurve],
+                 export_settings
+                 ) -> bool:
+    """
+    Check if baking is needed.
+
+    Some blender animations need to be baked as they can not directly be expressed in glTF.
+    """
+    def all_equal(lst):
+        return lst[1:] == lst[:-1]
+
+
+    if export_settings[gltf2_blender_export_keys.FORCE_SAMPLING]:
+        return True
+
+    interpolation = channels[0].keyframe_points[0].interpolation
+    if interpolation not in ["BEZIER", "LINEAR", "CONSTANT"]:
+        gltf2_io_debug.print_console("WARNING",
+                                     "Baking animation because of an unsupported interpolation method: {}".format(
+                                         interpolation)
+                                     )
+        return True
+
+    if any(any(k.interpolation != interpolation for k in c.keyframe_points) for c in channels):
+        # There are different interpolation methods in one action group
+        gltf2_io_debug.print_console("WARNING",
+                                     "Baking animation because there are different "
+                                     "interpolation methods in one channel"
+                                     )
+        return True
+
+    if not all_equal([len(c.keyframe_points) for c in channels]):
+        gltf2_io_debug.print_console("WARNING",
+                                     "Baking animation because the number of keyframes is not "
+                                     "equal for all channel tracks")
+        return True
+
+    if len(channels[0].keyframe_points) <= 1:
+        # we need to bake to 'STEP', as at least two keyframes are required to interpolate
+        return True
+
+    if not all(all_equal(key_times) for key_times in zip([[k.co[0] for k in c.keyframe_points] for c in channels])):
+        # The channels have differently located keyframes
+        gltf2_io_debug.print_console("WARNING",
+                                     "Baking animation because of differently located keyframes in one channel")
+        return True
+
+    return False
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_samplers.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_samplers.py
new file mode 100755
index 0000000000000000000000000000000000000000..c94f15288adaf8ceb52c0d313ed1ef41edba3143
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_samplers.py
@@ -0,0 +1,166 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import bpy
+import mathutils
+import typing
+import math
+
+from . import gltf2_blender_export_keys
+from mathutils import Matrix
+from io_scene_gltf2.blender.com.gltf2_blender_data_path import get_target_property_name, get_target_object_path
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.io.exp import gltf2_io_binary_data
+from io_scene_gltf2.io.com import gltf2_io_constants
+from io_scene_gltf2.blender.com import gltf2_blender_math
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_animation_sampler_keyframes
+
+
+@cached
+def gather_animation_sampler(channels: typing.Tuple[bpy.types.FCurve],
+                             blender_object: bpy.types.Object,
+                             export_settings
+                             ) -> gltf2_io.AnimationSampler:
+    return gltf2_io.AnimationSampler(
+        extensions=__gather_extensions(channels, blender_object, export_settings),
+        extras=__gather_extras(channels, blender_object, export_settings),
+        input=__gather_input(channels, blender_object, export_settings),
+        interpolation=__gather_interpolation(channels, blender_object, export_settings),
+        output=__gather_output(channels, blender_object, export_settings)
+    )
+
+
+def __gather_extensions(channels: typing.Tuple[bpy.types.FCurve],
+                        blender_object: bpy.types.Object,
+                        export_settings
+                        ) -> typing.Any:
+    return None
+
+
+def __gather_extras(channels: typing.Tuple[bpy.types.FCurve],
+                    blender_object: bpy.types.Object,
+                    export_settings
+                    ) -> typing.Any:
+    return None
+
+
+def __gather_input(channels: typing.Tuple[bpy.types.FCurve],
+                   blender_object: bpy.types.Object,
+                   export_settings
+                   ) -> gltf2_io.Accessor:
+    """Gather the key time codes."""
+    keyframes = gltf2_blender_gather_animation_sampler_keyframes.gather_keyframes(channels, export_settings)
+    times = [k.seconds for k in keyframes]
+
+    return gltf2_io.Accessor(
+        buffer_view=gltf2_io_binary_data.BinaryData.from_list(times, gltf2_io_constants.ComponentType.Float),
+        byte_offset=None,
+        component_type=gltf2_io_constants.ComponentType.Float,
+        count=len(times),
+        extensions=None,
+        extras=None,
+        max=[max(times)],
+        min=[min(times)],
+        name=None,
+        normalized=None,
+        sparse=None,
+        type=gltf2_io_constants.DataType.Scalar
+    )
+
+
+def __gather_interpolation(channels: typing.Tuple[bpy.types.FCurve],
+                           blender_object: bpy.types.Object,
+                           export_settings
+                           ) -> str:
+    if gltf2_blender_gather_animation_sampler_keyframes.needs_baking(channels, export_settings):
+        return 'STEP'
+
+    blender_keyframe = channels[0].keyframe_points[0]
+
+    # Select the interpolation method. Any unsupported method will fallback to STEP
+    return {
+        "BEZIER": "CUBICSPLINE",
+        "LINEAR": "LINEAR",
+        "CONSTANT": "STEP"
+    }[blender_keyframe.interpolation]
+
+
+def __gather_output(channels: typing.Tuple[bpy.types.FCurve],
+                    blender_object: bpy.types.Object,
+                    export_settings
+                    ) -> gltf2_io.Accessor:
+    """Gather the data of the keyframes."""
+    keyframes = gltf2_blender_gather_animation_sampler_keyframes.gather_keyframes(channels, export_settings)
+
+    target_datapath = channels[0].data_path
+
+    transform = Matrix.Identity(4)
+
+    if blender_object.type == "ARMATURE":
+        bone = blender_object.path_resolve(get_target_object_path(target_datapath))
+        if isinstance(bone, bpy.types.PoseBone):
+            transform = bone.bone.matrix_local
+            if bone.parent is not None:
+                parent_transform = bone.parent.bone.matrix_local
+                transform = gltf2_blender_math.multiply(parent_transform.inverted(), transform)
+                # if not export_settings[gltf2_blender_export_keys.YUP]:
+                #     transform = gltf2_blender_math.multiply(gltf2_blender_math.to_zup(), transform)
+            else:
+                # only apply the y-up conversion to root bones, as child bones already are in the y-up space
+                if export_settings[gltf2_blender_export_keys.YUP]:
+                    transform = gltf2_blender_math.multiply(gltf2_blender_math.to_yup(), transform)
+
+    values = []
+    for keyframe in keyframes:
+        # Transform the data and extract
+        value = gltf2_blender_math.transform(keyframe.value, target_datapath, transform)
+        if export_settings[gltf2_blender_export_keys.YUP] and not blender_object.type == "ARMATURE":
+            value = gltf2_blender_math.swizzle_yup(value, target_datapath)
+        keyframe_value = gltf2_blender_math.mathutils_to_gltf(value)
+        if keyframe.in_tangent is not None:
+            in_tangent = gltf2_blender_math.transform(keyframe.in_tangent, target_datapath, transform)
+            if export_settings[gltf2_blender_export_keys.YUP] and not blender_object.type == "ARMATURE":
+                in_tangent = gltf2_blender_math.swizzle_yup(in_tangent, target_datapath)
+            keyframe_value = gltf2_blender_math.mathutils_to_gltf(in_tangent) + keyframe_value
+        if keyframe.out_tangent is not None:
+            out_tangent = gltf2_blender_math.transform(keyframe.out_tangent, target_datapath, transform)
+            if export_settings[gltf2_blender_export_keys.YUP] and not blender_object.type == "ARMATURE":
+                out_tangent = gltf2_blender_math.swizzle_yup(out_tangent, target_datapath)
+            keyframe_value = keyframe_value + gltf2_blender_math.mathutils_to_gltf(out_tangent)
+        values += keyframe_value
+
+    component_type = gltf2_io_constants.ComponentType.Float
+    if get_target_property_name(target_datapath) == "value":
+        # channels with 'weight' targets must have scalar accessors
+        data_type = gltf2_io_constants.DataType.Scalar
+    else:
+        data_type = gltf2_io_constants.DataType.vec_type_from_num(len(keyframes[0].value))
+
+    return gltf2_io.Accessor(
+        buffer_view=gltf2_io_binary_data.BinaryData.from_list(values, component_type),
+        byte_offset=None,
+        component_type=component_type,
+        count=len(values) // gltf2_io_constants.DataType.num_elements(data_type),
+        extensions=None,
+        extras=None,
+        max=None,
+        min=None,
+        name=None,
+        normalized=None,
+        sparse=None,
+        type=data_type
+    )
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_animations.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animations.py
new file mode 100755
index 0000000000000000000000000000000000000000..3740fefa92985e51907f6bd8daaa23318e6c0467
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_animations.py
@@ -0,0 +1,169 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+import typing
+
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_animation_channels
+
+
+def gather_animations(blender_object: bpy.types.Object, export_settings) -> typing.List[gltf2_io.Animation]:
+    """
+    Gather all animations which contribute to the objects property.
+
+    :param blender_object: The blender object which is animated
+    :param export_settings:
+    :return: A list of glTF2 animations
+    """
+    animations = []
+
+    # Collect all 'actions' affecting this object. There is a direct mapping between blender actions and glTF animations
+    blender_actions = __get_blender_actions(blender_object)
+
+    # Export all collected actions.
+    for blender_action in blender_actions:
+        animation = __gather_animation(blender_action, blender_object, export_settings)
+        if animation is not None:
+            animations.append(animation)
+
+    return animations
+
+
+def __gather_animation(blender_action: bpy.types.Action,
+                       blender_object: bpy.types.Object,
+                       export_settings
+                       ) -> typing.Optional[gltf2_io.Animation]:
+    if not __filter_animation(blender_action, blender_object, export_settings):
+        return None
+
+    animation = gltf2_io.Animation(
+        channels=__gather_channels(blender_action, blender_object, export_settings),
+        extensions=__gather_extensions(blender_action, blender_object, export_settings),
+        extras=__gather_extras(blender_action, blender_object, export_settings),
+        name=__gather_name(blender_action, blender_object, export_settings),
+        samplers=__gather_samplers(blender_action, blender_object, export_settings)
+    )
+
+    # To allow reuse of samplers in one animation,
+    __link_samplers(animation, export_settings)
+
+    if not animation.channels:
+        return None
+
+    return animation
+
+
+def __filter_animation(blender_action: bpy.types.Action,
+                       blender_object: bpy.types.Object,
+                       export_settings
+                       ) -> bool:
+    if blender_action.users == 0:
+        return False
+
+    return True
+
+
+def __gather_channels(blender_action: bpy.types.Action,
+                      blender_object: bpy.types.Object,
+                      export_settings
+                      ) -> typing.List[gltf2_io.AnimationChannel]:
+    return gltf2_blender_gather_animation_channels.gather_animation_channels(
+        blender_action, blender_object, export_settings)
+
+
+def __gather_extensions(blender_action: bpy.types.Action,
+                        blender_object: bpy.types.Object,
+                        export_settings
+                        ) -> typing.Any:
+    return None
+
+
+def __gather_extras(blender_action: bpy.types.Action,
+                    blender_object: bpy.types.Object,
+                    export_settings
+                    ) -> typing.Any:
+    return None
+
+
+def __gather_name(blender_action: bpy.types.Action,
+                  blender_object: bpy.types.Object,
+                  export_settings
+                  ) -> typing.Optional[str]:
+    return blender_action.name
+
+
+def __gather_samplers(blender_action: bpy.types.Action,
+                      blender_object: bpy.types.Object,
+                      export_settings
+                      ) -> typing.List[gltf2_io.AnimationSampler]:
+    # We need to gather the samplers after gathering all channels --> populate this list in __link_samplers
+    return []
+
+
+def __link_samplers(animation: gltf2_io.Animation, export_settings):
+    """
+    Move animation samplers to their own list and store their indices at their previous locations.
+
+    After gathering, samplers are stored in the channels properties of the animation and need to be moved
+    to their own list while storing an index into this list at the position where they previously were.
+    This behaviour is similar to that of the glTFExporter that traverses all nodes
+    :param animation:
+    :param export_settings:
+    :return:
+    """
+    # TODO: move this to some util module and update gltf2 exporter also
+    T = typing.TypeVar('T')
+
+    def __append_unique_and_get_index(l: typing.List[T], item: T):
+        if item in l:
+            return l.index(item)
+        else:
+            index = len(l)
+            l.append(item)
+            return index
+
+    for i, channel in enumerate(animation.channels):
+        animation.channels[i].sampler = __append_unique_and_get_index(animation.samplers, channel.sampler)
+
+
+def __get_blender_actions(blender_object: bpy.types.Object
+                          ) -> typing.List[bpy.types.Action]:
+    blender_actions = []
+
+    if blender_object.animation_data is not None:
+        # Collect active action.
+        if blender_object.animation_data.action is not None:
+            blender_actions.append(blender_object.animation_data.action)
+
+        # Collect associated strips from NLA tracks.
+        for track in blender_object.animation_data.nla_tracks:
+            # Multi-strip tracks do not export correctly yet (they need to be baked),
+            # so skip them for now and only write single-strip tracks.
+            if track.strips is None or len(track.strips) != 1:
+                continue
+            for strip in track.strips:
+                blender_actions.append(strip.action)
+
+    if blender_object.type == "MESH"\
+            and blender_object.data is not None \
+            and blender_object.data.shape_keys is not None \
+            and blender_object.data.shape_keys.animation_data is not None:
+        blender_actions.append(blender_object.data.shape_keys.animation_data.action)
+
+    # Remove duplicate actions.
+    blender_actions = list(set(blender_actions))
+
+    return blender_actions
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_cache.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_cache.py
new file mode 100755
index 0000000000000000000000000000000000000000..5b00a98b205ddf31f0d46515bac7374a117dccef
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_cache.py
@@ -0,0 +1,60 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import functools
+
+
+def cached(func):
+    """
+    Decorate the cache gather functions results.
+
+    The gather function is only executed if its result isn't in the cache yet
+    :param func: the function to be decorated. It will have a static __cache member afterwards
+    :return:
+    """
+    @functools.wraps(func)
+    def wrapper_cached(*args, **kwargs):
+        assert len(args) >= 2 and 0 <= len(kwargs) <= 1, "Wrong signature for cached function"
+        cache_key_args = args
+        # make a shallow copy of the keyword arguments so that 'export_settings' can be removed
+        cache_key_kwargs = dict(kwargs)
+        if kwargs.get("export_settings"):
+            export_settings = kwargs["export_settings"]
+            # 'export_settings' should not be cached
+            del cache_key_kwargs["export_settings"]
+        else:
+            export_settings = args[-1]
+            cache_key_args = args[:-1]
+
+        # we make a tuple from the function arguments so that they can be used as a key to the cache
+        cache_key = tuple(cache_key_args + tuple(cache_key_kwargs.values()))
+
+        # invalidate cache if export settings have changed
+        if not hasattr(func, "__export_settings") or export_settings != func.__export_settings:
+            func.__cache = {}
+            func.__export_settings = export_settings
+        # use or fill cache
+        if cache_key in func.__cache:
+            return func.__cache[cache_key]
+        else:
+            result = func(*args)
+            func.__cache[cache_key] = result
+            return result
+    return wrapper_cached
+
+
+# TODO: replace "cached" with "unique" in all cases where the caching is functional and not only for performance reasons
+call_or_fetch = cached
+unique = cached
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_cameras.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_cameras.py
new file mode 100755
index 0000000000000000000000000000000000000000..b09092ca2662eb75f9b1f9739af8b505fb12b7e3
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_cameras.py
@@ -0,0 +1,124 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from . import gltf2_blender_export_keys
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.io.com import gltf2_io
+
+import bpy
+import math
+
+
+@cached
+def gather_camera(blender_object, export_settings):
+    if not __filter_camera(blender_object, export_settings):
+        return None
+
+    return gltf2_io.Camera(
+        extensions=__gather_extensions(blender_object, export_settings),
+        extras=__gather_extras(blender_object, export_settings),
+        name=__gather_name(blender_object, export_settings),
+        orthographic=__gather_orthographic(blender_object, export_settings),
+        perspective=__gather_perspective(blender_object, export_settings),
+        type=__gather_type(blender_object, export_settings)
+    )
+
+
+def __filter_camera(blender_object, export_settings):
+    if blender_object.type != 'CAMERA':
+        return False
+    if not __gather_type(blender_object, export_settings):
+        return False
+
+    return True
+
+
+def __gather_extensions(blender_object, export_settings):
+    return None
+
+
+def __gather_extras(blender_object, export_settings):
+    return None
+
+
+def __gather_name(blender_object, export_settings):
+    return blender_object.data.name
+
+
+def __gather_orthographic(blender_object, export_settings):
+    if __gather_type(blender_object, export_settings) == "orthographic":
+        orthographic = gltf2_io.CameraOrthographic(
+            extensions=None,
+            extras=None,
+            xmag=None,
+            ymag=None,
+            zfar=None,
+            znear=None
+        )
+        blender_camera = blender_object.data
+
+        orthographic.xmag = blender_camera.ortho_scale
+        orthographic.ymag = blender_camera.ortho_scale
+
+        orthographic.znear = blender_camera.clip_start
+        orthographic.zfar = blender_camera.clip_end
+
+        return orthographic
+    return None
+
+
+def __gather_perspective(blender_object, export_settings):
+    if __gather_type(blender_object, export_settings) == "perspective":
+        perspective = gltf2_io.CameraPerspective(
+            aspect_ratio=None,
+            extensions=None,
+            extras=None,
+            yfov=None,
+            zfar=None,
+            znear=None
+        )
+        blender_camera = blender_object.data
+
+        width = bpy.context.scene.render.pixel_aspect_x * bpy.context.scene.render.resolution_x
+        height = bpy.context.scene.render.pixel_aspect_y * bpy.context.scene.render.resolution_y
+        perspective.aspectRatio = width / height
+
+        if width >= height:
+            if blender_camera.sensor_fit != 'VERTICAL':
+                perspective.yfov = 2.0 * math.atan(math.tan(blender_camera.angle * 0.5) / perspective.aspectRatio)
+            else:
+                perspective.yfov = blender_camera.angle
+        else:
+            if blender_camera.sensor_fit != 'HORIZONTAL':
+                perspective.yfov = blender_camera.angle
+            else:
+                perspective.yfov = 2.0 * math.atan(math.tan(blender_camera.angle * 0.5) / perspective.aspectRatio)
+
+        perspective.znear = blender_camera.clip_start
+
+        if not export_settings[gltf2_blender_export_keys.CAMERA_INFINITE]:
+            perspective.zfar = blender_camera.clip_end
+
+        return perspective
+    return None
+
+
+def __gather_type(blender_object, export_settings):
+    camera = blender_object.data
+    if camera.type == 'PERSP':
+        return "perspective"
+    elif camera.type == 'ORTHO':
+        return "orthographic"
+    return None
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_image.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_image.py
new file mode 100755
index 0000000000000000000000000000000000000000..4941ffe2912829167beb15a18d322603044cf3c6
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_image.py
@@ -0,0 +1,166 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+import typing
+import os
+import numpy as np
+
+from . import gltf2_blender_export_keys
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp import gltf2_blender_search_node_tree
+from io_scene_gltf2.io.exp import gltf2_io_binary_data
+from io_scene_gltf2.io.exp import gltf2_io_image_data
+
+
+def gather_image(
+        blender_shader_sockets_or_texture_slots: typing.Union[typing.Tuple[bpy.types.NodeSocket],
+                                                              typing.Tuple[bpy.types.Texture]],
+        export_settings):
+    if not __filter_image(blender_shader_sockets_or_texture_slots, export_settings):
+        return None
+    image = gltf2_io.Image(
+        buffer_view=__gather_buffer_view(blender_shader_sockets_or_texture_slots, export_settings),
+        extensions=__gather_extensions(blender_shader_sockets_or_texture_slots, export_settings),
+        extras=__gather_extras(blender_shader_sockets_or_texture_slots, export_settings),
+        mime_type=__gather_mime_type(blender_shader_sockets_or_texture_slots, export_settings),
+        name=__gather_name(blender_shader_sockets_or_texture_slots, export_settings),
+        uri=__gather_uri(blender_shader_sockets_or_texture_slots, export_settings)
+    )
+    return image
+
+
+def __filter_image(sockets_or_slots, export_settings):
+    if not sockets_or_slots:
+        return False
+    return True
+
+
+def __gather_buffer_view(sockets_or_slots, export_settings):
+    if export_settings[gltf2_blender_export_keys.FORMAT] != 'ASCII':
+
+        image = __get_image_data(sockets_or_slots)
+        return gltf2_io_binary_data.BinaryData(
+            data=image.to_image_data(__gather_mime_type(sockets_or_slots, export_settings)))
+    return None
+
+
+def __gather_extensions(sockets_or_slots, export_settings):
+    return None
+
+
+def __gather_extras(sockets_or_slots, export_settings):
+    return None
+
+
+def __gather_mime_type(sockets_or_slots, export_settings):
+    return 'image/png'
+    # return 'image/jpeg'
+
+
+def __gather_name(sockets_or_slots, export_settings):
+    if __is_socket(sockets_or_slots):
+        node = __get_tex_from_socket(sockets_or_slots[0])
+        if node is not None:
+            return node.shader_node.image.name
+    elif isinstance(sockets_or_slots[0], bpy.types.MaterialTextureSlot):
+        return sockets_or_slots[0].name
+    return None
+
+
+def __gather_uri(sockets_or_slots, export_settings):
+    if export_settings[gltf2_blender_export_keys.FORMAT] == 'ASCII':
+        # as usual we just store the data in place instead of already resolving the references
+        return __get_image_data(sockets_or_slots)
+    return None
+
+
+def __is_socket(sockets_or_slots):
+    return isinstance(sockets_or_slots[0], bpy.types.NodeSocket)
+
+
+def __is_slot(sockets_or_slots):
+    return isinstance(sockets_or_slots[0], bpy.types.MaterialTextureSlot)
+
+
+def __get_image_data(sockets_or_slots):
+    # For shared ressources, such as images, we just store the portion of data that is needed in the glTF property
+    # in a helper class. During generation of the glTF in the exporter these will then be combined to actual binary
+    # ressources.
+    def split_pixels_by_channels(image: bpy.types.Image) -> typing.Iterable[typing.Iterable[float]]:
+        pixels = np.array(image.pixels)
+        pixels = pixels.reshape((pixels.shape[0] // image.channels, image.channels))
+        channels = np.split(pixels, pixels.shape[1], axis=1)
+        return channels
+
+    if __is_socket(sockets_or_slots):
+        results = [__get_tex_from_socket(socket) for socket in sockets_or_slots]
+        image = None
+        for result, socket in zip(results, sockets_or_slots):
+            # rudimentarily try follow the node tree to find the correct image data.
+            channel = None
+            for elem in result.path:
+                if isinstance(elem.from_node, bpy.types.ShaderNodeSeparateRGB):
+                    channel = {
+                        'R': 0,
+                        'G': 1,
+                        'B': 2
+                    }[elem.from_socket.name]
+
+            if channel is not None:
+                pixels = [split_pixels_by_channels(result.shader_node.image)[channel]]
+            else:
+                pixels = split_pixels_by_channels(result.shader_node.image)
+
+            file_name = os.path.splitext(result.shader_node.image.name)[0]
+
+            image_data = gltf2_io_image_data.ImageData(
+                file_name,
+                result.shader_node.image.size[0],
+                result.shader_node.image.size[1],
+                pixels)
+
+            if image is None:
+                image = image_data
+            else:
+                image.add_to_image(image_data)
+
+        return image
+    elif __is_slot(sockets_or_slots):
+        texture = __get_tex_from_slot(sockets_or_slots[0])
+        pixels = texture.image.pixels
+
+        image_data = gltf2_io_image_data.ImageData(
+            texture.name,
+            texture.image.size[0],
+            texture.image.size[1],
+            pixels)
+        return image_data
+    else:
+        # Texture slots
+        raise NotImplementedError()
+
+
+def __get_tex_from_socket(blender_shader_socket: bpy.types.NodeSocket):
+    result = gltf2_blender_search_node_tree.from_socket(
+        blender_shader_socket,
+        gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeTexImage))
+    if not result:
+        return None
+    return result[0]
+
+
+def __get_tex_from_slot(blender_texture_slot):
+    return blender_texture_slot.texture
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_joints.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_joints.py
new file mode 100755
index 0000000000000000000000000000000000000000..38e470317f6981ae2cdcbc0cd8478e75e36ce455
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_joints.py
@@ -0,0 +1,80 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import mathutils
+
+from . import gltf2_blender_export_keys
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.io.com import gltf2_io_debug
+from io_scene_gltf2.blender.exp import gltf2_blender_extract
+from io_scene_gltf2.blender.com import gltf2_blender_math
+
+
+@cached
+def gather_joint(blender_bone, export_settings):
+    """
+    Generate a glTF2 node from a blender bone, as joints in glTF2 are simply nodes.
+
+    :param blender_bone: a blender PoseBone
+    :param export_settings: the settings for this export
+    :return: a glTF2 node (acting as a joint)
+    """
+    axis_basis_change = mathutils.Matrix.Identity(4)
+    if export_settings[gltf2_blender_export_keys.YUP]:
+        axis_basis_change = mathutils.Matrix(
+            ((1.0, 0.0, 0.0, 0.0), (0.0, 0.0, 1.0, 0.0), (0.0, -1.0, 0.0, 0.0), (0.0, 0.0, 0.0, 1.0)))
+
+    # extract bone transform
+    if blender_bone.parent is None:
+        correction_matrix_local = gltf2_blender_math.multiply(axis_basis_change, blender_bone.bone.matrix_local)
+    else:
+        correction_matrix_local = gltf2_blender_math.multiply(
+            blender_bone.parent.bone.matrix_local.inverted(), blender_bone.bone.matrix_local)
+    matrix_basis = blender_bone.matrix_basis
+    if export_settings[gltf2_blender_export_keys.BAKE_SKINS]:
+        gltf2_io_debug.print_console("WARNING", "glTF bake skins not supported")
+        # matrix_basis = blender_object.convert_space(blender_bone, blender_bone.matrix, from_space='POSE',
+        #                                             to_space='LOCAL')
+    trans, rot, sca = gltf2_blender_extract.decompose_transition(
+        gltf2_blender_math.multiply(correction_matrix_local, matrix_basis), 'JOINT', export_settings)
+    translation, rotation, scale = (None, None, None)
+    if trans[0] != 0.0 or trans[1] != 0.0 or trans[2] != 0.0:
+        translation = [trans[0], trans[1], trans[2]]
+    if rot[0] != 0.0 or rot[1] != 0.0 or rot[2] != 0.0 or rot[3] != 1.0:
+        rotation = [rot[0], rot[1], rot[2], rot[3]]
+    if sca[0] != 1.0 or sca[1] != 1.0 or sca[2] != 1.0:
+        scale = [sca[0], sca[1], sca[2]]
+
+    # traverse into children
+    children = []
+    for bone in blender_bone.children:
+        children.append(gather_joint(bone, export_settings))
+
+    # finally add to the joints array containing all the joints in the hierarchy
+    return gltf2_io.Node(
+        camera=None,
+        children=children,
+        extensions=None,
+        extras=None,
+        matrix=None,
+        mesh=None,
+        name=blender_bone.name,
+        rotation=rotation,
+        scale=scale,
+        skin=None,
+        translation=translation,
+        weights=None
+    )
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_material_normal_texture_info_class.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_material_normal_texture_info_class.py
new file mode 100755
index 0000000000000000000000000000000000000000..0d314681135e7a8cb5602fbed92a05d1de91333d
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_material_normal_texture_info_class.py
@@ -0,0 +1,113 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+import typing
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture
+from io_scene_gltf2.blender.exp import gltf2_blender_search_node_tree
+
+
+@cached
+def gather_material_normal_texture_info_class(blender_shader_sockets_or_texture_slots: typing.Union[
+    typing.Tuple[bpy.types.NodeSocket], typing.Tuple[bpy.types.Texture]],
+        export_settings):
+    if not __filter_texture_info(blender_shader_sockets_or_texture_slots, export_settings):
+        return None
+
+    texture_info = gltf2_io.MaterialNormalTextureInfoClass(
+        extensions=__gather_extensions(blender_shader_sockets_or_texture_slots, export_settings),
+        extras=__gather_extras(blender_shader_sockets_or_texture_slots, export_settings),
+        scale=__gather_scale(blender_shader_sockets_or_texture_slots, export_settings),
+        index=__gather_index(blender_shader_sockets_or_texture_slots, export_settings),
+        tex_coord=__gather_tex_coord(blender_shader_sockets_or_texture_slots, export_settings)
+    )
+
+    return texture_info
+
+
+def __filter_texture_info(blender_shader_sockets_or_texture_slots, export_settings):
+    if not blender_shader_sockets_or_texture_slots:
+        return False
+    if not all([elem is not None for elem in blender_shader_sockets_or_texture_slots]):
+        return False
+    if isinstance(blender_shader_sockets_or_texture_slots[0], bpy.types.NodeSocket):
+        if any([__get_tex_from_socket(socket) is None for socket in blender_shader_sockets_or_texture_slots]):
+            # sockets do not lead to a texture --> discard
+            return False
+    return True
+
+
+def __gather_extensions(blender_shader_sockets_or_texture_slots, export_settings):
+    return None
+
+
+def __gather_extras(blender_shader_sockets_or_texture_slots, export_settings):
+    return None
+
+
+def __gather_scale(blender_shader_sockets_or_texture_slots, export_settings):
+    return None
+
+
+def __gather_index(blender_shader_sockets_or_texture_slots, export_settings):
+    # We just put the actual shader into the 'index' member
+    return gltf2_blender_gather_texture.gather_texture(blender_shader_sockets_or_texture_slots, export_settings)
+
+
+def __gather_tex_coord(blender_shader_sockets_or_texture_slots, export_settings):
+    if __is_socket(blender_shader_sockets_or_texture_slots):
+        blender_shader_node = __get_tex_from_socket(blender_shader_sockets_or_texture_slots[0]).shader_node
+        if len(blender_shader_node.inputs['Vector'].links) == 0:
+            return 0
+
+        input_node = blender_shader_node.inputs['Vector'].links[0].from_node
+
+        if isinstance(input_node, bpy.types.ShaderNodeMapping):
+
+            if len(input_node.inputs['Vector'].links) == 0:
+                return 0
+
+            input_node = input_node.inputs['Vector'].links[0].from_node
+
+        if not isinstance(input_node, bpy.types.ShaderNodeUVMap):
+            return 0
+
+        if input_node.uv_map == '':
+            return 0
+
+        # Try to gather map index.
+        for blender_mesh in bpy.data.meshes:
+            texCoordIndex = blender_mesh.uv_textures.find(input_node.uv_map)
+            if texCoordIndex >= 0:
+                return texCoordIndex
+
+        return 0
+    else:
+        raise NotImplementedError()
+
+
+def __is_socket(sockets_or_slots):
+    return isinstance(sockets_or_slots[0], bpy.types.NodeSocket)
+
+
+def __get_tex_from_socket(socket):
+    result = gltf2_blender_search_node_tree.from_socket(
+        socket,
+        gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeTexImage))
+    if not result:
+        return None
+    return result[0]
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_material_occlusion_texture_info_class.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_material_occlusion_texture_info_class.py
new file mode 100755
index 0000000000000000000000000000000000000000..af21931875d7de602bbaa8d22a8a37bd7eff094d
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_material_occlusion_texture_info_class.py
@@ -0,0 +1,113 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+import typing
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture
+from io_scene_gltf2.blender.exp import gltf2_blender_search_node_tree
+
+
+@cached
+def gather_material_occlusion_texture_info_class(blender_shader_sockets_or_texture_slots: typing.Union[
+    typing.Tuple[bpy.types.NodeSocket], typing.Tuple[bpy.types.Texture]],
+        export_settings):
+    if not __filter_texture_info(blender_shader_sockets_or_texture_slots, export_settings):
+        return None
+
+    texture_info = gltf2_io.MaterialOcclusionTextureInfoClass(
+        extensions=__gather_extensions(blender_shader_sockets_or_texture_slots, export_settings),
+        extras=__gather_extras(blender_shader_sockets_or_texture_slots, export_settings),
+        strength=__gather_scale(blender_shader_sockets_or_texture_slots, export_settings),
+        index=__gather_index(blender_shader_sockets_or_texture_slots, export_settings),
+        tex_coord=__gather_tex_coord(blender_shader_sockets_or_texture_slots, export_settings)
+    )
+
+    return texture_info
+
+
+def __filter_texture_info(blender_shader_sockets_or_texture_slots, export_settings):
+    if not blender_shader_sockets_or_texture_slots:
+        return False
+    if not all([elem is not None for elem in blender_shader_sockets_or_texture_slots]):
+        return False
+    if isinstance(blender_shader_sockets_or_texture_slots[0], bpy.types.NodeSocket):
+        if any([__get_tex_from_socket(socket) is None for socket in blender_shader_sockets_or_texture_slots]):
+            # sockets do not lead to a texture --> discard
+            return False
+    return True
+
+
+def __gather_extensions(blender_shader_sockets_or_texture_slots, export_settings):
+    return None
+
+
+def __gather_extras(blender_shader_sockets_or_texture_slots, export_settings):
+    return None
+
+
+def __gather_scale(blender_shader_sockets_or_texture_slots, export_settings):
+    return None
+
+
+def __gather_index(blender_shader_sockets_or_texture_slots, export_settings):
+    # We just put the actual shader into the 'index' member
+    return gltf2_blender_gather_texture.gather_texture(blender_shader_sockets_or_texture_slots, export_settings)
+
+
+def __gather_tex_coord(blender_shader_sockets_or_texture_slots, export_settings):
+    if __is_socket(blender_shader_sockets_or_texture_slots):
+        blender_shader_node = __get_tex_from_socket(blender_shader_sockets_or_texture_slots[0]).shader_node
+        if len(blender_shader_node.inputs['Vector'].links) == 0:
+            return 0
+
+        input_node = blender_shader_node.inputs['Vector'].links[0].from_node
+
+        if isinstance(input_node, bpy.types.ShaderNodeMapping):
+
+            if len(input_node.inputs['Vector'].links) == 0:
+                return 0
+
+            input_node = input_node.inputs['Vector'].links[0].from_node
+
+        if not isinstance(input_node, bpy.types.ShaderNodeUVMap):
+            return 0
+
+        if input_node.uv_map == '':
+            return 0
+
+        # Try to gather map index.
+        for blender_mesh in bpy.data.meshes:
+            texCoordIndex = blender_mesh.uv_textures.find(input_node.uv_map)
+            if texCoordIndex >= 0:
+                return texCoordIndex
+
+        return 0
+    else:
+        raise NotImplementedError()
+
+
+def __is_socket(sockets_or_slots):
+    return isinstance(sockets_or_slots[0], bpy.types.NodeSocket)
+
+
+def __get_tex_from_socket(socket):
+    result = gltf2_blender_search_node_tree.from_socket(
+        socket,
+        gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeTexImage))
+    if not result:
+        return None
+    return result[0]
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials.py
new file mode 100755
index 0000000000000000000000000000000000000000..d801eca7e5503887d22a4f29a92f5da51f46dcbf
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials.py
@@ -0,0 +1,138 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture_info
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_material_normal_texture_info_class
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_material_occlusion_texture_info_class
+
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_materials_pbr_metallic_roughness
+from io_scene_gltf2.blender.exp import gltf2_blender_get
+
+
+@cached
+def gather_material(blender_material, export_settings):
+    """
+    Gather the material used by the blender primitive.
+
+    :param blender_material: the blender material used in the glTF primitive
+    :param export_settings:
+    :return: a glTF material
+    """
+    if not __filter_material(blender_material, export_settings):
+        return None
+
+    material = gltf2_io.Material(
+        alpha_cutoff=__gather_alpha_cutoff(blender_material, export_settings),
+        alpha_mode=__gather_alpha_mode(blender_material, export_settings),
+        double_sided=__gather_double_sided(blender_material, export_settings),
+        emissive_factor=__gather_emmissive_factor(blender_material, export_settings),
+        emissive_texture=__gather_emissive_texture(blender_material, export_settings),
+        extensions=__gather_extensions(blender_material, export_settings),
+        extras=__gather_extras(blender_material, export_settings),
+        name=__gather_name(blender_material, export_settings),
+        normal_texture=__gather_normal_texture(blender_material, export_settings),
+        occlusion_texture=__gather_occlusion_texture(blender_material, export_settings),
+        pbr_metallic_roughness=__gather_pbr_metallic_roughness(blender_material, export_settings)
+    )
+
+    return material
+    # material = blender_primitive['material']
+    #
+    #     if get_material_requires_texcoords(glTF, material) and not export_settings['gltf_texcoords']:
+    #         material = -1
+    #
+    #     if get_material_requires_normals(glTF, material) and not export_settings['gltf_normals']:
+    #         material = -1
+    #
+    #     # Meshes/primitives without material are allowed.
+    #     if material >= 0:
+    #         primitive.material = material
+    #     else:
+    #         print_console('WARNING', 'Material ' + internal_primitive[
+    #             'material'] + ' not found. Please assign glTF 2.0 material or enable Blinn-Phong material in export.')
+
+
+def __filter_material(blender_material, export_settings):
+    # if not blender_material.use_nodes:
+    #     return False
+    # if not blender_material.node_tree:
+    #     return False
+    return True
+
+
+def __gather_alpha_cutoff(blender_material, export_settings):
+    return None
+
+
+def __gather_alpha_mode(blender_material, export_settings):
+    return None
+
+
+def __gather_double_sided(blender_material, export_settings):
+    return None
+
+
+def __gather_emmissive_factor(blender_material, export_settings):
+    emissive = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Emissive")
+    if isinstance(emissive, bpy.types.NodeSocket):
+        return emissive.default_value
+    return None
+
+
+def __gather_emissive_texture(blender_material, export_settings):
+    emissive = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Emissive")
+    return gltf2_blender_gather_texture_info.gather_texture_info((emissive,), export_settings)
+
+
+def __gather_extensions(blender_material, export_settings):
+    extensions = {}
+
+
+    # TODO specular glossiness extension
+
+    return extensions if extensions else None
+
+
+def __gather_extras(blender_material, export_setttings):
+    return None
+
+
+def __gather_name(blender_material, export_settings):
+
+    return None
+
+
+def __gather_normal_texture(blender_material, export_settings):
+    normal = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Normal")
+    return gltf2_blender_gather_material_normal_texture_info_class.gather_material_normal_texture_info_class(
+        (normal,),
+        export_settings)
+
+
+def __gather_occlusion_texture(blender_material, export_settings):
+    emissive = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Occlusion")
+    return gltf2_blender_gather_material_occlusion_texture_info_class.gather_material_occlusion_texture_info_class(
+        (emissive,),
+        export_settings)
+
+
+def __gather_pbr_metallic_roughness(blender_material, export_settings):
+    return gltf2_blender_gather_materials_pbr_metallic_roughness.gather_material_pbr_metallic_roughness(
+        blender_material,
+        export_settings)
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_pbr_metallic_roughness.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_pbr_metallic_roughness.py
new file mode 100755
index 0000000000000000000000000000000000000000..7a567bc3bf820e9a0b103596ece706a03843cade
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_materials_pbr_metallic_roughness.py
@@ -0,0 +1,93 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture_info
+from io_scene_gltf2.blender.exp import gltf2_blender_get
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+
+
+@cached
+def gather_material_pbr_metallic_roughness(blender_material, export_settings):
+    if not __filter_pbr_material(blender_material, export_settings):
+        return None
+
+    material = gltf2_io.MaterialPBRMetallicRoughness(
+        base_color_factor=__gather_base_color_factor(blender_material, export_settings),
+        base_color_texture=__gather_base_color_texture(blender_material, export_settings),
+        extensions=__gather_extensions(blender_material, export_settings),
+        extras=__gather_extras(blender_material, export_settings),
+        metallic_factor=__gather_metallic_factor(blender_material, export_settings),
+        metallic_roughness_texture=__gather_metallic_roughness_texture(blender_material, export_settings),
+        roughness_factor=__gather_roughness_factor(blender_material, export_settings)
+    )
+
+    return material
+
+
+def __filter_pbr_material(blender_material, export_settings):
+    return True
+
+
+def __gather_base_color_factor(blender_material, export_settings):
+    base_color_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Base Color")
+    if base_color_socket is None:
+        base_color_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "BaseColor")
+    if isinstance(base_color_socket, bpy.types.NodeSocket):
+        return list(base_color_socket.default_value)
+    return None
+
+def __gather_base_color_texture(blender_material, export_settings):
+    base_color_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Base Color")
+    if base_color_socket is None:
+        base_color_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "BaseColor")
+    return gltf2_blender_gather_texture_info.gather_texture_info((base_color_socket,), export_settings)
+
+
+def __gather_extensions(blender_material, export_settings):
+    return None
+
+
+def __gather_extras(blender_material, export_settings):
+    return None
+
+
+def __gather_metallic_factor(blender_material, export_settings):
+    metallic_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Metallic")
+    if isinstance(metallic_socket, bpy.types.NodeSocket):
+        return metallic_socket.default_value
+    return None
+
+
+def __gather_metallic_roughness_texture(blender_material, export_settings):
+    metallic_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Metallic")
+    roughness_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Roughness")
+
+    if metallic_socket is None and roughness_socket is None:
+        metallic_roughness = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "MetallicRoughness")
+        texture_input = (metallic_roughness,)
+    else:
+        texture_input = (metallic_socket, roughness_socket)
+
+    return gltf2_blender_gather_texture_info.gather_texture_info(texture_input, export_settings)
+
+
+def __gather_roughness_factor(blender_material, export_settings):
+    roughness_socket = gltf2_blender_get.get_socket_or_texture_slot(blender_material, "Roughness")
+    if isinstance(roughness_socket, bpy.types.NodeSocket):
+        return roughness_socket.default_value
+    return None
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_mesh.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_mesh.py
new file mode 100755
index 0000000000000000000000000000000000000000..57903287f6f6cc5bf4ff2edb749b3a7ebcca35d6
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_mesh.py
@@ -0,0 +1,90 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+from typing import Optional, Dict, List, Any
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_primitives
+
+
+@cached
+def gather_mesh(blender_mesh: bpy.types.Mesh,
+                vertex_groups: Optional[bpy.types.VertexGroups],
+                modifiers: Optional[bpy.types.ObjectModifiers],
+                export_settings
+                ) -> Optional[gltf2_io.Mesh]:
+    if not __filter_mesh(blender_mesh, vertex_groups, modifiers, export_settings):
+        return None
+
+    mesh = gltf2_io.Mesh(
+        extensions=__gather_extensions(blender_mesh, vertex_groups, modifiers, export_settings),
+        extras=__gather_extras(blender_mesh, vertex_groups, modifiers, export_settings),
+        name=__gather_name(blender_mesh, vertex_groups, modifiers, export_settings),
+        primitives=__gather_primitives(blender_mesh, vertex_groups, modifiers, export_settings),
+        weights=__gather_weights(blender_mesh, vertex_groups, modifiers, export_settings)
+    )
+
+    return mesh
+
+
+def __filter_mesh(blender_mesh: bpy.types.Mesh,
+                  vertex_groups: Optional[bpy.types.VertexGroups],
+                  modifiers: Optional[bpy.types.ObjectModifiers],
+                  export_settings
+                  ) -> bool:
+    if blender_mesh.users == 0:
+        return False
+    return True
+
+
+def __gather_extensions(blender_mesh: bpy.types.Mesh,
+                        vertex_groups: Optional[bpy.types.VertexGroups],
+                        modifiers: Optional[bpy.types.ObjectModifiers],
+                        export_settings
+                        ) -> Any:
+    return None
+
+
+def __gather_extras(blender_mesh: bpy.types.Mesh,
+                    vertex_groups: Optional[bpy.types.VertexGroups],
+                    modifiers: Optional[bpy.types.ObjectModifiers],
+                    export_settings
+                    ) -> Optional[Dict[Any, Any]]:
+    return None
+
+
+def __gather_name(blender_mesh: bpy.types.Mesh,
+                  vertex_groups: Optional[bpy.types.VertexGroups],
+                  modifiers: Optional[bpy.types.ObjectModifiers],
+                  export_settings
+                  ) -> str:
+    return blender_mesh.name
+
+
+def __gather_primitives(blender_mesh: bpy.types.Mesh,
+                        vertex_groups: Optional[bpy.types.VertexGroups],
+                        modifiers: Optional[bpy.types.ObjectModifiers],
+                        export_settings
+                        ) -> List[gltf2_io.MeshPrimitive]:
+    return gltf2_blender_gather_primitives.gather_primitives(blender_mesh, vertex_groups, modifiers, export_settings)
+
+
+def __gather_weights(blender_mesh: bpy.types.Mesh,
+                     vertex_groups: Optional[bpy.types.VertexGroups],
+                     modifiers: Optional[bpy.types.ObjectModifiers],
+                     export_settings
+                     ) -> Optional[List[float]]:
+    return None
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py
new file mode 100755
index 0000000000000000000000000000000000000000..dc7192d8a48621de392c96258c6b32d58843e1cc
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py
@@ -0,0 +1,148 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from . import gltf2_blender_export_keys
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_skins
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_cameras
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_mesh
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_joints
+from io_scene_gltf2.blender.exp import gltf2_blender_extract
+from io_scene_gltf2.io.com import gltf2_io
+
+
+@cached
+def gather_node(blender_object, export_settings):
+    if not __filter_node(blender_object, export_settings):
+        return None
+
+    node = gltf2_io.Node(
+        camera=__gather_camera(blender_object, export_settings),
+        children=__gather_children(blender_object, export_settings),
+        extensions=__gather_extensions(blender_object, export_settings),
+        extras=__gather_extras(blender_object, export_settings),
+        matrix=__gather_matrix(blender_object, export_settings),
+        mesh=__gather_mesh(blender_object, export_settings),
+        name=__gather_name(blender_object, export_settings),
+        rotation=None,
+        scale=None,
+        skin=__gather_skin(blender_object, export_settings),
+        translation=None,
+        weights=__gather_weights(blender_object, export_settings)
+    )
+    node.translation, node.rotation, node.scale = __gather_trans_rot_scale(blender_object, export_settings)
+
+    return node
+
+
+def __filter_node(blender_object, export_settings):
+    if blender_object.users == 0:
+        return False
+    if export_settings[gltf2_blender_export_keys.SELECTED] and not blender_object.select:
+        return False
+    if not export_settings[gltf2_blender_export_keys.LAYERS] and not blender_object.layers[0]:
+        return False
+    if blender_object.dupli_group is not None and not blender_object.dupli_group.layers[0]:
+        return False
+
+    return True
+
+
+def __gather_camera(blender_object, export_settings):
+    return gltf2_blender_gather_cameras.gather_camera(blender_object, export_settings)
+
+
+def __gather_children(blender_object, export_settings):
+    children = []
+    # standard children
+    for child_object in blender_object.children:
+        node = gather_node(child_object, export_settings)
+        if node is not None:
+            children.append(node)
+    # blender dupli objects
+    if blender_object.dupli_type == 'GROUP' and blender_object.dupli_group:
+        for dupli_object in blender_object.dupli_group.objects:
+            node = gather_node(dupli_object, export_settings)
+            if node is not None:
+                children.append(node)
+
+    # blender bones
+    if blender_object.type == "ARMATURE":
+        for blender_bone in blender_object.pose.bones:
+            if not blender_bone.parent:
+                children.append(gltf2_blender_gather_joints.gather_joint(blender_bone, export_settings))
+
+    return children
+
+
+def __gather_extensions(blender_object, export_settings):
+    return None
+
+
+def __gather_extras(blender_object, export_settings):
+    return None
+
+
+def __gather_matrix(blender_object, export_settings):
+    # return blender_object.matrix_local
+    return []
+
+
+def __gather_mesh(blender_object, export_settings):
+    if blender_object.type == "MESH":
+        # If not using vertex group, they are irrelevant for caching --> ensure that they do not trigger a cache miss
+        vertex_groups = blender_object.vertex_groups
+        modifiers = blender_object.modifiers
+        if len(vertex_groups) == 0:
+            vertex_groups = None
+        if len(modifiers) == 0:
+            modifiers = None
+
+        return gltf2_blender_gather_mesh.gather_mesh(blender_object.data, vertex_groups, modifiers, export_settings)
+    else:
+        return None
+
+
+def __gather_name(blender_object, export_settings):
+    if blender_object.dupli_type == 'GROUP' and blender_object.dupli_group:
+        return "Duplication_Offset_" + blender_object.name
+    return blender_object.name
+
+
+def __gather_trans_rot_scale(blender_object, export_settings):
+    trans, rot, sca = gltf2_blender_extract.decompose_transition(blender_object.matrix_local, 'NODE', export_settings)
+    if blender_object.dupli_type == 'GROUP' and blender_object.dupli_group:
+        trans = -gltf2_blender_extract.convert_swizzle_location(
+            blender_object.dupli_group.dupli_offset, export_settings)
+    translation, rotation, scale = (None, None, None)
+    if trans[0] != 0.0 or trans[1] != 0.0 or trans[2] != 0.0:
+        translation = [trans[0], trans[1], trans[2]]
+    if rot[0] != 0.0 or rot[1] != 0.0 or rot[2] != 0.0 or rot[3] != 1.0:
+        rotation = [rot[0], rot[1], rot[2], rot[3]]
+    if sca[0] != 1.0 or sca[1] != 1.0 or sca[2] != 1.0:
+        scale = [sca[0], sca[1], sca[2]]
+    return translation, rotation, scale
+
+
+def __gather_skin(blender_object, export_settings):
+    modifiers = {m.type: m for m in blender_object.modifiers}
+
+    if "ARMATURE" in modifiers:
+        # Skins and meshes must be in the same glTF node, which is different from how blender handles armatures
+        return gltf2_blender_gather_skins.gather_skin(modifiers["ARMATURE"].object, export_settings)
+
+
+def __gather_weights(blender_object, export_settings):
+    return None
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py
new file mode 100755
index 0000000000000000000000000000000000000000..bd1294cd7407356db5a334f186f82b11bf8cb685
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py
@@ -0,0 +1,217 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from . import gltf2_blender_export_keys
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.io.com import gltf2_io_constants
+from io_scene_gltf2.io.com import gltf2_io_debug
+from io_scene_gltf2.io.exp import gltf2_io_binary_data
+from io_scene_gltf2.blender.exp import gltf2_blender_utils
+
+
+def gather_primitive_attributes(blender_primitive, export_settings):
+    """
+    Gathers the attributes, such as POSITION, NORMAL, TANGENT from a blender primitive.
+
+    :return: a dictionary of attributes
+    """
+    attributes = {}
+    attributes.update(__gather_position(blender_primitive, export_settings))
+    attributes.update(__gather_normal(blender_primitive, export_settings))
+    attributes.update(__gather_tangent(blender_primitive, export_settings))
+    attributes.update(__gather_texcoord(blender_primitive, export_settings))
+    attributes.update(__gather_colors(blender_primitive, export_settings))
+    attributes.update(__gather_skins(blender_primitive, export_settings))
+    return attributes
+
+
+def __gather_position(blender_primitive, export_settings):
+    position = blender_primitive["attributes"]["POSITION"]
+    componentType = gltf2_io_constants.ComponentType.Float
+    return {
+        "POSITION": gltf2_io.Accessor(
+            buffer_view=gltf2_io_binary_data.BinaryData.from_list(position, componentType),
+            byte_offset=None,
+            component_type=componentType,
+            count=len(position) // gltf2_io_constants.DataType.num_elements(gltf2_io_constants.DataType.Vec3),
+            extensions=None,
+            extras=None,
+            max=gltf2_blender_utils.max_components(position, gltf2_io_constants.DataType.Vec3),
+            min=gltf2_blender_utils.min_components(position, gltf2_io_constants.DataType.Vec3),
+            name=None,
+            normalized=None,
+            sparse=None,
+            type=gltf2_io_constants.DataType.Vec3
+        )
+    }
+
+
+def __gather_normal(blender_primitive, export_settings):
+    if export_settings[gltf2_blender_export_keys.NORMALS]:
+        normal = blender_primitive["attributes"]['NORMAL']
+        return {
+            "NORMAL": gltf2_io.Accessor(
+                buffer_view=gltf2_io_binary_data.BinaryData.from_list(normal, gltf2_io_constants.ComponentType.Float),
+                byte_offset=None,
+                component_type=gltf2_io_constants.ComponentType.Float,
+                count=len(normal) // gltf2_io_constants.DataType.num_elements(gltf2_io_constants.DataType.Vec3),
+                extensions=None,
+                extras=None,
+                max=None,
+                min=None,
+                name=None,
+                normalized=None,
+                sparse=None,
+                type=gltf2_io_constants.DataType.Vec3
+            )
+        }
+    return {}
+
+
+def __gather_tangent(blender_primitive, export_settings):
+    if export_settings[gltf2_blender_export_keys.TANGENTS]:
+        if blender_primitive["attributes"].get('TANGENT') is not None:
+            tangent = blender_primitive["attributes"]['TANGENT']
+            return {
+                "TANGENT": gltf2_io.Accessor(
+                    buffer_view=gltf2_io_binary_data.BinaryData.from_list(
+                        tangent, gltf2_io_constants.ComponentType.Float),
+                    byte_offset=None,
+                    component_type=gltf2_io_constants.ComponentType.Float,
+                    count=len(tangent) // gltf2_io_constants.DataType.num_elements(gltf2_io_constants.DataType.Vec4),
+                    extensions=None,
+                    extras=None,
+                    max=None,
+                    min=None,
+                    name=None,
+                    normalized=None,
+                    sparse=None,
+                    type=gltf2_io_constants.DataType.Vec4
+                )
+            }
+
+    return {}
+
+
+def __gather_texcoord(blender_primitive, export_settings):
+    attributes = {}
+    if export_settings[gltf2_blender_export_keys.TEX_COORDS]:
+        tex_coord_index = 0
+        tex_coord_id = 'TEXCOORD_' + str(tex_coord_index)
+        while blender_primitive["attributes"].get(tex_coord_id) is not None:
+            tex_coord = blender_primitive["attributes"][tex_coord_id]
+            attributes[tex_coord_id] = gltf2_io.Accessor(
+                buffer_view=gltf2_io_binary_data.BinaryData.from_list(
+                    tex_coord, gltf2_io_constants.ComponentType.Float),
+                byte_offset=None,
+                component_type=gltf2_io_constants.ComponentType.Float,
+                count=len(tex_coord) // gltf2_io_constants.DataType.num_elements(gltf2_io_constants.DataType.Vec2),
+                extensions=None,
+                extras=None,
+                max=None,
+                min=None,
+                name=None,
+                normalized=None,
+                sparse=None,
+                type=gltf2_io_constants.DataType.Vec2
+            )
+            tex_coord_index += 1
+            tex_coord_id = 'TEXCOORD_' + str(tex_coord_index)
+    return attributes
+
+
+def __gather_colors(blender_primitive, export_settings):
+    attributes = {}
+    if export_settings[gltf2_blender_export_keys.COLORS]:
+        color_index = 0
+        color_id = 'COLOR_' + str(color_index)
+        while blender_primitive["attributes"].get(color_id) is not None:
+            internal_color = blender_primitive["attributes"][color_id]
+            attributes[color_id] = gltf2_io.Accessor(
+                buffer_view=gltf2_io_binary_data.BinaryData.from_list(
+                    internal_color, gltf2_io_constants.ComponentType.Float),
+                byte_offset=None,
+                component_type=gltf2_io_constants.ComponentType.Float,
+                count=len(internal_color) // gltf2_io_constants.DataType.num_elements(gltf2_io_constants.DataType.Vec4),
+                extensions=None,
+                extras=None,
+                max=None,
+                min=None,
+                name=None,
+                normalized=None,
+                sparse=None,
+                type=gltf2_io_constants.DataType.Vec4
+            )
+            color_index += 1
+            color_id = 'COLOR_' + str(color_index)
+    return attributes
+
+
+def __gather_skins(blender_primitive, export_settings):
+    attributes = {}
+    if export_settings[gltf2_blender_export_keys.SKINS]:
+        bone_index = 0
+        joint_id = 'JOINTS_' + str(bone_index)
+        weight_id = 'WEIGHTS_' + str(bone_index)
+        while blender_primitive["attributes"].get(joint_id) and blender_primitive["attributes"].get(weight_id):
+            if bone_index >= 4:
+                gltf2_io_debug.print_console("WARNING", "There are more than 4 joint vertex influences."
+                                                        "Consider to apply blenders Limit Total function.")
+                # TODO: add option to stop after 4
+                # break
+
+            # joints
+            internal_joint = blender_primitive["attributes"][joint_id]
+            joint = gltf2_io.Accessor(
+                buffer_view=gltf2_io_binary_data.BinaryData.from_list(
+                    internal_joint, gltf2_io_constants.ComponentType.UnsignedShort),
+                byte_offset=None,
+                component_type=gltf2_io_constants.ComponentType.UnsignedShort,
+                count=len(internal_joint) // gltf2_io_constants.DataType.num_elements(gltf2_io_constants.DataType.Vec4),
+                extensions=None,
+                extras=None,
+                max=None,
+                min=None,
+                name=None,
+                normalized=None,
+                sparse=None,
+                type=gltf2_io_constants.DataType.Vec4
+            )
+            attributes[joint_id] = joint
+
+            # weights
+            internal_weight = blender_primitive["attributes"][weight_id]
+            weight = gltf2_io.Accessor(
+                buffer_view=gltf2_io_binary_data.BinaryData.from_list(
+                    internal_weight, gltf2_io_constants.ComponentType.Float),
+                byte_offset=None,
+                component_type=gltf2_io_constants.ComponentType.Float,
+                count=len(internal_weight) // gltf2_io_constants.DataType.num_elements(
+                    gltf2_io_constants.DataType.Vec4),
+                extensions=None,
+                extras=None,
+                max=None,
+                min=None,
+                name=None,
+                normalized=None,
+                sparse=None,
+                type=gltf2_io_constants.DataType.Vec4
+            )
+            attributes[weight_id] = weight
+
+            bone_index += 1
+            joint_id = 'JOINTS_' + str(bone_index)
+            weight_id = 'WEIGHTS_' + str(bone_index)
+    return attributes
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py
new file mode 100755
index 0000000000000000000000000000000000000000..5b3e607bb9c4111f57c93d0606460c5bd442cc7b
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py
@@ -0,0 +1,200 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+from typing import List, Optional
+
+from .gltf2_blender_export_keys import INDICES, FORCE_INDICES, NORMALS, MORPH_NORMAL, TANGENTS, MORPH_TANGENT, MORPH
+
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.blender.exp import gltf2_blender_extract
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_primitive_attributes
+from io_scene_gltf2.blender.exp import gltf2_blender_utils
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_materials
+
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.io.exp import gltf2_io_binary_data
+from io_scene_gltf2.io.com import gltf2_io_constants
+from io_scene_gltf2.io.com.gltf2_io_debug import print_console
+
+
+@cached
+def gather_primitives(
+        blender_mesh: bpy.types.Mesh,
+        vertex_groups: Optional[bpy.types.VertexGroups],
+        modifiers: Optional[bpy.types.ObjectModifiers],
+        export_settings
+        ) -> List[gltf2_io.MeshPrimitive]:
+    """
+    Extract the mesh primitives from a blender object
+
+    :return: a list of glTF2 primitives
+    """
+    primitives = []
+    blender_primitives = gltf2_blender_extract.extract_primitives(
+        None, blender_mesh, vertex_groups, modifiers, export_settings)
+
+    for internal_primitive in blender_primitives:
+
+        primitive = gltf2_io.MeshPrimitive(
+            attributes=__gather_attributes(internal_primitive, blender_mesh, modifiers, export_settings),
+            extensions=None,
+            extras=None,
+            indices=__gather_indices(internal_primitive, blender_mesh, modifiers, export_settings),
+            material=__gather_materials(internal_primitive, blender_mesh, modifiers, export_settings),
+            mode=None,
+            targets=__gather_targets(internal_primitive, blender_mesh, modifiers, export_settings)
+        )
+        primitives.append(primitive)
+
+    return primitives
+
+
+def __gather_materials(blender_primitive, blender_mesh, modifiers, export_settings):
+    if not blender_primitive['material']:
+        # TODO: fix 'extract_promitives' so that the value of 'material' is None and not empty string
+        return None
+    material = bpy.data.materials[blender_primitive['material']]
+    return gltf2_blender_gather_materials.gather_material(material, export_settings)
+
+
+def __gather_indices(blender_primitive, blender_mesh, modifiers, export_settings):
+    indices = blender_primitive['indices']
+
+    max_index = max(indices)
+    if max_index < (1 << 8):
+        component_type = gltf2_io_constants.ComponentType.UnsignedByte
+    elif max_index < (1 << 16):
+        component_type = gltf2_io_constants.ComponentType.UnsignedShort
+    elif max_index < (1 << 32):
+        component_type = gltf2_io_constants.ComponentType.UnsignedInt
+    else:
+        print_console('ERROR', 'Invalid max_index: ' + str(max_index))
+        return None
+
+    if export_settings[FORCE_INDICES]:
+        component_type = gltf2_io_constants.ComponentType.from_legacy_define(export_settings[INDICES])
+
+    element_type = gltf2_io_constants.DataType.Scalar
+    binary_data = gltf2_io_binary_data.BinaryData.from_list(indices, component_type)
+    return gltf2_io.Accessor(
+        buffer_view=binary_data,
+        byte_offset=None,
+        component_type=component_type,
+        count=len(indices) // gltf2_io_constants.DataType.num_elements(element_type),
+        extensions=None,
+        extras=None,
+        max=None,
+        min=None,
+        name=None,
+        normalized=None,
+        sparse=None,
+        type=element_type
+    )
+
+
+def __gather_attributes(blender_primitive, blender_mesh, modifiers, export_settings):
+    return gltf2_blender_gather_primitive_attributes.gather_primitive_attributes(blender_primitive, export_settings)
+
+
+def __gather_targets(blender_primitive, blender_mesh, modifiers, export_settings):
+    if export_settings[MORPH]:
+        targets = []
+        if blender_mesh.shape_keys is not None:
+            morph_index = 0
+            for blender_shape_key in blender_mesh.shape_keys.key_blocks:
+                if blender_shape_key != blender_shape_key.relative_key:
+
+                    target_position_id = 'MORPH_POSITION_' + str(morph_index)
+                    target_normal_id = 'MORPH_NORMAL_' + str(morph_index)
+                    target_tangent_id = 'MORPH_TANGENT_' + str(morph_index)
+
+                    if blender_primitive["attributes"].get(target_position_id):
+                        target = {}
+                        internal_target_position = blender_primitive["attributes"][target_position_id]
+                        binary_data = gltf2_io_binary_data.BinaryData.from_list(
+                            internal_target_position,
+                            gltf2_io_constants.ComponentType.Float
+                        )
+                        target["POSITION"] = gltf2_io.Accessor(
+                            buffer_view=binary_data,
+                            byte_offset=None,
+                            component_type=gltf2_io_constants.ComponentType.Float,
+                            count=len(internal_target_position) // gltf2_io_constants.DataType.num_elements(
+                                gltf2_io_constants.DataType.Vec3),
+                            extensions=None,
+                            extras=None,
+                            max=gltf2_blender_utils.max_components(
+                                internal_target_position, gltf2_io_constants.DataType.Vec3),
+                            min=gltf2_blender_utils.min_components(
+                                internal_target_position, gltf2_io_constants.DataType.Vec3),
+                            name=None,
+                            normalized=None,
+                            sparse=None,
+                            type=gltf2_io_constants.DataType.Vec3
+                        )
+
+                        if export_settings[NORMALS] \
+                                and export_settings[MORPH_NORMAL] \
+                                and blender_primitive["attributes"].get(target_normal_id):
+
+                            internal_target_normal = blender_primitive["attributes"][target_normal_id]
+                            binary_data = gltf2_io_binary_data.BinaryData.from_list(
+                                internal_target_normal,
+                                gltf2_io_constants.ComponentType.Float,
+                            )
+                            target['NORMAL'] = gltf2_io.Accessor(
+                                buffer_view=binary_data,
+                                byte_offset=None,
+                                component_type=gltf2_io_constants.ComponentType.Float,
+                                count=len(internal_target_normal) // gltf2_io_constants.DataType.num_elements(
+                                    gltf2_io_constants.DataType.Vec3),
+                                extensions=None,
+                                extras=None,
+                                max=None,
+                                min=None,
+                                name=None,
+                                normalized=None,
+                                sparse=None,
+                                type=gltf2_io_constants.DataType.Vec3
+                            )
+
+                        if export_settings[TANGENTS] \
+                                and export_settings[MORPH_TANGENT] \
+                                and blender_primitive["attributes"].get(target_tangent_id):
+                            internal_target_tangent = blender_primitive["attributes"][target_tangent_id]
+                            binary_data = gltf2_io_binary_data.BinaryData.from_list(
+                                internal_target_tangent,
+                                gltf2_io_constants.ComponentType.Float,
+                            )
+                            target['TANGENT'] = gltf2_io.Accessor(
+                                buffer_view=binary_data,
+                                byte_offset=None,
+                                component_type=gltf2_io_constants.ComponentType.Float,
+                                count=len(internal_target_tangent) // gltf2_io_constants.DataType.num_elements(
+                                    gltf2_io_constants.DataType.Vec3),
+                                extensions=None,
+                                extras=None,
+                                max=None,
+                                min=None,
+                                name=None,
+                                normalized=None,
+                                sparse=None,
+                                type=gltf2_io_constants.DataType.Vec3
+                            )
+                        targets.append(target)
+                        morph_index += 1
+        return targets
+    return None
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_sampler.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_sampler.py
new file mode 100755
index 0000000000000000000000000000000000000000..840c98f4ad28e8eaf19e0918e4e56fa2e489cc12
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_sampler.py
@@ -0,0 +1,98 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+
+
+@cached
+def gather_sampler(blender_shader_node: bpy.types.Node, export_settings):
+    if not __filter_sampler(blender_shader_node, export_settings):
+        return None
+
+    return gltf2_io.Sampler(
+        extensions=__gather_extensions(blender_shader_node, export_settings),
+        extras=__gather_extras(blender_shader_node, export_settings),
+        mag_filter=__gather_mag_filter(blender_shader_node, export_settings),
+        min_filter=__gather_min_filter(blender_shader_node, export_settings),
+        name=__gather_name(blender_shader_node, export_settings),
+        wrap_s=__gather_wrap_s(blender_shader_node, export_settings),
+        wrap_t=__gather_wrap_t(blender_shader_node, export_settings)
+    )
+
+
+def __filter_sampler(blender_shader_node, export_settings):
+    if not blender_shader_node.interpolation == 'Closest' and not blender_shader_node.extension == 'CLIP':
+        return False
+    return True
+
+
+def __gather_extensions(blender_shader_node, export_settings):
+    return None
+
+
+def __gather_extras(blender_shader_node, export_settings):
+    return None
+
+
+def __gather_mag_filter(blender_shader_node, export_settings):
+    if blender_shader_node.interpolation == 'Closest':
+        return 9728  # NEAREST
+    return 9729  # LINEAR
+
+
+def __gather_min_filter(blender_shader_node, export_settings):
+    if blender_shader_node.interpolation == 'Closest':
+        return 9984  # NEAREST_MIPMAP_NEAREST
+    return 9986  # NEAREST_MIPMAP_LINEAR
+
+
+def __gather_name(blender_shader_node, export_settings):
+    return None
+
+
+def __gather_wrap_s(blender_shader_node, export_settings):
+    if blender_shader_node.extension == 'CLIP':
+        return 33071
+    return None
+
+
+def __gather_wrap_t(blender_shader_node, export_settings):
+    if blender_shader_node.extension == 'CLIP':
+        return 33071
+    return None
+
+
+@cached
+def gather_sampler_from_texture_slot(blender_texture: bpy.types.TextureSlot, export_settings):
+    magFilter = 9729
+    wrap = 10497
+    if blender_texture.texture.extension == 'CLIP':
+        wrap = 33071
+
+    minFilter = 9986
+    if magFilter == 9728:
+        minFilter = 9984
+
+    return gltf2_io.Sampler(
+        extensions=None,
+        extras=None,
+        mag_filter=magFilter,
+        min_filter=minFilter,
+        name=None,
+        wrap_s=wrap,
+        wrap_t=wrap
+    )
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_skins.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_skins.py
new file mode 100755
index 0000000000000000000000000000000000000000..84703414838ef50f9e2929d603d0d2c362d40fb3
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_skins.py
@@ -0,0 +1,150 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import mathutils
+from . import gltf2_blender_export_keys
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.io.exp import gltf2_io_binary_data
+from io_scene_gltf2.io.com import gltf2_io_constants
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_joints
+from io_scene_gltf2.blender.com import gltf2_blender_math
+
+
+@cached
+def gather_skin(blender_object, export_settings):
+    """
+    Gather armatures, bones etc into a glTF2 skin object.
+
+    :param blender_object: the object which may contain a skin
+    :param export_settings:
+    :return: a glTF2 skin object
+    """
+    if not __filter_skin(blender_object, export_settings):
+        return None
+
+    return gltf2_io.Skin(
+        extensions=__gather_extensions(blender_object, export_settings),
+        extras=__gather_extras(blender_object, export_settings),
+        inverse_bind_matrices=__gather_inverse_bind_matrices(blender_object, export_settings),
+        joints=__gather_joints(blender_object, export_settings),
+        name=__gather_name(blender_object, export_settings),
+        skeleton=__gather_skeleton(blender_object, export_settings)
+    )
+
+
+def __filter_skin(blender_object, export_settings):
+    if not export_settings[gltf2_blender_export_keys.SKINS]:
+        return False
+    if blender_object.type != 'ARMATURE' or len(blender_object.pose.bones) == 0:
+        return False
+
+    return True
+
+
+def __gather_extensions(blender_object, export_settings):
+    return None
+
+
+def __gather_extras(blender_object, export_settings):
+    return None
+
+
+def __gather_inverse_bind_matrices(blender_object, export_settings):
+    inverse_matrices = []
+
+    axis_basis_change = mathutils.Matrix.Identity(4)
+    if export_settings[gltf2_blender_export_keys.YUP]:
+        axis_basis_change = mathutils.Matrix(
+            ((1.0, 0.0, 0.0, 0.0), (0.0, 0.0, 1.0, 0.0), (0.0, -1.0, 0.0, 0.0), (0.0, 0.0, 0.0, 1.0)))
+
+    # # artificial torso, as needed by glTF
+    # inverse_bind_matrix = blender_object.matrix_world.inverted() * axis_basis_change.inverted()
+    # for column in range(0, 4):
+    #     for row in range(0, 4):
+    #         inverse_matrices.append(inverse_bind_matrix[row][column])
+
+    #
+    for blender_bone in blender_object.pose.bones:
+        inverse_bind_matrix = gltf2_blender_math.multiply(axis_basis_change, blender_bone.bone.matrix_local)
+        bind_shape_matrix = gltf2_blender_math.multiply(gltf2_blender_math.multiply(
+            axis_basis_change, blender_object.matrix_world.inverted()), axis_basis_change.inverted())
+
+        inverse_bind_matrix = gltf2_blender_math.multiply(inverse_bind_matrix.inverted(), bind_shape_matrix)
+        for column in range(0, 4):
+            for row in range(0, 4):
+                inverse_matrices.append(inverse_bind_matrix[row][column])
+
+    binary_data = gltf2_io_binary_data.BinaryData.from_list(inverse_matrices, gltf2_io_constants.ComponentType.Float)
+    return gltf2_io.Accessor(
+        buffer_view=binary_data,
+        byte_offset=None,
+        component_type=gltf2_io_constants.ComponentType.Float,
+        count=len(inverse_matrices) // gltf2_io_constants.DataType.num_elements(gltf2_io_constants.DataType.Mat4),
+        extensions=None,
+        extras=None,
+        max=None,
+        min=None,
+        name=None,
+        normalized=None,
+        sparse=None,
+        type=gltf2_io_constants.DataType.Mat4
+    )
+
+
+def __gather_joints(blender_object, export_settings):
+    # # the skeletal hierarchy groups below a 'root' joint
+    # # TODO: add transform?
+    # torso = gltf2_io.Node(
+    #     camera=None,
+    #     children=[],
+    #     extensions={},
+    #     extras=None,
+    #     matrix=[],
+    #     mesh=None,
+    #     name="Skeleton_" + blender_object.name,
+    #     rotation=None,
+    #     scale=None,
+    #     skin=None,
+    #     translation=None,
+    #     weights=None
+    # )
+
+    root_joints = []
+    # build the hierarchy of nodes out of the bones
+    for blender_bone in blender_object.pose.bones:
+        if not blender_bone.parent:
+            root_joints.append(gltf2_blender_gather_joints.gather_joint(blender_bone, export_settings))
+
+    # joints is a flat list containing all nodes belonging to the skin
+    joints = []
+
+    def __collect_joints(node):
+        joints.append(node)
+        for child in node.children:
+            __collect_joints(child)
+    for joint in root_joints:
+        __collect_joints(joint)
+
+    return joints
+
+
+def __gather_name(blender_object, export_settings):
+    return blender_object.name
+
+
+def __gather_skeleton(blender_object, export_settings):
+    # In the future support the result of https://github.com/KhronosGroup/glTF/pull/1195
+    return None  # gltf2_blender_gather_nodes.gather_node(blender_object, export_settings)
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture.py
new file mode 100755
index 0000000000000000000000000000000000000000..93db33f9e740f3f027db0cac3cf3531a353027f4
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture.py
@@ -0,0 +1,100 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import typing
+import bpy
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_sampler
+from io_scene_gltf2.blender.exp import gltf2_blender_search_node_tree
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_image
+from io_scene_gltf2.io.com import gltf2_io_debug
+
+
+@cached
+def gather_texture(
+        blender_shader_sockets_or_texture_slots: typing.Union[
+            typing.Tuple[bpy.types.NodeSocket], typing.Tuple[typing.Any]],
+        export_settings):
+    """
+    Gather texture sampling information and image channels from a blender shader textu  re attached to a shader socket.
+
+    :param blender_shader_sockets: The sockets of the material which should contribute to the texture
+    :param export_settings: configuration of the export
+    :return: a glTF 2.0 texture with sampler and source embedded (will be converted to references by the exporter)
+    """
+    # TODO: extend to texture slots
+    if not __filter_texture(blender_shader_sockets_or_texture_slots, export_settings):
+        return None
+
+    return gltf2_io.Texture(
+        extensions=__gather_extensions(blender_shader_sockets_or_texture_slots, export_settings),
+        extras=__gather_extras(blender_shader_sockets_or_texture_slots, export_settings),
+        name=__gather_name(blender_shader_sockets_or_texture_slots, export_settings),
+        sampler=__gather_sampler(blender_shader_sockets_or_texture_slots, export_settings),
+        source=__gather_source(blender_shader_sockets_or_texture_slots, export_settings)
+    )
+
+
+def __filter_texture(blender_shader_sockets_or_texture_slots, export_settings):
+    return True
+
+
+def __gather_extensions(blender_shader_sockets, export_settings):
+    return None
+
+
+def __gather_extras(blender_shader_sockets, export_settings):
+    return None
+
+
+def __gather_name(blender_shader_sockets, export_settings):
+    return None
+
+
+def __gather_sampler(blender_shader_sockets_or_texture_slots, export_settings):
+    if isinstance(blender_shader_sockets_or_texture_slots[0], bpy.types.NodeSocket):
+        shader_nodes = [__get_tex_from_socket(socket).shader_node for socket in blender_shader_sockets_or_texture_slots]
+        if len(shader_nodes) > 1:
+            gltf2_io_debug.print_console("WARNING",
+                                         "More than one shader node tex image used for a texture. "
+                                         "The resulting glTF sampler will behave like the first shader node tex image.")
+        return gltf2_blender_gather_sampler.gather_sampler(
+            shader_nodes[0],
+            export_settings)
+    elif isinstance(blender_shader_sockets_or_texture_slots[0], bpy.types.MaterialTextureSlot):
+        return gltf2_blender_gather_sampler.gather_sampler_from_texture_slot(
+            blender_shader_sockets_or_texture_slots[0],
+            export_settings
+        )
+    else:
+        # TODO: implement texture slot sampler
+        raise NotImplementedError()
+
+
+def __gather_source(blender_shader_sockets_or_texture_slots, export_settings):
+    return gltf2_blender_gather_image.gather_image(blender_shader_sockets_or_texture_slots, export_settings)
+
+# Helpers
+
+
+def __get_tex_from_socket(socket):
+    result = gltf2_blender_search_node_tree.from_socket(
+        socket,
+        gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeTexImage))
+    if not result:
+        return None
+    return result[0]
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture_info.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture_info.py
new file mode 100755
index 0000000000000000000000000000000000000000..149a2a84e70f53e27ccd1f9e98c87834c8bdec1a
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_texture_info.py
@@ -0,0 +1,107 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+import typing
+from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_texture
+from io_scene_gltf2.blender.exp import gltf2_blender_search_node_tree
+
+
+@cached
+def gather_texture_info(blender_shader_sockets_or_texture_slots: typing.Union[
+    typing.Tuple[bpy.types.NodeSocket], typing.Tuple[bpy.types.Texture]],
+        export_settings):
+    if not __filter_texture_info(blender_shader_sockets_or_texture_slots, export_settings):
+        return None
+
+    texture_info = gltf2_io.TextureInfo(
+        extensions=__gather_extensions(blender_shader_sockets_or_texture_slots, export_settings),
+        extras=__gather_extras(blender_shader_sockets_or_texture_slots, export_settings),
+        index=__gather_index(blender_shader_sockets_or_texture_slots, export_settings),
+        tex_coord=__gather_tex_coord(blender_shader_sockets_or_texture_slots, export_settings)
+    )
+
+    return texture_info
+
+
+def __filter_texture_info(blender_shader_sockets_or_texture_slots, export_settings):
+    if not blender_shader_sockets_or_texture_slots:
+        return False
+    if not all([elem is not None for elem in blender_shader_sockets_or_texture_slots]):
+        return False
+    if isinstance(blender_shader_sockets_or_texture_slots[0], bpy.types.NodeSocket):
+        if any([__get_tex_from_socket(socket) is None for socket in blender_shader_sockets_or_texture_slots]):
+            # sockets do not lead to a texture --> discard
+            return False
+    return True
+
+
+def __gather_extensions(blender_shader_sockets_or_texture_slots, export_settings):
+    return None
+
+
+def __gather_extras(blender_shader_sockets_or_texture_slots, export_settings):
+    return None
+
+
+def __gather_index(blender_shader_sockets_or_texture_slots, export_settings):
+    # We just put the actual shader into the 'index' member
+    return gltf2_blender_gather_texture.gather_texture(blender_shader_sockets_or_texture_slots, export_settings)
+
+
+def __gather_tex_coord(blender_shader_sockets_or_texture_slots, export_settings):
+    if isinstance(blender_shader_sockets_or_texture_slots[0], bpy.types.NodeSocket):
+        blender_shader_node = __get_tex_from_socket(blender_shader_sockets_or_texture_slots[0]).shader_node
+        if len(blender_shader_node.inputs['Vector'].links) == 0:
+            return 0
+
+        input_node = blender_shader_node.inputs['Vector'].links[0].from_node
+
+        if isinstance(input_node, bpy.types.ShaderNodeMapping):
+
+            if len(input_node.inputs['Vector'].links) == 0:
+                return 0
+
+            input_node = input_node.inputs['Vector'].links[0].from_node
+
+        if not isinstance(input_node, bpy.types.ShaderNodeUVMap):
+            return 0
+
+        if input_node.uv_map == '':
+            return 0
+
+        # Try to gather map index.
+        for blender_mesh in bpy.data.meshes:
+            texCoordIndex = blender_mesh.uv_textures.find(input_node.uv_map)
+            if texCoordIndex >= 0:
+                return texCoordIndex
+
+        return 0
+    elif isinstance(blender_shader_sockets_or_texture_slots[0], bpy.types.MaterialTextureSlot):
+        # TODO: implement for texture slots
+        return 0
+    else:
+        raise NotImplementedError()
+
+
+def __get_tex_from_socket(socket):
+    result = gltf2_blender_search_node_tree.from_socket(
+        socket,
+        gltf2_blender_search_node_tree.FilterByType(bpy.types.ShaderNodeTexImage))
+    if not result:
+        return None
+    return result[0]
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_generate_extras.py b/io_scene_gltf2/blender/exp/gltf2_blender_generate_extras.py
new file mode 100755
index 0000000000000000000000000000000000000000..c26c494a900aebf3b37d0ab346e31676d643e552
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_generate_extras.py
@@ -0,0 +1,64 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import bpy
+from io_scene_gltf2.blender.com import gltf2_blender_json
+
+
+def generate_extras(blender_element):
+    """Filter and create a custom property, which is stored in the glTF extra field."""
+    if not blender_element:
+        return None
+
+    extras = {}
+
+    # Custom properties, which are in most cases present and should not be exported.
+    black_list = ['cycles', 'cycles_visibility', 'cycles_curves', '_RNA_UI']
+
+    count = 0
+    for custom_property in blender_element.keys():
+        if custom_property in black_list:
+            continue
+
+        value = blender_element[custom_property]
+
+        add_value = False
+
+        if isinstance(value, bpy.types.ID):
+            add_value = True
+
+        if isinstance(value, str):
+            add_value = True
+
+        if isinstance(value, (int, float)):
+            add_value = True
+
+        if hasattr(value, "to_list"):
+            value = value.to_list()
+            add_value = True
+
+        if hasattr(value, "to_dict"):
+            value = value.to_dict()
+            add_value = gltf2_blender_json.is_json_convertible(value)
+
+        if add_value:
+            extras[custom_property] = value
+            count += 1
+
+    if count == 0:
+        return None
+
+    return extras
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_get.py b/io_scene_gltf2/blender/exp/gltf2_blender_get.py
new file mode 100755
index 0000000000000000000000000000000000000000..27135224e359984c95247a177e85d4b83f45fcc5
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_get.py
@@ -0,0 +1,376 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Imports
+#
+
+import bpy
+
+from . import gltf2_blender_export_keys
+from ...io.exp import gltf2_io_get
+from io_scene_gltf2.io.com import gltf2_io_debug
+#
+# Globals
+#
+
+#
+# Functions
+#
+
+
+def get_animation_target(action_group: bpy.types.ActionGroup):
+    return action_group.channels[0].data_path.split('.')[-1]
+
+
+def get_socket_or_texture_slot(blender_material: bpy.types.Material, name: str):
+    """
+    For a given material input name, retrieve the corresponding node tree socket or blender render texture slot.
+
+    :param blender_material: a blender material for which to get the socket/slot
+    :param name: the name of the socket/slot
+    :return: either a blender NodeSocket, if the material is a node tree or a blender Texture otherwise
+    """
+    if blender_material.node_tree and blender_material.use_nodes:
+        if name == "Emissive":
+            # Emissive is a special case as  the input node in the 'Emission' shader node is named 'Color' and only the
+            # output is named 'Emission'
+            links = [link for link in blender_material.node_tree.links if link.from_socket.name == 'Emission']
+            if not links:
+                return None
+            return links[0].to_socket
+        i = [input for input in blender_material.node_tree.inputs]
+        o = [output for output in blender_material.node_tree.outputs]
+        nodes = [node for node in blender_material.node_tree.nodes]
+        nodes = filter(lambda n: isinstance(n, bpy.types.ShaderNodeBsdfPrincipled), nodes)
+        inputs = sum([[input for input in node.inputs if input.name == name] for node in nodes], [])
+        if not inputs:
+            return None
+        return inputs[0]
+
+
+
+    return None
+
+
+def find_shader_image_from_shader_socket(shader_socket, max_hops=10):
+    """Find any ShaderNodeTexImage in the path from the socket."""
+    if shader_socket is None:
+        return None
+
+    if max_hops <= 0:
+        return None
+
+    for link in shader_socket.links:
+        if isinstance(link.from_node, bpy.types.ShaderNodeTexImage):
+            return link.from_node
+
+        for socket in link.from_node.inputs.values():
+            image = find_shader_image_from_shader_socket(shader_socket=socket, max_hops=max_hops - 1)
+            if image is not None:
+                return image
+
+    return None
+
+
+def get_shader_add_to_shader_node(shader_node):
+
+    if shader_node is None:
+        return None
+
+    if len(shader_node.outputs['BSDF'].links) == 0:
+        return None
+
+    to_node = shader_node.outputs['BSDF'].links[0].to_node
+
+    if not isinstance(to_node, bpy.types.ShaderNodeAddShader):
+        return None
+
+    return to_node
+
+#
+
+
+def get_shader_emission_from_shader_add(shader_add):
+
+    if shader_add is None:
+        return None
+
+    if not isinstance(shader_add, bpy.types.ShaderNodeAddShader):
+        return None
+
+    from_node = None
+
+    for input in shader_add.inputs:
+
+        if len(input.links) == 0:
+            continue
+
+        from_node = input.links[0].from_node
+
+        if isinstance(from_node, bpy.types.ShaderNodeEmission):
+            break
+
+    return from_node
+
+
+def get_shader_mapping_from_shader_image(shader_image):
+
+    if shader_image is None:
+        return None
+
+    if not isinstance(shader_image, bpy.types.ShaderNodeTexImage):
+        return None
+
+    if shader_image.inputs.get('Vector') is None:
+        return None
+
+    if len(shader_image.inputs['Vector'].links) == 0:
+        return None
+
+    from_node = shader_image.inputs['Vector'].links[0].from_node
+
+    #
+
+    if not isinstance(from_node, bpy.types.ShaderNodeMapping):
+        return None
+
+    return from_node
+
+
+def get_image_material_usage_to_socket(shader_image, socket_name):
+    if shader_image is None:
+        return -1
+
+    if not isinstance(shader_image, bpy.types.ShaderNodeTexImage):
+        return -2
+
+    if shader_image.outputs.get('Color') is None:
+        return -3
+
+    if len(shader_image.outputs.get('Color').links) == 0:
+        return -4
+
+    for img_link in shader_image.outputs.get('Color').links:
+        separate_rgb = img_link.to_node
+
+        if not isinstance(separate_rgb, bpy.types.ShaderNodeSeparateRGB):
+            continue
+
+        for i, channel in enumerate("RGB"):
+            if separate_rgb.outputs.get(channel) is None:
+                continue
+            for link in separate_rgb.outputs.get(channel).links:
+                if socket_name == link.to_socket.name:
+                    return i
+
+    return -6
+
+
+def get_emission_node_from_lamp_output_node(lamp_node):
+    if lamp_node is None:
+        return None
+
+    if not isinstance(lamp_node, bpy.types.ShaderNodeOutputLamp):
+        return None
+
+    if lamp_node.inputs.get('Surface') is None:
+        return None
+
+    if len(lamp_node.inputs.get('Surface').links) == 0:
+        return None
+
+    from_node = lamp_node.inputs.get('Surface').links[0].from_node
+    if isinstance(from_node, bpy.types.ShaderNodeEmission):
+        return from_node
+
+    return None
+
+
+def get_ligth_falloff_node_from_emission_node(emission_node, type):
+    if emission_node is None:
+        return None
+
+    if not isinstance(emission_node, bpy.types.ShaderNodeEmission):
+        return None
+
+    if emission_node.inputs.get('Strength') is None:
+        return None
+
+    if len(emission_node.inputs.get('Strength').links) == 0:
+        return None
+
+    from_node = emission_node.inputs.get('Strength').links[0].from_node
+    if not isinstance(from_node, bpy.types.ShaderNodeLightFalloff):
+        return None
+
+    if from_node.outputs.get(type) is None:
+        return None
+
+    if len(from_node.outputs.get(type).links) == 0:
+        return None
+
+    if emission_node != from_node.outputs.get(type).links[0].to_node:
+        return None
+
+    return from_node
+
+
+def get_shader_image_from_shader_node(name, shader_node):
+
+    if shader_node is None:
+        return None
+
+    if not isinstance(shader_node, bpy.types.ShaderNodeGroup) and \
+            not isinstance(shader_node, bpy.types.ShaderNodeBsdfPrincipled) and \
+            not isinstance(shader_node, bpy.types.ShaderNodeEmission):
+        return None
+
+    if shader_node.inputs.get(name) is None:
+        return None
+
+    if len(shader_node.inputs[name].links) == 0:
+        return None
+
+    from_node = shader_node.inputs[name].links[0].from_node
+
+    #
+
+    if isinstance(from_node, bpy.types.ShaderNodeNormalMap):
+
+        name = 'Color'
+
+        if len(from_node.inputs[name].links) == 0:
+            return None
+
+        from_node = from_node.inputs[name].links[0].from_node
+
+    #
+
+    if not isinstance(from_node, bpy.types.ShaderNodeTexImage):
+        return None
+
+    return from_node
+
+
+def get_texture_index_from_shader_node(export_settings, glTF, name, shader_node):
+    """Return the texture index in the glTF array."""
+    from_node = get_shader_image_from_shader_node(name, shader_node)
+
+    if from_node is None:
+        return -1
+
+    #
+
+    if from_node.image is None or from_node.image.size[0] == 0 or from_node.image.size[1] == 0:
+        return -1
+
+    return gltf2_io_get.get_texture_index(glTF, from_node.image.name)
+
+
+def get_texture_index_from_export_settings(export_settings, name):
+    """Return the texture index in the glTF array."""
+
+
+def get_texcoord_index_from_shader_node(glTF, name, shader_node):
+    """Return the texture coordinate index, if assigned and used."""
+    from_node = get_shader_image_from_shader_node(name, shader_node)
+
+    if from_node is None:
+        return 0
+
+    #
+
+    if len(from_node.inputs['Vector'].links) == 0:
+        return 0
+
+    input_node = from_node.inputs['Vector'].links[0].from_node
+
+    #
+
+    if isinstance(input_node, bpy.types.ShaderNodeMapping):
+
+        if len(input_node.inputs['Vector'].links) == 0:
+            return 0
+
+        input_node = input_node.inputs['Vector'].links[0].from_node
+
+    #
+
+    if not isinstance(input_node, bpy.types.ShaderNodeUVMap):
+        return 0
+
+    if input_node.uv_map == '':
+        return 0
+
+    #
+
+    # Try to gather map index.
+    for blender_mesh in bpy.data.meshes:
+        texCoordIndex = blender_mesh.uv_textures.find(input_node.uv_map)
+        if texCoordIndex >= 0:
+            return texCoordIndex
+
+    return 0
+
+
+def get_image_uri(export_settings, blender_image):
+    """Return the final URI depending on a file path."""
+    file_format = get_image_format(export_settings, blender_image)
+    extension = '.jpg' if file_format == 'JPEG' else '.png'
+
+    return gltf2_io_get.get_image_name(blender_image.name) + extension
+
+
+def get_image_format(export_settings, blender_image):
+    """
+    Return the final output format of the given image.
+
+    Only PNG and JPEG are supported as outputs - all other formats must be converted.
+    """
+    if blender_image.file_format in ['PNG', 'JPEG']:
+        return blender_image.file_format
+
+    use_alpha = export_settings[gltf2_blender_export_keys.FILTERED_IMAGES_USE_ALPHA].get(blender_image.name)
+
+    return 'PNG' if use_alpha else 'JPEG'
+
+
+def get_node(data_path):
+    """Return Blender node on a given Blender data path."""
+    if data_path is None:
+        return None
+
+    index = data_path.find("[\"")
+    if (index == -1):
+        return None
+
+    node_name = data_path[(index + 2):]
+
+    index = node_name.find("\"")
+    if (index == -1):
+        return None
+
+    return node_name[:(index)]
+
+
+def get_data_path(data_path):
+    """Return Blender data path."""
+    index = data_path.rfind('.')
+
+    if index == -1:
+        return data_path
+
+    return data_path[(index + 1):]
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gltf2_exporter.py b/io_scene_gltf2/blender/exp/gltf2_blender_gltf2_exporter.py
new file mode 100755
index 0000000000000000000000000000000000000000..14b62c230f89efa70b05a71ba3fd89366edbf4a4
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gltf2_exporter.py
@@ -0,0 +1,263 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.io.exp import gltf2_io_binary_data
+from io_scene_gltf2.io.exp import gltf2_io_image_data
+from io_scene_gltf2.io.exp import gltf2_io_buffer
+
+
+class GlTF2Exporter:
+    """
+    The glTF exporter flattens a scene graph to a glTF serializable format.
+
+    Any child properties are replaced with references where necessary
+    """
+
+    def __init__(self, copyright=None):
+        self.__finalized = False
+
+        asset = gltf2_io.Asset(
+            copyright=copyright,
+            extensions=None,
+            extras=None,
+            generator='Khronos Blender glTF 2.0 I/O',
+            min_version=None,
+            version='2.0')
+
+        self.__gltf = gltf2_io.Gltf(
+            accessors=[],
+            animations=[],
+            asset=asset,
+            buffers=[],
+            buffer_views=[],
+            cameras=[],
+            extensions={},
+            extensions_required=[],
+            extensions_used=[],
+            extras=None,
+            images=[],
+            materials=[],
+            meshes=[],
+            nodes=[],
+            samplers=[],
+            scene=-1,
+            scenes=[],
+            skins=[],
+            textures=[]
+        )
+
+        self.__buffer = gltf2_io_buffer.Buffer()
+        self.__images = []
+
+        # mapping of all glTFChildOfRootProperty types to their corresponding root level arrays
+        self.__childOfRootPropertyTypeLookup = {
+            gltf2_io.Accessor: self.__gltf.accessors,
+            gltf2_io.Animation: self.__gltf.animations,
+            gltf2_io.Buffer: self.__gltf.buffers,
+            gltf2_io.BufferView: self.__gltf.buffer_views,
+            gltf2_io.Camera: self.__gltf.cameras,
+            gltf2_io.Image: self.__gltf.images,
+            gltf2_io.Material: self.__gltf.materials,
+            gltf2_io.Mesh: self.__gltf.meshes,
+            gltf2_io.Node: self.__gltf.nodes,
+            gltf2_io.Sampler: self.__gltf.samplers,
+            gltf2_io.Scene: self.__gltf.scenes,
+            gltf2_io.Skin: self.__gltf.skins,
+            gltf2_io.Texture: self.__gltf.textures
+        }
+
+        self.__propertyTypeLookup = [
+            gltf2_io.AccessorSparseIndices,
+            gltf2_io.AccessorSparse,
+            gltf2_io.AccessorSparseValues,
+            gltf2_io.AnimationChannel,
+            gltf2_io.AnimationChannelTarget,
+            gltf2_io.AnimationSampler,
+            gltf2_io.Asset,
+            gltf2_io.CameraOrthographic,
+            gltf2_io.CameraPerspective,
+            gltf2_io.MeshPrimitive,
+            gltf2_io.TextureInfo,
+            gltf2_io.MaterialPBRMetallicRoughness,
+            gltf2_io.MaterialNormalTextureInfoClass,
+            gltf2_io.MaterialOcclusionTextureInfoClass
+        ]
+
+    @property
+    def glTF(self):
+        if not self.__finalized:
+            raise RuntimeError("glTF requested, but buffers are not finalized yet")
+        return self.__gltf
+
+    def finalize_buffer(self, output_path=None, buffer_name=None, is_glb=False):
+        """
+        Finalize the glTF and write buffers.
+
+        :param buffer_path:
+        :return:
+        """
+        if self.__finalized:
+            raise RuntimeError("Tried to finalize buffers for finalized glTF file")
+
+        if is_glb:
+            uri = None
+        elif output_path and buffer_name:
+            with open(output_path + buffer_name, 'wb') as f:
+                f.write(self.__buffer.to_bytes())
+            uri = buffer_name
+        else:
+            uri = self.__buffer.to_embed_string()
+
+        buffer = gltf2_io.Buffer(
+            byte_length=self.__buffer.byte_length,
+            extensions=None,
+            extras=None,
+            name=None,
+            uri=uri
+        )
+        self.__gltf.buffers.append(buffer)
+
+        self.__finalized = True
+
+        if is_glb:
+            return self.__buffer.to_bytes()
+
+    def finalize_images(self, output_path):
+        """
+        Write all images.
+
+        Due to a current limitation the output_path must be the same as that of the glTF file
+        :param output_path:
+        :return:
+        """
+        for image in self.__images:
+            uri = output_path + image.name + ".png"
+            with open(uri, 'wb') as f:
+                f.write(image.to_png_data())
+
+    def add_scene(self, scene: gltf2_io.Scene, active: bool = True):
+        """
+        Add a scene to the glTF.
+
+        The scene should be built up with the generated glTF classes
+        :param scene: gltf2_io.Scene type. Root node of the scene graph
+        :param active: If true, sets the glTD.scene index to the added scene
+        :return: nothing
+        """
+        if self.__finalized:
+            raise RuntimeError("Tried to add scene to finalized glTF file")
+
+        # for node in scene.nodes:
+        #     self.__traverse(node)
+        scene_num = self.__traverse(scene)
+        if active:
+            self.__gltf.scene = scene_num
+
+    def add_animation(self, animation: gltf2_io.Animation):
+        """
+        Add an animation to the glTF.
+
+        :param animation: glTF animation, with python style references (names)
+        :return: nothing
+        """
+        if self.__finalized:
+            raise RuntimeError("Tried to add animation to finalized glTF file")
+
+        self.__traverse(animation)
+
+    def __to_reference(self, property):
+        """
+        Append a child of root property to its respective list and return a reference into said list.
+
+        If the property is not child of root, the property itself is returned.
+        :param property: A property type object that should be converted to a reference
+        :return: a reference or the object itself if it is not child or root
+        """
+        gltf_list = self.__childOfRootPropertyTypeLookup.get(type(property), None)
+        if gltf_list is None:
+            # The object is not of a child of root --> don't convert to reference
+            return property
+
+        return self.__append_unique_and_get_index(gltf_list, property)
+
+    @staticmethod
+    def __append_unique_and_get_index(target: list, obj):
+        if obj in target:
+            return target.index(obj)
+        else:
+            index = len(target)
+            target.append(obj)
+            return index
+
+    def __add_image(self, image: gltf2_io_image_data.ImageData):
+        self.__images.append(image)
+        # TODO: we need to know the image url at this point already --> maybe add all options to the constructor of the
+        # exporter
+        # TODO: allow embedding of images (base64)
+        return image.name + ".png"
+
+    def __traverse(self, node):
+        """
+        Recursively traverse a scene graph consisting of gltf compatible elements.
+
+        The tree is traversed downwards until a primitive is reached. Then any ChildOfRoot property
+        is stored in the according list in the glTF and replaced with a index reference in the upper level.
+        """
+        def traverse_all_members(node):
+            for member_name in [a for a in dir(node) if not a.startswith('__') and not callable(getattr(node, a))]:
+                new_value = self.__traverse(getattr(node, member_name))
+                setattr(node, member_name, new_value)  # usually this is the same as before
+
+                # TODO: maybe with extensions hooks we can find a more elegant solution
+                if member_name == "extensions" and new_value is not None:
+                    for extension_name in new_value.keys():
+                        self.__append_unique_and_get_index(self.__gltf.extensions_used, extension_name)
+                        self.__append_unique_and_get_index(self.__gltf.extensions_required, extension_name)
+            return node
+
+        # traverse nodes of a child of root property type and add them to the glTF root
+        if type(node) in self.__childOfRootPropertyTypeLookup:
+            node = traverse_all_members(node)
+            idx = self.__to_reference(node)
+            # child of root properties are only present at root level --> replace with index in upper level
+            return idx
+
+        # traverse lists, such as children and replace them with indices
+        if isinstance(node, list):
+            for i in range(len(node)):
+                node[i] = self.__traverse(node[i])
+            return node
+
+        if isinstance(node, dict):
+            for key in node.keys():
+                node[key] = self.__traverse(node[key])
+            return node
+
+        # traverse into any other property
+        if type(node) in self.__propertyTypeLookup:
+            return traverse_all_members(node)
+
+        # binary data needs to be moved to a buffer and referenced with a buffer view
+        if isinstance(node, gltf2_io_binary_data.BinaryData):
+            buffer_view = self.__buffer.add_and_get_view(node)
+            return self.__to_reference(buffer_view)
+
+        # image data needs to be saved to file
+        if isinstance(node, gltf2_io_image_data.ImageData):
+            return self.__add_image(node)
+
+        # do nothing for any type that does not match a glTF schema (primitives)
+        return node
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_search_node_tree.py b/io_scene_gltf2/blender/exp/gltf2_blender_search_node_tree.py
new file mode 100755
index 0000000000000000000000000000000000000000..a9dc86cfe20b5df2c8bf368109ca073e9a7481da
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_search_node_tree.py
@@ -0,0 +1,97 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Imports
+#
+
+import bpy
+import typing
+
+
+class Filter:
+    """Base class for all node tree filter operations."""
+
+    def __init__(self):
+        pass
+
+    def __call__(self, shader_node):
+        return True
+
+
+class FilterByName(Filter):
+    """
+    Filter the material node tree by name.
+
+    example usage:
+    find_from_socket(start_socket, ShaderNodeFilterByName("Normal"))
+    """
+
+    def __init__(self, name):
+        self.name = name
+        super(FilterByName, self).__init__()
+
+    def __call__(self, shader_node):
+        return shader_node.name == self.name
+
+
+class FilterByType(Filter):
+    """Filter the material node tree by type."""
+
+    def __init__(self, type):
+        self.type = type
+        super(FilterByType, self).__init__()
+
+    def __call__(self, shader_node):
+        return isinstance(shader_node, self.type)
+
+
+class NodeTreeSearchResult:
+    def __init__(self, shader_node: bpy.types.Node, path: typing.List[bpy.types.NodeLink]):
+        self.shader_node = shader_node
+        self.path = path
+
+
+# TODO: cache these searches
+def from_socket(start_socket: bpy.types.NodeSocket,
+                shader_node_filter: typing.Union[Filter, typing.Callable]) -> typing.List[NodeTreeSearchResult]:
+    """
+    Find shader nodes where the filter expression is true.
+
+    :param start_socket: the beginning of the traversal
+    :param shader_node_filter: should be a function(x: shader_node) -> bool
+    :return: a list of shader nodes for which filter is true
+    """
+    # hide implementation (especially the search path
+    def __search_from_socket(start_socket: bpy.types.NodeSocket,
+                             shader_node_filter: typing.Union[Filter, typing.Callable],
+                             search_path: typing.List[bpy.types.NodeLink]) -> typing.List[NodeTreeSearchResult]:
+        results = []
+
+        for link in start_socket.links:
+            # follow the link to a shader node
+            linked_node = link.from_node
+            # add the link to the current path
+            search_path.append(link)
+            # check if the node matches the filter
+            if shader_node_filter(linked_node):
+                results.append(NodeTreeSearchResult(linked_node, search_path))
+            # traverse into inputs of the node
+            for input_socket in linked_node.inputs:
+                results += __search_from_socket(input_socket, shader_node_filter, search_path)
+
+        return results
+
+    return __search_from_socket(start_socket, shader_node_filter, [])
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_search_scene.py b/io_scene_gltf2/blender/exp/gltf2_blender_search_scene.py
new file mode 100755
index 0000000000000000000000000000000000000000..0fa7db6ed51429252e5eb412ceccf32785ec03b8
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_search_scene.py
@@ -0,0 +1,89 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+import typing
+
+
+class Filter:
+    """Base class for all node tree filter operations."""
+
+    def __call__(self, obj: bpy.types.Object):
+        return True
+
+
+class ByName(Filter):
+    """
+    Filter the objects by name.
+
+    example usage:
+    find_objects(FilterByName("Cube"))
+    """
+
+    def __init__(self, name):
+        self.name = name
+
+    def __call__(self, obj: bpy.types.Object):
+        return obj.name == self.name
+
+
+class ByDataType(Filter):
+    """Filter the scene objects by their data type."""
+
+    def __init__(self, data_type: str):
+        self.type = data_type
+
+    def __call__(self, obj: bpy.types.Object):
+        return obj.type == self.type
+
+
+class ByDataInstance(Filter):
+    """Filter the scene objects by a specific ID instance."""
+
+    def __init__(self, data_instance: bpy.types.ID):
+        self.data = data_instance
+
+    def __call__(self, obj: bpy.types.Object):
+        return self.data == obj.data
+
+
+def find_objects(object_filter: typing.Union[Filter, typing.Callable]):
+    """
+    Find objects in the scene where the filter expression is true.
+
+    :param object_filter: should be a function(x: object) -> bool
+    :return: a list of shader nodes for which filter is true
+    """
+    results = []
+    for obj in bpy.context.scene.objects:
+        if object_filter(obj):
+            results.append(obj)
+    return results
+
+
+def find_objects_from(obj: bpy.types.Object, object_filter: typing.Union[Filter, typing.Callable]):
+    """
+    Search for objects matching a filter function below a specified object.
+
+    :param obj: the starting point of the search
+    :param object_filter: a function(x: object) -> bool
+    :return: a list of objects which passed the filter
+    """
+    results = []
+    if object_filter(obj):
+        results.append(obj)
+    for child in obj.children:
+        results += find_objects_from(child, object_filter)
+    return results
+
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_utils.py b/io_scene_gltf2/blender/exp/gltf2_blender_utils.py
new file mode 100755
index 0000000000000000000000000000000000000000..c3e0d6ee904f12bfc1dab903332c36e717e06d0d
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_utils.py
@@ -0,0 +1,68 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import math
+from io_scene_gltf2.io.com import gltf2_io_constants
+
+
+# TODO: we could apply functional programming to these problems (currently we only have a single use case)
+
+def split_list_by_data_type(l: list, data_type: gltf2_io_constants.DataType):
+    """
+    Split a flat list of components by their data type.
+
+    E.g.: A list [0,1,2,3,4,5] of data type Vec3 would be split to [[0,1,2], [3,4,5]]
+    :param l: the flat list
+    :param data_type: the data type of the list
+    :return: a list of lists, where each element list contains the components of the data type
+    """
+    if not (len(l) % gltf2_io_constants.DataType.num_elements(data_type) == 0):
+        raise ValueError("List length does not match specified data type")
+    num_elements = gltf2_io_constants.DataType.num_elements(data_type)
+    return [l[i:i + num_elements] for i in range(0, len(l), num_elements)]
+
+
+def max_components(l: list, data_type: gltf2_io_constants.DataType) -> list:
+    """
+    Find the maximum components in a flat list.
+
+    This is required, for example, for the glTF2.0 accessor min and max properties
+    :param l: the flat list of components
+    :param data_type: the data type of the list (determines the length of the result)
+    :return: a list with length num_elements(data_type) containing the maximum per component along the list
+    """
+    components_lists = split_list_by_data_type(l, data_type)
+    result = [-math.inf] * gltf2_io_constants.DataType.num_elements(data_type)
+    for components in components_lists:
+        for i, c in enumerate(components):
+            result[i] = max(result[i], c)
+    return result
+
+
+def min_components(l: list, data_type: gltf2_io_constants.DataType) -> list:
+    """
+    Find the minimum components in a flat list.
+
+    This is required, for example, for the glTF2.0 accessor min and max properties
+    :param l: the flat list of components
+    :param data_type: the data type of the list (determines the length of the result)
+    :return: a list with length num_elements(data_type) containing the minimum per component along the list
+    """
+    components_lists = split_list_by_data_type(l, data_type)
+    result = [math.inf] * gltf2_io_constants.DataType.num_elements(data_type)
+    for components in components_lists:
+        for i, c in enumerate(components):
+            result[i] = min(result[i], c)
+    return result
+
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_pbrSpecularGlossiness.py b/io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_pbrSpecularGlossiness.py
new file mode 100755
index 0000000000000000000000000000000000000000..610da22e5ad204ba4a250839b421c4077012a97a
--- /dev/null
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_KHR_materials_pbrSpecularGlossiness.py
@@ -0,0 +1,327 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+from .gltf2_blender_texture import BlenderTextureInfo
+
+
+class BlenderKHR_materials_pbrSpecularGlossiness():
+    """Blender KHR_materials_pbrSpecularGlossiness extension."""
+    def __new__(cls, *args, **kwargs):
+        raise RuntimeError("%s should not be instantiated" % cls)
+
+    @staticmethod
+    def create(gltf, pbrSG, mat_name, vertex_color):
+        """KHR_materials_pbrSpecularGlossiness creation."""
+        engine = bpy.context.scene.render.engine
+        if engine in ['CYCLES', 'BLENDER_EEVEE']:
+            BlenderKHR_materials_pbrSpecularGlossiness.create_nodetree(gltf, pbrSG, mat_name, vertex_color)
+
+    @staticmethod
+    def create_nodetree(gltf, pbrSG, mat_name, vertex_color):
+        """Node tree creation."""
+        material = bpy.data.materials[mat_name]
+        material.use_nodes = True
+        node_tree = material.node_tree
+
+        # delete all nodes except output
+        for node in list(node_tree.nodes):
+            if not node.type == 'OUTPUT_MATERIAL':
+                node_tree.nodes.remove(node)
+
+        output_node = node_tree.nodes[0]
+        output_node.location = 1000, 0
+
+        # create PBR node
+        diffuse = node_tree.nodes.new('ShaderNodeBsdfDiffuse')
+        diffuse.location = 0, 0
+        glossy = node_tree.nodes.new('ShaderNodeBsdfGlossy')
+        glossy.location = 0, 100
+        mix = node_tree.nodes.new('ShaderNodeMixShader')
+        mix.location = 500, 0
+
+        glossy.inputs[1].default_value = 1 - pbrSG['glossinessFactor']
+
+        if pbrSG['diffuse_type'] == gltf.SIMPLE:
+            if not vertex_color:
+                # change input values
+                diffuse.inputs[0].default_value = pbrSG['diffuseFactor']
+
+            else:
+                # Create attribute node to get COLOR_0 data
+                attribute_node = node_tree.nodes.new('ShaderNodeAttribute')
+                attribute_node.attribute_name = 'COLOR_0'
+                attribute_node.location = -500, 0
+
+                # links
+                node_tree.links.new(diffuse.inputs[0], attribute_node.outputs[1])
+
+        elif pbrSG['diffuse_type'] == gltf.TEXTURE_FACTOR:
+
+            # TODO alpha ?
+            if vertex_color:
+                # TODO tree locations
+                # Create attribute / separate / math nodes
+                attribute_node = node_tree.nodes.new('ShaderNodeAttribute')
+                attribute_node.attribute_name = 'COLOR_0'
+
+                separate_vertex_color = node_tree.nodes.new('ShaderNodeSeparateRGB')
+                math_vc_R = node_tree.nodes.new('ShaderNodeMath')
+                math_vc_R.operation = 'MULTIPLY'
+
+                math_vc_G = node_tree.nodes.new('ShaderNodeMath')
+                math_vc_G.operation = 'MULTIPLY'
+
+                math_vc_B = node_tree.nodes.new('ShaderNodeMath')
+                math_vc_B.operation = 'MULTIPLY'
+
+            BlenderTextureInfo.create(gltf, pbrSG['diffuseTexture']['index'])
+
+            # create UV Map / Mapping / Texture nodes / separate & math and combine
+            text_node = node_tree.nodes.new('ShaderNodeTexImage')
+            text_node.image = \
+                bpy.data.images[
+                    gltf.data.images[gltf.data.textures[pbrSG['diffuseTexture']['index']].source].blender_image_name
+                ]
+            text_node.location = -1000, 500
+
+            combine = node_tree.nodes.new('ShaderNodeCombineRGB')
+            combine.location = -250, 500
+
+            math_R = node_tree.nodes.new('ShaderNodeMath')
+            math_R.location = -500, 750
+            math_R.operation = 'MULTIPLY'
+            math_R.inputs[1].default_value = pbrSG['diffuseFactor'][0]
+
+            math_G = node_tree.nodes.new('ShaderNodeMath')
+            math_G.location = -500, 500
+            math_G.operation = 'MULTIPLY'
+            math_G.inputs[1].default_value = pbrSG['diffuseFactor'][1]
+
+            math_B = node_tree.nodes.new('ShaderNodeMath')
+            math_B.location = -500, 250
+            math_B.operation = 'MULTIPLY'
+            math_B.inputs[1].default_value = pbrSG['diffuseFactor'][2]
+
+            separate = node_tree.nodes.new('ShaderNodeSeparateRGB')
+            separate.location = -750, 500
+
+            mapping = node_tree.nodes.new('ShaderNodeMapping')
+            mapping.location = -1500, 500
+
+            uvmap = node_tree.nodes.new('ShaderNodeUVMap')
+            uvmap.location = -2000, 500
+            if 'texCoord' in pbrSG['diffuseTexture'].keys():
+                uvmap["gltf2_texcoord"] = pbrSG['diffuseTexture']['texCoord']  # Set custom flag to retrieve TexCoord
+            else:
+                uvmap["gltf2_texcoord"] = 0  # TODO: set in precompute instead of here?
+            # UV Map will be set after object/UVMap creation
+
+            # Create links
+            if vertex_color:
+                node_tree.links.new(separate_vertex_color.inputs[0], attribute_node.outputs[0])
+                node_tree.links.new(math_vc_R.inputs[1], separate_vertex_color.outputs[0])
+                node_tree.links.new(math_vc_G.inputs[1], separate_vertex_color.outputs[1])
+                node_tree.links.new(math_vc_B.inputs[1], separate_vertex_color.outputs[2])
+                node_tree.links.new(math_vc_R.inputs[0], math_R.outputs[0])
+                node_tree.links.new(math_vc_G.inputs[0], math_G.outputs[0])
+                node_tree.links.new(math_vc_B.inputs[0], math_B.outputs[0])
+                node_tree.links.new(combine.inputs[0], math_vc_R.outputs[0])
+                node_tree.links.new(combine.inputs[1], math_vc_G.outputs[0])
+                node_tree.links.new(combine.inputs[2], math_vc_B.outputs[0])
+
+            else:
+                node_tree.links.new(combine.inputs[0], math_R.outputs[0])
+                node_tree.links.new(combine.inputs[1], math_G.outputs[0])
+                node_tree.links.new(combine.inputs[2], math_B.outputs[0])
+
+            # Common for both mode (non vertex color / vertex color)
+            node_tree.links.new(math_R.inputs[0], separate.outputs[0])
+            node_tree.links.new(math_G.inputs[0], separate.outputs[1])
+            node_tree.links.new(math_B.inputs[0], separate.outputs[2])
+
+            node_tree.links.new(mapping.inputs[0], uvmap.outputs[0])
+            node_tree.links.new(text_node.inputs[0], mapping.outputs[0])
+            node_tree.links.new(separate.inputs[0], text_node.outputs[0])
+
+            node_tree.links.new(diffuse.inputs[0], combine.outputs[0])
+
+        elif pbrSG['diffuse_type'] == gltf.TEXTURE:
+
+            BlenderTextureInfo.create(gltf, pbrSG['diffuseTexture']['index'])
+
+            # TODO alpha ?
+            if vertex_color:
+                # Create attribute / separate / math nodes
+                attribute_node = node_tree.nodes.new('ShaderNodeAttribute')
+                attribute_node.attribute_name = 'COLOR_0'
+                attribute_node.location = -2000, 250
+
+                separate_vertex_color = node_tree.nodes.new('ShaderNodeSeparateRGB')
+                separate_vertex_color.location = -1500, 250
+
+                math_vc_R = node_tree.nodes.new('ShaderNodeMath')
+                math_vc_R.operation = 'MULTIPLY'
+                math_vc_R.location = -1000, 750
+
+                math_vc_G = node_tree.nodes.new('ShaderNodeMath')
+                math_vc_G.operation = 'MULTIPLY'
+                math_vc_G.location = -1000, 500
+
+                math_vc_B = node_tree.nodes.new('ShaderNodeMath')
+                math_vc_B.operation = 'MULTIPLY'
+                math_vc_B.location = -1000, 250
+
+                combine = node_tree.nodes.new('ShaderNodeCombineRGB')
+                combine.location = -500, 500
+
+                separate = node_tree.nodes.new('ShaderNodeSeparateRGB')
+                separate.location = -1500, 500
+
+            # create UV Map / Mapping / Texture nodes / separate & math and combine
+            text_node = node_tree.nodes.new('ShaderNodeTexImage')
+            text_node.image = bpy.data.images[
+                gltf.data.images[gltf.data.textures[pbrSG['diffuseTexture']['index']].source].blender_image_name
+            ]
+            if vertex_color:
+                text_node.location = -2000, 500
+            else:
+                text_node.location = -500, 500
+
+            mapping = node_tree.nodes.new('ShaderNodeMapping')
+            if vertex_color:
+                mapping.location = -2500, 500
+            else:
+                mapping.location = -1500, 500
+
+            uvmap = node_tree.nodes.new('ShaderNodeUVMap')
+            if vertex_color:
+                uvmap.location = -3000, 500
+            else:
+                uvmap.location = -2000, 500
+            if 'texCoord' in pbrSG['diffuseTexture'].keys():
+                uvmap["gltf2_texcoord"] = pbrSG['diffuseTexture']['texCoord']  # Set custom flag to retrieve TexCoord
+            else:
+                uvmap["gltf2_texcoord"] = 0  # TODO: set in precompute instead of here?
+            # UV Map will be set after object/UVMap creation
+
+            # Create links
+            if vertex_color:
+                node_tree.links.new(separate_vertex_color.inputs[0], attribute_node.outputs[0])
+
+                node_tree.links.new(math_vc_R.inputs[1], separate_vertex_color.outputs[0])
+                node_tree.links.new(math_vc_G.inputs[1], separate_vertex_color.outputs[1])
+                node_tree.links.new(math_vc_B.inputs[1], separate_vertex_color.outputs[2])
+
+                node_tree.links.new(combine.inputs[0], math_vc_R.outputs[0])
+                node_tree.links.new(combine.inputs[1], math_vc_G.outputs[0])
+                node_tree.links.new(combine.inputs[2], math_vc_B.outputs[0])
+
+                node_tree.links.new(separate.inputs[0], text_node.outputs[0])
+
+                node_tree.links.new(principled.inputs[0], combine.outputs[0])
+
+                node_tree.links.new(math_vc_R.inputs[0], separate.outputs[0])
+                node_tree.links.new(math_vc_G.inputs[0], separate.outputs[1])
+                node_tree.links.new(math_vc_B.inputs[0], separate.outputs[2])
+
+            else:
+                node_tree.links.new(diffuse.inputs[0], text_node.outputs[0])
+
+            # Common for both mode (non vertex color / vertex color)
+
+            node_tree.links.new(mapping.inputs[0], uvmap.outputs[0])
+            node_tree.links.new(text_node.inputs[0], mapping.outputs[0])
+
+        if pbrSG['specgloss_type'] == gltf.SIMPLE:
+
+            combine = node_tree.nodes.new('ShaderNodeCombineRGB')
+            combine.inputs[0].default_value = pbrSG['specularFactor'][0]
+            combine.inputs[1].default_value = pbrSG['specularFactor'][1]
+            combine.inputs[2].default_value = pbrSG['specularFactor'][2]
+
+            # links
+            node_tree.links.new(glossy.inputs[0], combine.outputs[0])
+
+        elif pbrSG['specgloss_type'] == gltf.TEXTURE:
+            BlenderTextureInfo.create(gltf, pbrSG['specularGlossinessTexture']['index'])
+            spec_text = node_tree.nodes.new('ShaderNodeTexImage')
+            spec_text.image = bpy.data.images[
+                gltf.data.images[
+                    gltf.data.textures[pbrSG['specularGlossinessTexture']['index']].source
+                ].blender_image_name
+            ]
+            spec_text.color_space = 'NONE'
+            spec_text.location = -500, 0
+
+            spec_mapping = node_tree.nodes.new('ShaderNodeMapping')
+            spec_mapping.location = -1000, 0
+
+            spec_uvmap = node_tree.nodes.new('ShaderNodeUVMap')
+            spec_uvmap.location = -1500, 0
+            if 'texCoord' in pbrSG['specularGlossinessTexture'].keys():
+                # Set custom flag to retrieve TexCoord
+                spec_uvmap["gltf2_texcoord"] = pbrSG['specularGlossinessTexture']['texCoord']
+            else:
+                spec_uvmap["gltf2_texcoord"] = 0  # TODO: set in precompute instead of here?
+
+            # links
+            node_tree.links.new(glossy.inputs[0], spec_text.outputs[0])
+            node_tree.links.new(mix.inputs[0], spec_text.outputs[1])
+
+            node_tree.links.new(spec_mapping.inputs[0], spec_uvmap.outputs[0])
+            node_tree.links.new(spec_text.inputs[0], spec_mapping.outputs[0])
+
+        elif pbrSG['specgloss_type'] == gltf.TEXTURE_FACTOR:
+
+            BlenderTextureInfo.create(gltf, pbrSG['specularGlossinessTexture']['index'])
+
+            spec_text = node_tree.nodes.new('ShaderNodeTexImage')
+            spec_text.image = bpy.data.images[gltf.data.images[
+                gltf.data.textures[pbrSG['specularGlossinessTexture']['index']].source
+            ].blender_image_name]
+            spec_text.color_space = 'NONE'
+            spec_text.location = -1000, 0
+
+            spec_math = node_tree.nodes.new('ShaderNodeMath')
+            spec_math.operation = 'MULTIPLY'
+            spec_math.inputs[0].default_value = pbrSG['glossinessFactor']
+            spec_math.location = -250, 100
+
+            spec_mapping = node_tree.nodes.new('ShaderNodeMapping')
+            spec_mapping.location = -1000, 0
+
+            spec_uvmap = node_tree.nodes.new('ShaderNodeUVMap')
+            spec_uvmap.location = -1500, 0
+            if 'texCoord' in pbrSG['specularGlossinessTexture'].keys():
+                # Set custom flag to retrieve TexCoord
+                spec_uvmap["gltf2_texcoord"] = pbrSG['specularGlossinessTexture']['texCoord']
+            else:
+                spec_uvmap["gltf2_texcoord"] = 0  # TODO: set in precompute instead of here?
+
+            # links
+
+            node_tree.links.new(spec_math.inputs[1], spec_text.outputs[0])
+            node_tree.links.new(mix.inputs[0], spec_text.outputs[1])
+            node_tree.links.new(glossy.inputs[1], spec_math.outputs[0])
+            node_tree.links.new(glossy.inputs[0], spec_text.outputs[0])
+
+            node_tree.links.new(spec_mapping.inputs[0], spec_uvmap.outputs[0])
+            node_tree.links.new(spec_text.inputs[0], spec_mapping.outputs[0])
+
+        # link node to output
+        node_tree.links.new(mix.inputs[2], diffuse.outputs[0])
+        node_tree.links.new(mix.inputs[1], glossy.outputs[0])
+        node_tree.links.new(output_node.inputs[0], mix.outputs[0])
+
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_animation.py b/io_scene_gltf2/blender/imp/gltf2_blender_animation.py
new file mode 100755
index 0000000000000000000000000000000000000000..4180672a12ec0c069621efa42fef85b91aa77af0
--- /dev/null
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_animation.py
@@ -0,0 +1,35 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from .gltf2_blender_animation_bone import BlenderBoneAnim
+from .gltf2_blender_animation_node import BlenderNodeAnim
+
+
+class BlenderAnimation():
+    """Dispatch Animation to bone or object animation."""
+    def __new__(cls, *args, **kwargs):
+        raise RuntimeError("%s should not be instantiated" % cls)
+
+    @staticmethod
+    def anim(gltf, anim_idx, node_idx):
+        """Dispatch Animation to bone or object."""
+        if gltf.data.nodes[node_idx].is_joint:
+            BlenderBoneAnim.anim(gltf, anim_idx, node_idx)
+        else:
+            BlenderNodeAnim.anim(gltf, anim_idx, node_idx)
+
+        if gltf.data.nodes[node_idx].children:
+            for child in gltf.data.nodes[node_idx].children:
+                BlenderAnimation.anim(gltf, anim_idx, child)
+
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_animation_bone.py b/io_scene_gltf2/blender/imp/gltf2_blender_animation_bone.py
new file mode 100755
index 0000000000000000000000000000000000000000..6b670aea9579963ef53c009b9d409340994da5d0
--- /dev/null
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_animation_bone.py
@@ -0,0 +1,188 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+from mathutils import Matrix
+
+from ..com.gltf2_blender_conversion import loc_gltf_to_blender, quaternion_gltf_to_blender, scale_to_matrix
+from ...io.imp.gltf2_io_binary import BinaryData
+
+
+class BlenderBoneAnim():
+    """Blender Bone Animation."""
+    def __new__(cls, *args, **kwargs):
+        raise RuntimeError("%s should not be instantiated" % cls)
+
+    @staticmethod
+    def set_interpolation(interpolation, kf):
+        """Set interpolation."""
+        if interpolation == "LINEAR":
+            kf.interpolation = 'LINEAR'
+        elif interpolation == "STEP":
+            kf.interpolation = 'CONSTANT'
+        elif interpolation == "CUBICSPLINE":
+            kf.interpolation = 'BEZIER'
+        else:
+            kf.interpolation = 'BEZIER'
+
+    @staticmethod
+    def parse_translation_channel(gltf, node, obj, bone, channel, animation):
+        """Manage Location animation."""
+        fps = bpy.context.scene.render.fps
+        blender_path = "location"
+
+        keys = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].input)
+        values = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].output)
+        inv_bind_matrix = node.blender_bone_matrix.to_quaternion().to_matrix().to_4x4().inverted() \
+            @ Matrix.Translation(node.blender_bone_matrix.to_translation()).inverted()
+
+        for idx, key in enumerate(keys):
+            if animation.samplers[channel.sampler].interpolation == "CUBICSPLINE":
+                # TODO manage tangent?
+                translation_keyframe = loc_gltf_to_blender(values[idx * 3 + 1])
+            else:
+                translation_keyframe = loc_gltf_to_blender(values[idx])
+            if not node.parent:
+                parent_mat = Matrix()
+            else:
+                if not gltf.data.nodes[node.parent].is_joint:
+                    parent_mat = Matrix()
+                else:
+                    parent_mat = gltf.data.nodes[node.parent].blender_bone_matrix
+
+            # Pose is in object (armature) space and it's value if the offset from the bind pose
+            # (which is also in object space)
+            # Scale is not taken into account
+            final_trans = (parent_mat @ Matrix.Translation(translation_keyframe)).to_translation()
+            bone.location = inv_bind_matrix @ final_trans
+            bone.keyframe_insert(blender_path, frame=key[0] * fps, group="location")
+
+        for fcurve in [curve for curve in obj.animation_data.action.fcurves if curve.group.name == "location"]:
+            for kf in fcurve.keyframe_points:
+                BlenderBoneAnim.set_interpolation(animation.samplers[channel.sampler].interpolation, kf)
+
+    @staticmethod
+    def parse_rotation_channel(gltf, node, obj, bone, channel, animation):
+        """Manage rotation animation."""
+        # Note: some operations lead to issue with quaternions. Converting to matrix and then back to quaternions breaks
+        # quaternion continuity
+        # (see antipodal quaternions). Blender interpolates between two antipodal quaternions, which causes glitches in
+        # animation.
+        # Converting to euler and then back to quaternion is a dirty fix preventing this issue in animation, until a
+        # better solution is found
+        # This fix is skipped when parent matrix is identity
+        fps = bpy.context.scene.render.fps
+        blender_path = "rotation_quaternion"
+
+        keys = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].input)
+        values = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].output)
+        bind_rotation = node.blender_bone_matrix.to_quaternion()
+
+        for idx, key in enumerate(keys):
+            if animation.samplers[channel.sampler].interpolation == "CUBICSPLINE":
+                # TODO manage tangent?
+                quat_keyframe = quaternion_gltf_to_blender(values[idx * 3 + 1])
+            else:
+                quat_keyframe = quaternion_gltf_to_blender(values[idx])
+            if not node.parent:
+                bone.rotation_quaternion = bind_rotation.inverted() @ quat_keyframe
+            else:
+                if not gltf.data.nodes[node.parent].is_joint:
+                    parent_mat = Matrix()
+                else:
+                    parent_mat = gltf.data.nodes[node.parent].blender_bone_matrix
+
+                if parent_mat != parent_mat.inverted():
+                    final_rot = (parent_mat @ quat_keyframe.to_matrix().to_4x4()).to_quaternion()
+                    bone.rotation_quaternion = bind_rotation.rotation_difference(final_rot).to_euler().to_quaternion()
+                else:
+                    bone.rotation_quaternion = \
+                        bind_rotation.rotation_difference(quat_keyframe).to_euler().to_quaternion()
+
+            bone.keyframe_insert(blender_path, frame=key[0] * fps, group='rotation')
+
+        for fcurve in [curve for curve in obj.animation_data.action.fcurves if curve.group.name == "rotation"]:
+            for kf in fcurve.keyframe_points:
+                BlenderBoneAnim.set_interpolation(animation.samplers[channel.sampler].interpolation, kf)
+
+    @staticmethod
+    def parse_scale_channel(gltf, node, obj, bone, channel, animation):
+        """Manage scaling animation."""
+        fps = bpy.context.scene.render.fps
+        blender_path = "scale"
+
+        keys = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].input)
+        values = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].output)
+        bind_scale = scale_to_matrix(node.blender_bone_matrix.to_scale())
+
+        for idx, key in enumerate(keys):
+            if animation.samplers[channel.sampler].interpolation == "CUBICSPLINE":
+                # TODO manage tangent?
+                scale_mat = scale_to_matrix(loc_gltf_to_blender(values[idx * 3 + 1]))
+            else:
+                scale_mat = scale_to_matrix(loc_gltf_to_blender(values[idx]))
+            if not node.parent:
+                bone.scale = (bind_scale.inverted() @ scale_mat).to_scale()
+            else:
+                if not gltf.data.nodes[node.parent].is_joint:
+                    parent_mat = Matrix()
+                else:
+                    parent_mat = gltf.data.nodes[node.parent].blender_bone_matrix
+
+                bone.scale = (
+                    bind_scale.inverted() @ scale_to_matrix(parent_mat.to_scale()) @ scale_mat
+                ).to_scale()
+
+            bone.keyframe_insert(blender_path, frame=key[0] * fps, group='scale')
+
+        for fcurve in [curve for curve in obj.animation_data.action.fcurves if curve.group.name == "scale"]:
+            for kf in fcurve.keyframe_points:
+                BlenderBoneAnim.set_interpolation(animation.samplers[channel.sampler].interpolation, kf)
+
+    @staticmethod
+    def anim(gltf, anim_idx, node_idx):
+        """Manage animation."""
+        node = gltf.data.nodes[node_idx]
+        obj = bpy.data.objects[gltf.data.skins[node.skin_id].blender_armature_name]
+        bone = obj.pose.bones[node.blender_bone_name]
+
+        if anim_idx not in node.animations.keys():
+            return
+
+        animation = gltf.data.animations[anim_idx]
+
+        if animation.name:
+            name = animation.name + "_" + obj.name
+        else:
+            name = "Animation_" + str(anim_idx) + "_" + obj.name
+        if name not in bpy.data.actions:
+            action = bpy.data.actions.new(name)
+        else:
+            action = bpy.data.actions[name]
+        if not obj.animation_data:
+            obj.animation_data_create()
+        obj.animation_data.action = bpy.data.actions[action.name]
+
+        for channel_idx in node.animations[anim_idx]:
+            channel = animation.channels[channel_idx]
+
+            if channel.target.path == "translation":
+                BlenderBoneAnim.parse_translation_channel(gltf, node, obj, bone, channel, animation)
+
+            elif channel.target.path == "rotation":
+                BlenderBoneAnim.parse_rotation_channel(gltf, node, obj, bone, channel, animation)
+
+            elif channel.target.path == "scale":
+                BlenderBoneAnim.parse_scale_channel(gltf, node, obj, bone, channel, animation)
+
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_animation_node.py b/io_scene_gltf2/blender/imp/gltf2_blender_animation_node.py
new file mode 100755
index 0000000000000000000000000000000000000000..a5d4b40f5f9c6741dc47fd25adb461b101529268
--- /dev/null
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_animation_node.py
@@ -0,0 +1,132 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+from mathutils import Vector
+
+from ..com.gltf2_blender_conversion import loc_gltf_to_blender, quaternion_gltf_to_blender, scale_gltf_to_blender
+from ...io.imp.gltf2_io_binary import BinaryData
+
+
+class BlenderNodeAnim():
+    """Blender Object Animation."""
+    def __new__(cls, *args, **kwargs):
+        raise RuntimeError("%s should not be instantiated" % cls)
+
+    @staticmethod
+    def set_interpolation(interpolation, kf):
+        """Manage interpolation."""
+        if interpolation == "LINEAR":
+            kf.interpolation = 'LINEAR'
+        elif interpolation == "STEP":
+            kf.interpolation = 'CONSTANT'
+        elif interpolation == "CUBICSPLINE":
+            kf.interpolation = 'BEZIER'
+        else:
+            kf.interpolation = 'BEZIER'
+
+    @staticmethod
+    def anim(gltf, anim_idx, node_idx):
+        """Manage animation."""
+        node = gltf.data.nodes[node_idx]
+        obj = bpy.data.objects[node.blender_object]
+        fps = bpy.context.scene.render.fps
+
+        if anim_idx not in node.animations.keys():
+            return
+
+        animation = gltf.data.animations[anim_idx]
+
+        if animation.name:
+            name = animation.name + "_" + obj.name
+        else:
+            name = "Animation_" + str(anim_idx) + "_" + obj.name
+        action = bpy.data.actions.new(name)
+        if not obj.animation_data:
+            obj.animation_data_create()
+        obj.animation_data.action = bpy.data.actions[action.name]
+
+        for channel_idx in node.animations[anim_idx]:
+            channel = animation.channels[channel_idx]
+
+            keys = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].input)
+            values = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].output)
+
+            if channel.target.path in ['translation', 'rotation', 'scale']:
+
+                if channel.target.path == "translation":
+                    blender_path = "location"
+                    for idx, key in enumerate(keys):
+                        if animation.samplers[channel.sampler].interpolation == "CUBICSPLINE":
+                            # TODO manage tangent?
+                            obj.location = Vector(loc_gltf_to_blender(list(values[idx * 3 + 1])))
+                        else:
+                            obj.location = Vector(loc_gltf_to_blender(list(values[idx])))
+                        obj.keyframe_insert(blender_path, frame=key[0] * fps, group='location')
+
+                    # Setting interpolation
+                    for fcurve in [curve for curve in obj.animation_data.action.fcurves
+                                   if curve.group.name == "location"]:
+                        for kf in fcurve.keyframe_points:
+                            BlenderNodeAnim.set_interpolation(animation.samplers[channel.sampler].interpolation, kf)
+
+                elif channel.target.path == "rotation":
+                    blender_path = "rotation_quaternion"
+                    for idx, key in enumerate(keys):
+                        if animation.samplers[channel.sampler].interpolation == "CUBICSPLINE":
+                            # TODO manage tangent?
+                            obj.rotation_quaternion = quaternion_gltf_to_blender(values[idx * 3 + 1])
+                        else:
+                            obj.rotation_quaternion = quaternion_gltf_to_blender(values[idx])
+                        obj.keyframe_insert(blender_path, frame=key[0] * fps, group='rotation')
+
+                    # Setting interpolation
+                    for fcurve in [curve for curve in obj.animation_data.action.fcurves
+                                   if curve.group.name == "rotation"]:
+                        for kf in fcurve.keyframe_points:
+                            BlenderNodeAnim.set_interpolation(animation.samplers[channel.sampler].interpolation, kf)
+
+                elif channel.target.path == "scale":
+                    blender_path = "scale"
+                    for idx, key in enumerate(keys):
+                        # TODO manage tangent?
+                        if animation.samplers[channel.sampler].interpolation == "CUBICSPLINE":
+                            obj.scale = Vector(scale_gltf_to_blender(list(values[idx * 3 + 1])))
+                        else:
+                            obj.scale = Vector(scale_gltf_to_blender(list(values[idx])))
+                        obj.keyframe_insert(blender_path, frame=key[0] * fps, group='scale')
+
+                    # Setting interpolation
+                    for fcurve in [curve for curve in obj.animation_data.action.fcurves if curve.group.name == "scale"]:
+                        for kf in fcurve.keyframe_points:
+                            BlenderNodeAnim.set_interpolation(animation.samplers[channel.sampler].interpolation, kf)
+
+            elif channel.target.path == 'weights':
+
+                # retrieve number of targets
+                nb_targets = 0
+                for prim in gltf.data.meshes[gltf.data.nodes[node_idx].mesh].primitives:
+                    if prim.targets:
+                        if len(prim.targets) > nb_targets:
+                            nb_targets = len(prim.targets)
+
+                for idx, key in enumerate(keys):
+                    for sk in range(nb_targets):
+                        obj.data.shape_keys.key_blocks[sk + 1].value = values[idx * nb_targets + sk][0]
+                        obj.data.shape_keys.key_blocks[sk + 1].keyframe_insert(
+                            "value",
+                            frame=key[0] * fps,
+                            group='ShapeKeys'
+                        )
+
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_camera.py b/io_scene_gltf2/blender/imp/gltf2_blender_camera.py
new file mode 100755
index 0000000000000000000000000000000000000000..b5a10ac559f6587d1916a8e97f51157dac045c75
--- /dev/null
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_camera.py
@@ -0,0 +1,47 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+
+
+class BlenderCamera():
+    """Blender Camera."""
+    def __new__(cls, *args, **kwargs):
+        raise RuntimeError("%s should not be instantiated" % cls)
+
+    @staticmethod
+    def create(gltf, camera_id):
+        """Camera creation."""
+        pycamera = gltf.data.cameras[camera_id]
+
+        if not pycamera.name:
+            pycamera.name = "Camera"
+
+        cam = bpy.data.cameras.new(pycamera.name)
+
+        # Blender create a perspective camera by default
+        if pycamera.type == "orthographic":
+            cam.type = "ORTHO"
+
+        # TODO: lot's of work for camera here...
+        if hasattr(pycamera, "znear"):
+            cam.clip_start = pycamera.znear
+
+        if hasattr(pycamera, "zfar"):
+            cam.clip_end = pycamera.zfar
+
+        obj = bpy.data.objects.new(pycamera.name, cam)
+        bpy.data.scenes[gltf.blender_scene].collection.objects.link(obj)
+        return obj
+
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_gltf.py b/io_scene_gltf2/blender/imp/gltf2_blender_gltf.py
new file mode 100755
index 0000000000000000000000000000000000000000..9f8a35843103190cdbc80523d88a35765540acb2
--- /dev/null
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_gltf.py
@@ -0,0 +1,217 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+from .gltf2_blender_scene import BlenderScene
+from ...io.com.gltf2_io_trs import TRS
+
+
+class BlenderGlTF():
+    """Main glTF import class."""
+    def __new__(cls, *args, **kwargs):
+        raise RuntimeError("%s should not be instantiated" % cls)
+
+    @staticmethod
+    def create(gltf):
+        """Create glTF main method."""
+        bpy.context.scene.render.engine = 'BLENDER_EEVEE'
+        BlenderGlTF.pre_compute(gltf)
+
+        for scene_idx, scene in enumerate(gltf.data.scenes):
+            BlenderScene.create(gltf, scene_idx)
+
+        # Armature correction
+        # Try to detect bone chains, and set bone lengths
+        # To detect if a bone is in a chain, we try to detect if a bone head is aligned
+        # with parent_bone :
+        #          Parent bone defined a line (between head & tail)
+        #          Bone head defined a point
+        #          Calcul of distance between point and line
+        #          If < threshold --> In a chain
+        # Based on an idea of @Menithal, but added alignement detection to avoid some bad cases
+
+        threshold = 0.001
+        for armobj in [obj for obj in bpy.data.objects if obj.type == "ARMATURE"]:
+            bpy.context.view_layer.objects.active = armobj
+            armature = armobj.data
+            bpy.ops.object.mode_set(mode="EDIT")
+            for bone in armature.edit_bones:
+                if bone.parent is None:
+                    continue
+
+                parent = bone.parent
+
+                # case where 2 bones are aligned (not in chain, same head)
+                if (bone.head - parent.head).length < threshold:
+                    continue
+
+                u = (parent.tail - parent.head).normalized()
+                point = bone.head
+                distance = ((point - parent.head).cross(u)).length / u.length
+                if distance < threshold:
+                    save_parent_direction = (parent.tail - parent.head).normalized().copy()
+                    save_parent_tail = parent.tail.copy()
+                    parent.tail = bone.head
+
+                    # case where 2 bones are aligned (not in chain, same head)
+                    # bone is no more is same direction
+                    if (parent.tail - parent.head).normalized().dot(save_parent_direction) < 0.9:
+                        parent.tail = save_parent_tail
+
+            bpy.ops.object.mode_set(mode="OBJECT")
+
+    @staticmethod
+    def pre_compute(gltf):
+        """Pre compute, just before creation."""
+        # default scene used
+        gltf.blender_scene = None
+
+        # Blender material
+        if gltf.data.materials:
+            for material in gltf.data.materials:
+                material.blender_material = None
+
+                if material.pbr_metallic_roughness:
+                    # Init
+                    material.pbr_metallic_roughness.color_type = gltf.SIMPLE
+                    material.pbr_metallic_roughness.vertex_color = False
+                    material.pbr_metallic_roughness.metallic_type = gltf.SIMPLE
+
+                    if material.pbr_metallic_roughness.base_color_texture:
+                        material.pbr_metallic_roughness.color_type = gltf.TEXTURE
+
+                    if material.pbr_metallic_roughness.metallic_roughness_texture:
+                        material.pbr_metallic_roughness.metallic_type = gltf.TEXTURE
+
+                    if material.pbr_metallic_roughness.base_color_factor:
+                        if material.pbr_metallic_roughness.color_type == gltf.TEXTURE and \
+                                material.pbr_metallic_roughness.base_color_factor != [1.0, 1.0, 1.0, 1.0]:
+                            material.pbr_metallic_roughness.color_type = gltf.TEXTURE_FACTOR
+                    else:
+                        material.pbr_metallic_roughness.base_color_factor = [1.0, 1.0, 1.0, 1.0]
+
+                    if material.pbr_metallic_roughness.metallic_factor is not None:
+                        if material.pbr_metallic_roughness.metallic_type == gltf.TEXTURE \
+                                and material.pbr_metallic_roughness.metallic_factor != 1.0:
+                            material.pbr_metallic_roughness.metallic_type = gltf.TEXTURE_FACTOR
+                    else:
+                        material.pbr_metallic_roughness.metallic_factor = 1.0
+
+                    if material.pbr_metallic_roughness.roughness_factor is not None:
+                        if material.pbr_metallic_roughness.metallic_type == gltf.TEXTURE \
+                                and material.pbr_metallic_roughness.roughness_factor != 1.0:
+                            material.pbr_metallic_roughness.metallic_type = gltf.TEXTURE_FACTOR
+                    else:
+                        material.pbr_metallic_roughness.roughness_factor = 1.0
+
+                # pre compute material for KHR_materials_pbrSpecularGlossiness
+                if material.extensions is not None \
+                        and 'KHR_materials_pbrSpecularGlossiness' in material.extensions.keys():
+                    # Init
+                    material.extensions['KHR_materials_pbrSpecularGlossiness']['diffuse_type'] = gltf.SIMPLE
+                    material.extensions['KHR_materials_pbrSpecularGlossiness']['vertex_color'] = False
+                    material.extensions['KHR_materials_pbrSpecularGlossiness']['specgloss_type'] = gltf.SIMPLE
+
+                    if 'diffuseTexture' in material.extensions['KHR_materials_pbrSpecularGlossiness'].keys():
+                        material.extensions['KHR_materials_pbrSpecularGlossiness']['diffuse_type'] = gltf.TEXTURE
+
+                    if 'diffuseFactor' in material.extensions['KHR_materials_pbrSpecularGlossiness'].keys():
+                        if material.extensions['KHR_materials_pbrSpecularGlossiness']['diffuse_type'] == gltf.TEXTURE \
+                                and material.extensions['KHR_materials_pbrSpecularGlossiness']['diffuseFactor'] != \
+                                [1.0, 1.0, 1.0, 1.0]:
+                            material.extensions['KHR_materials_pbrSpecularGlossiness']['diffuse_type'] = \
+                                gltf.TEXTURE_FACTOR
+                    else:
+                        material.extensions['KHR_materials_pbrSpecularGlossiness']['diffuseFactor'] = \
+                            [1.0, 1.0, 1.0, 1.0]
+
+                    if 'specularGlossinessTexture' in material.extensions['KHR_materials_pbrSpecularGlossiness'].keys():
+                        material.extensions['KHR_materials_pbrSpecularGlossiness']['specgloss_type'] = gltf.TEXTURE
+
+                    if 'specularFactor' in material.extensions['KHR_materials_pbrSpecularGlossiness'].keys():
+                        if material.extensions['KHR_materials_pbrSpecularGlossiness']['specgloss_type'] == \
+                                gltf.TEXTURE \
+                                and material.extensions['KHR_materials_pbrSpecularGlossiness']['specularFactor'] != \
+                                [1.0, 1.0, 1.0]:
+                            material.extensions['KHR_materials_pbrSpecularGlossiness']['specgloss_type'] = \
+                                gltf.TEXTURE_FACTOR
+                    else:
+                        material.extensions['KHR_materials_pbrSpecularGlossiness']['specularFactor'] = [1.0, 1.0, 1.0]
+
+                    if 'glossinessFactor' not in material.extensions['KHR_materials_pbrSpecularGlossiness'].keys():
+                        material.extensions['KHR_materials_pbrSpecularGlossiness']['glossinessFactor'] = 1.0
+
+        for node_idx, node in enumerate(gltf.data.nodes):
+
+            # skin management
+            if node.skin is not None and node.mesh is not None:
+                if not hasattr(gltf.data.skins[node.skin], "node_ids"):
+                    gltf.data.skins[node.skin].node_ids = []
+
+                gltf.data.skins[node.skin].node_ids.append(node_idx)
+
+            # transform management
+            if node.matrix:
+                node.transform = node.matrix
+                continue
+
+            # No matrix, but TRS
+            mat = [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]  # init
+
+            if node.scale:
+                mat = TRS.scale_to_matrix(node.scale)
+
+            if node.rotation:
+                q_mat = TRS.quaternion_to_matrix(node.rotation)
+                mat = TRS.matrix_multiply(q_mat, mat)
+
+            if node.translation:
+                loc_mat = TRS.translation_to_matrix(node.translation)
+                mat = TRS.matrix_multiply(loc_mat, mat)
+
+            node.transform = mat
+
+        # joint management
+        for node_idx, node in enumerate(gltf.data.nodes):
+            is_joint, skin_idx = gltf.is_node_joint(node_idx)
+            if is_joint:
+                node.is_joint = True
+                node.skin_id = skin_idx
+            else:
+                node.is_joint = False
+
+        if gltf.data.skins:
+            for skin_id, skin in enumerate(gltf.data.skins):
+                # init blender values
+                skin.blender_armature_name = None
+                # if skin.skeleton and skin.skeleton not in skin.joints:
+                #     gltf.data.nodes[skin.skeleton].is_joint = True
+                #     gltf.data.nodes[skin.skeleton].skin_id  = skin_id
+
+        # Dispatch animation
+        if gltf.data.animations:
+            for node_idx, node in enumerate(gltf.data.nodes):
+                node.animations = {}
+
+            for anim_idx, anim in enumerate(gltf.data.animations):
+                for channel_idx, channel in enumerate(anim.channels):
+                    if anim_idx not in gltf.data.nodes[channel.target.node].animations.keys():
+                        gltf.data.nodes[channel.target.node].animations[anim_idx] = []
+                    gltf.data.nodes[channel.target.node].animations[anim_idx].append(channel_idx)
+
+        # Meshes
+        if gltf.data.meshes:
+            for mesh in gltf.data.meshes:
+                mesh.blender_name = None
+
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_image.py b/io_scene_gltf2/blender/imp/gltf2_blender_image.py
new file mode 100755
index 0000000000000000000000000000000000000000..ca1eb6266463464f3f90bb06c36f32a9125185ec
--- /dev/null
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_image.py
@@ -0,0 +1,101 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+import os
+import tempfile
+from os.path import dirname, join, isfile, basename
+
+from ...io.imp.gltf2_io_binary import BinaryData
+
+
+# Note that Image is not a glTF2.0 object
+class BlenderImage():
+    """Manage Image."""
+    def __new__(cls, *args, **kwargs):
+        raise RuntimeError("%s should not be instantiated" % cls)
+
+    @staticmethod
+    def get_image_path(gltf, img_idx):
+        """Return image path."""
+        pyimage = gltf.data.images[img_idx]
+
+        image_name = "Image_" + str(img_idx)
+
+        if pyimage.uri:
+            sep = ';base64,'
+            if pyimage.uri[:5] == 'data:':
+                idx = pyimage.uri.find(sep)
+                if idx != -1:
+                    return False, None, None
+
+            if isfile(join(dirname(gltf.filename), pyimage.uri)):
+                return True, join(dirname(gltf.filename), pyimage.uri), basename(join(dirname(gltf.filename), pyimage.uri))
+            else:
+                pyimage.gltf.log.error("Missing file (index " + str(img_idx) + "): " + pyimage.uri)
+                return False, None, None
+
+        if pyimage.buffer_view is None:
+            return False, None, None
+
+        return False, None, None
+
+    @staticmethod
+    def create(gltf, img_idx):
+        """Image creation."""
+        img = gltf.data.images[img_idx]
+
+        img.blender_image_name = None
+
+        if gltf.import_settings['import_pack_images'] is False:
+
+            # Images are not packed (if image is a real file)
+            real, path, img_name = BlenderImage.get_image_path(gltf, img_idx)
+
+            if real is True:
+
+                # Check if image is already loaded
+                for img_ in bpy.data.images:
+                    if img_.filepath == path:
+                        # Already loaded, not needed to reload it
+                        img.blender_image_name = img_.name
+                        return
+
+                blender_image = bpy.data.images.load(path)
+                blender_image.name = img_name
+                img.blender_image_name = blender_image.name
+                return
+
+        # Check if the file is already loaded (packed file)
+        file_creation_needed = True
+        for img_ in bpy.data.images:
+            if hasattr(img_, "gltf_index") and img_['gltf_index'] == img_idx:
+                file_creation_needed = False
+                img.blender_image_name = img_.name
+                break
+
+        if file_creation_needed is True:
+            # Create a temp image, pack, and delete image
+            tmp_image = tempfile.NamedTemporaryFile(delete=False)
+            img_data, img_name = BinaryData.get_image_data(gltf, img_idx)
+            tmp_image.write(img_data)
+            tmp_image.close()
+
+            blender_image = bpy.data.images.load(tmp_image.name)
+            blender_image.pack()
+            blender_image.name = img_name
+            img.blender_image_name = blender_image.name
+            blender_image['gltf_index'] = img_idx
+            os.remove(tmp_image.name)
+
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_map_emissive.py b/io_scene_gltf2/blender/imp/gltf2_blender_map_emissive.py
new file mode 100755
index 0000000000000000000000000000000000000000..81cfd76e25d83baf355cfb8bea5858b5b99e444b
--- /dev/null
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_map_emissive.py
@@ -0,0 +1,110 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+from .gltf2_blender_texture import BlenderTextureInfo
+from ..com.gltf2_blender_material_helpers import get_preoutput_node_output
+
+
+class BlenderEmissiveMap():
+    """Blender Emissive Map."""
+    def __new__(cls, *args, **kwargs):
+        raise RuntimeError("%s should not be instantiated" % cls)
+
+    @staticmethod
+    def create(gltf, material_idx):
+        """Create emissive map."""
+        engine = bpy.context.scene.render.engine
+        if engine in ['CYCLES', 'BLENDER_EEVEE']:
+            BlenderEmissiveMap.create_nodetree(gltf, material_idx)
+
+    def create_nodetree(gltf, material_idx):
+        """Create node tree."""
+        pymaterial = gltf.data.materials[material_idx]
+
+        material = bpy.data.materials[pymaterial.blender_material]
+        node_tree = material.node_tree
+
+        BlenderTextureInfo.create(gltf, pymaterial.emissive_texture.index)
+
+        # check if there is some emssive_factor on material
+        if pymaterial.emissive_factor is None:
+            pymaterial.emissive_factor = [1.0, 1.0, 1.0]
+
+        # retrieve principled node and output node
+        principled = get_preoutput_node_output(node_tree)
+        output = [node for node in node_tree.nodes if node.type == 'OUTPUT_MATERIAL'][0]
+
+        # add nodes
+        emit = node_tree.nodes.new('ShaderNodeEmission')
+        emit.location = 0, 1000
+        if pymaterial.emissive_factor != [1.0, 1.0, 1.0]:
+            separate = node_tree.nodes.new('ShaderNodeSeparateRGB')
+            separate.location = -750, 1000
+            combine = node_tree.nodes.new('ShaderNodeCombineRGB')
+            combine.location = -250, 1000
+        mapping = node_tree.nodes.new('ShaderNodeMapping')
+        mapping.location = -1500, 1000
+        uvmap = node_tree.nodes.new('ShaderNodeUVMap')
+        uvmap.location = -2000, 1000
+        if pymaterial.emissive_texture.tex_coord is not None:
+            uvmap["gltf2_texcoord"] = pymaterial.emissive_texture.tex_coord  # Set custom flag to retrieve TexCoord
+        else:
+            uvmap["gltf2_texcoord"] = 0  # TODO: set in precompute instead of here?
+
+        text = node_tree.nodes.new('ShaderNodeTexImage')
+        text.image = bpy.data.images[gltf.data.images[
+            gltf.data.textures[pymaterial.emissive_texture.index].source
+        ].blender_image_name]
+        text.label = 'EMISSIVE'
+        text.location = -1000, 1000
+        add = node_tree.nodes.new('ShaderNodeAddShader')
+        add.location = 500, 500
+
+        if pymaterial.emissive_factor != [1.0, 1.0, 1.0]:
+            math_R = node_tree.nodes.new('ShaderNodeMath')
+            math_R.location = -500, 1500
+            math_R.operation = 'MULTIPLY'
+            math_R.inputs[1].default_value = pymaterial.emissive_factor[0]
+
+            math_G = node_tree.nodes.new('ShaderNodeMath')
+            math_G.location = -500, 1250
+            math_G.operation = 'MULTIPLY'
+            math_G.inputs[1].default_value = pymaterial.emissive_factor[1]
+
+            math_B = node_tree.nodes.new('ShaderNodeMath')
+            math_B.location = -500, 1000
+            math_B.operation = 'MULTIPLY'
+            math_B.inputs[1].default_value = pymaterial.emissive_factor[2]
+
+        # create links
+        node_tree.links.new(mapping.inputs[0], uvmap.outputs[0])
+        node_tree.links.new(text.inputs[0], mapping.outputs[0])
+        if pymaterial.emissive_factor != [1.0, 1.0, 1.0]:
+            node_tree.links.new(separate.inputs[0], text.outputs[0])
+            node_tree.links.new(math_R.inputs[0], separate.outputs[0])
+            node_tree.links.new(math_G.inputs[0], separate.outputs[1])
+            node_tree.links.new(math_B.inputs[0], separate.outputs[2])
+            node_tree.links.new(combine.inputs[0], math_R.outputs[0])
+            node_tree.links.new(combine.inputs[1], math_G.outputs[0])
+            node_tree.links.new(combine.inputs[2], math_B.outputs[0])
+            node_tree.links.new(emit.inputs[0], combine.outputs[0])
+        else:
+            node_tree.links.new(emit.inputs[0], text.outputs[0])
+
+        # following  links will modify PBR node tree
+        node_tree.links.new(add.inputs[0], emit.outputs[0])
+        node_tree.links.new(add.inputs[1], principled)
+        node_tree.links.new(output.inputs[0], add.outputs[0])
+
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_map_normal.py b/io_scene_gltf2/blender/imp/gltf2_blender_map_normal.py
new file mode 100755
index 0000000000000000000000000000000000000000..ab09f24c8cd4ea38e086b98fcfdfc35e11c1c517
--- /dev/null
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_map_normal.py
@@ -0,0 +1,89 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+from .gltf2_blender_texture import BlenderTextureInfo
+
+
+class BlenderNormalMap():
+    """Blender Normal map."""
+    def __new__(cls, *args, **kwargs):
+        raise RuntimeError("%s should not be instantiated" % cls)
+
+    @staticmethod
+    def create(gltf, material_idx):
+        """Creation of Normal map."""
+        engine = bpy.context.scene.render.engine
+        if engine in ['CYCLES', 'BLENDER_EEVEE']:
+            BlenderNormalMap.create_nodetree(gltf, material_idx)
+
+    def create_nodetree(gltf, material_idx):
+        """Creation of Nodetree."""
+        pymaterial = gltf.data.materials[material_idx]
+
+        material = bpy.data.materials[pymaterial.blender_material]
+        node_tree = material.node_tree
+
+        BlenderTextureInfo.create(gltf, pymaterial.normal_texture.index)
+
+        # retrieve principled node and output node
+        principled = None
+        diffuse = None
+        glossy = None
+        if len([node for node in node_tree.nodes if node.type == "BSDF_PRINCIPLED"]) != 0:
+            principled = [node for node in node_tree.nodes if node.type == "BSDF_PRINCIPLED"][0]
+        else:
+            # No principled, we are probably coming from extension
+            diffuse = [node for node in node_tree.nodes if node.type == "BSDF_DIFFUSE"][0]
+            glossy = [node for node in node_tree.nodes if node.type == "BSDF_GLOSSY"][0]
+
+        # add nodes
+        mapping = node_tree.nodes.new('ShaderNodeMapping')
+        mapping.location = -1000, -500
+        uvmap = node_tree.nodes.new('ShaderNodeUVMap')
+        uvmap.location = -1500, -500
+        if pymaterial.normal_texture.tex_coord is not None:
+            uvmap["gltf2_texcoord"] = pymaterial.normal_texture.tex_coord  # Set custom flag to retrieve TexCoord
+        else:
+            uvmap["gltf2_texcoord"] = 0  # TODO set in pre_compute instead of here
+
+        text = node_tree.nodes.new('ShaderNodeTexImage')
+        text.image = bpy.data.images[gltf.data.images[
+            gltf.data.textures[pymaterial.normal_texture.index].source
+        ].blender_image_name]
+        text.label = 'NORMALMAP'
+        text.color_space = 'NONE'
+        text.location = -500, -500
+
+        normalmap_node = node_tree.nodes.new('ShaderNodeNormalMap')
+        normalmap_node.location = -250, -500
+        if pymaterial.normal_texture.tex_coord is not None:
+            # Set custom flag to retrieve TexCoord
+            normalmap_node["gltf2_texcoord"] = pymaterial.normal_texture.tex_coord
+        else:
+            normalmap_node["gltf2_texcoord"] = 0  # TODO set in pre_compute instead of here
+
+        # create links
+        node_tree.links.new(mapping.inputs[0], uvmap.outputs[0])
+        node_tree.links.new(text.inputs[0], mapping.outputs[0])
+        node_tree.links.new(normalmap_node.inputs[1], text.outputs[0])
+
+        # following  links will modify PBR node tree
+        if principled:
+            node_tree.links.new(principled.inputs[17], normalmap_node.outputs[0])
+        if diffuse:
+            node_tree.links.new(diffuse.inputs[2], normalmap_node.outputs[0])
+        if glossy:
+            node_tree.links.new(glossy.inputs[2], normalmap_node.outputs[0])
+
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_map_occlusion.py b/io_scene_gltf2/blender/imp/gltf2_blender_map_occlusion.py
new file mode 100755
index 0000000000000000000000000000000000000000..5c41ce9eb05ccc7fa10f104f04f2f4c3f88f8461
--- /dev/null
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_map_occlusion.py
@@ -0,0 +1,41 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+from .gltf2_blender_texture import BlenderTextureInfo
+
+
+class BlenderOcclusionMap():
+    """Blender Occlusion map."""
+    def __new__(cls, *args, **kwargs):
+        raise RuntimeError("%s should not be instantiated" % cls)
+
+    @staticmethod
+    def create(gltf, material_idx):
+        """Occlusion map creation."""
+        engine = bpy.context.scene.render.engine
+        if engine in ['CYCLES', 'BLENDER_EEVEE']:
+            BlenderOcclusionMap.create_nodetree(gltf, material_idx)
+
+    def create_nodetree(gltf, material_idx):
+        """Nodetree creation."""
+        pymaterial = gltf.data.materials[material_idx]
+
+        BlenderTextureInfo.create(gltf, pymaterial.occlusion_texture.index)
+
+        # Pack texture, but doesn't use it for now. Occlusion is calculated from Cycles.
+        bpy.data.images[gltf.data.images[gltf.data.textures[
+            pymaterial.occlusion_texture.index
+        ].source].blender_image_name].use_fake_user = True
+
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_material.py b/io_scene_gltf2/blender/imp/gltf2_blender_material.py
new file mode 100755
index 0000000000000000000000000000000000000000..c9fa69278925a82e5f49cdcd275e6bef8df6a374
--- /dev/null
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_material.py
@@ -0,0 +1,150 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+from .gltf2_blender_pbrMetallicRoughness import BlenderPbr
+from .gltf2_blender_KHR_materials_pbrSpecularGlossiness import BlenderKHR_materials_pbrSpecularGlossiness
+from .gltf2_blender_map_emissive import BlenderEmissiveMap
+from .gltf2_blender_map_normal import BlenderNormalMap
+from .gltf2_blender_map_occlusion import BlenderOcclusionMap
+from ..com.gltf2_blender_material_helpers import get_output_surface_input
+from ..com.gltf2_blender_material_helpers import get_preoutput_node_output
+from ..com.gltf2_blender_material_helpers import get_base_color_node
+from ...io.com.gltf2_io import MaterialPBRMetallicRoughness
+
+
+class BlenderMaterial():
+    """Blender Material."""
+    def __new__(cls, *args, **kwargs):
+        raise RuntimeError("%s should not be instantiated" % cls)
+
+    @staticmethod
+    def create(gltf, material_idx, vertex_color):
+        """Material creation."""
+        pymaterial = gltf.data.materials[material_idx]
+
+        if pymaterial.name is not None:
+            name = pymaterial.name
+        else:
+            name = "Material_" + str(material_idx)
+
+        mat = bpy.data.materials.new(name)
+        pymaterial.blender_material = mat.name
+
+        if pymaterial.extensions is not None and 'KHR_materials_pbrSpecularGlossiness' in pymaterial.extensions.keys():
+            BlenderKHR_materials_pbrSpecularGlossiness.create(
+                gltf, pymaterial.extensions['KHR_materials_pbrSpecularGlossiness'], mat.name, vertex_color
+            )
+        else:
+            # create pbr material
+            if pymaterial.pbr_metallic_roughness is None:
+                # If no pbr material is set, we need to apply all default of pbr
+                pbr = {}
+                pbr["baseColorFactor"] = [1.0, 1.0, 1.0, 1.0]
+                pbr["metallicFactor"] = 1.0
+                pbr["roughnessFactor"] = 1.0
+                pymaterial.pbr_metallic_roughness = MaterialPBRMetallicRoughness.from_dict(pbr)
+                pymaterial.pbr_metallic_roughness.color_type = gltf.SIMPLE
+                pymaterial.pbr_metallic_roughness.metallic_type = gltf.SIMPLE
+
+            BlenderPbr.create(gltf, pymaterial.pbr_metallic_roughness, mat.name, vertex_color)
+
+        # add emission map if needed
+        if pymaterial.emissive_texture is not None:
+            BlenderEmissiveMap.create(gltf, material_idx)
+
+        # add normal map if needed
+        if pymaterial.normal_texture is not None:
+            BlenderNormalMap.create(gltf, material_idx)
+
+        # add occlusion map if needed
+        # will be pack, but not used
+        if pymaterial.occlusion_texture is not None:
+            BlenderOcclusionMap.create(gltf, material_idx)
+
+        if pymaterial.alpha_mode is not None and pymaterial.alpha_mode != 'OPAQUE':
+            BlenderMaterial.blender_alpha(gltf, material_idx)
+
+    @staticmethod
+    def set_uvmap(gltf, material_idx, prim, obj):
+        """Set UV Map."""
+        pymaterial = gltf.data.materials[material_idx]
+
+        node_tree = bpy.data.materials[pymaterial.blender_material].node_tree
+        uvmap_nodes = [node for node in node_tree.nodes if node.type in ['UVMAP', 'NORMAL_MAP']]
+        for uvmap_node in uvmap_nodes:
+            if uvmap_node["gltf2_texcoord"] in prim.blender_texcoord.keys():
+                uvmap_node.uv_map = prim.blender_texcoord[uvmap_node["gltf2_texcoord"]]
+
+    @staticmethod
+    def blender_alpha(gltf, material_idx):
+        """Set alpha."""
+        pymaterial = gltf.data.materials[material_idx]
+        material = bpy.data.materials[pymaterial.blender_material]
+
+        node_tree = material.node_tree
+        # Add nodes for basic transparency
+        # Add mix shader between output and Principled BSDF
+        trans = node_tree.nodes.new('ShaderNodeBsdfTransparent')
+        trans.location = 750, -500
+        mix = node_tree.nodes.new('ShaderNodeMixShader')
+        mix.location = 1000, 0
+
+        output_surface_input = get_output_surface_input(node_tree)
+        preoutput_node_output = get_preoutput_node_output(node_tree)
+
+        link = output_surface_input.links[0]
+        node_tree.links.remove(link)
+
+        # PBR => Mix input 1
+        node_tree.links.new(preoutput_node_output, mix.inputs[1])
+
+        # Trans => Mix input 2
+        node_tree.links.new(trans.outputs['BSDF'], mix.inputs[2])
+
+        # Mix => Output
+        node_tree.links.new(mix.outputs['Shader'], output_surface_input)
+
+        # alpha blend factor
+        add = node_tree.nodes.new('ShaderNodeMath')
+        add.operation = 'ADD'
+        add.location = 750, -250
+
+        diffuse_factor = 1.0
+        if pymaterial.extensions is not None and 'KHR_materials_pbrSpecularGlossiness' in pymaterial.extensions:
+            diffuse_factor = pymaterial.extensions['KHR_materials_pbrSpecularGlossiness']['diffuseFactor'][3]
+        elif pymaterial.pbr_metallic_roughness:
+            diffuse_factor = pymaterial.pbr_metallic_roughness.base_color_factor[3]
+
+        add.inputs[0].default_value = abs(1.0 - diffuse_factor)
+        add.inputs[1].default_value = 0.0
+        node_tree.links.new(add.outputs['Value'], mix.inputs[0])
+
+        # Take diffuse texture alpha into account if any
+        diffuse_texture = get_base_color_node(node_tree)
+        if diffuse_texture:
+            inverter = node_tree.nodes.new('ShaderNodeInvert')
+            inverter.location = 250, -250
+            inverter.inputs[1].default_value = (1.0, 1.0, 1.0, 1.0)
+            node_tree.links.new(diffuse_texture.outputs['Alpha'], inverter.inputs[0])
+
+            mult = node_tree.nodes.new('ShaderNodeMath')
+            mult.operation = 'MULTIPLY' if pymaterial.alpha_mode == 'BLEND' else 'GREATER_THAN'
+            mult.location = 500, -250
+            alpha_cutoff = 1.0 if pymaterial.alpha_mode == 'BLEND' else \
+                1.0 - pymaterial.alpha_cutoff if pymaterial.alpha_cutoff is not None else 0.5
+            mult.inputs[1].default_value = alpha_cutoff
+            node_tree.links.new(inverter.outputs['Color'], mult.inputs[0])
+            node_tree.links.new(mult.outputs['Value'], add.inputs[0])
+
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py b/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py
new file mode 100755
index 0000000000000000000000000000000000000000..883964556707f73fdc27ba634ab0f230854d118b
--- /dev/null
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py
@@ -0,0 +1,158 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+import bmesh
+
+from .gltf2_blender_primitive import BlenderPrimitive
+from ...io.imp.gltf2_io_binary import BinaryData
+from ..com.gltf2_blender_conversion import loc_gltf_to_blender
+
+
+class BlenderMesh():
+    """Blender Mesh."""
+    def __new__(cls, *args, **kwargs):
+        raise RuntimeError("%s should not be instantiated" % cls)
+
+    @staticmethod
+    def create(gltf, mesh_idx, node_idx, parent):
+        """Mesh creation."""
+        pymesh = gltf.data.meshes[mesh_idx]
+
+        # Geometry
+        if pymesh.name:
+            mesh_name = pymesh.name
+        else:
+            mesh_name = "Mesh_" + str(mesh_idx)
+
+        mesh = bpy.data.meshes.new(mesh_name)
+        verts = []
+        edges = []
+        faces = []
+        for prim in pymesh.primitives:
+            verts, edges, faces = BlenderPrimitive.create(gltf, prim, verts, edges, faces)
+
+        mesh.from_pydata(verts, edges, faces)
+        mesh.validate()
+
+        pymesh.blender_name = mesh.name
+
+        return mesh
+
+    @staticmethod
+    def set_mesh(gltf, pymesh, mesh, obj):
+        """Set all data after mesh creation."""
+        # Normals
+        offset = 0
+        for prim in pymesh.primitives:
+            offset = BlenderPrimitive.set_normals(gltf, prim, mesh, offset)
+
+        mesh.update()
+
+        # manage UV
+        offset = 0
+        for prim in pymesh.primitives:
+            offset = BlenderPrimitive.set_UV(gltf, prim, obj, mesh, offset)
+
+        mesh.update()
+
+        # Object and UV are now created, we can set UVMap into material
+        for prim in pymesh.primitives:
+            BlenderPrimitive.set_UV_in_mat(gltf, prim, obj)
+
+        # Assign materials to mesh
+        offset = 0
+        cpt_index_mat = 0
+        bm = bmesh.new()
+        bm.from_mesh(obj.data)
+        bm.faces.ensure_lookup_table()
+        for prim in pymesh.primitives:
+            offset, cpt_index_mat = BlenderPrimitive.assign_material(gltf, prim, obj, bm, offset, cpt_index_mat)
+
+        bm.to_mesh(obj.data)
+        bm.free()
+
+        # Create shapekeys if needed
+        max_shape_to_create = 0
+        for prim in pymesh.primitives:
+            if prim.targets:
+                if len(prim.targets) > max_shape_to_create:
+                    max_shape_to_create = len(prim.targets)
+
+        # Create basis shape key
+        if max_shape_to_create > 0:
+            obj.shape_key_add(name="Basis")
+
+        for i in range(max_shape_to_create):
+
+            obj.shape_key_add(name="target_" + str(i))
+
+            offset_idx = 0
+            for prim in pymesh.primitives:
+                if prim.targets is None:
+                    continue
+                if i >= len(prim.targets):
+                    continue
+
+                bm = bmesh.new()
+                bm.from_mesh(mesh)
+
+                shape_layer = bm.verts.layers.shape[i + 1]
+
+                pos = BinaryData.get_data_from_accessor(gltf, prim.targets[i]['POSITION'])
+
+                for vert in bm.verts:
+                    if vert.index not in range(offset_idx, offset_idx + prim.vertices_length):
+                        continue
+
+                    shape = vert[shape_layer]
+
+                    co = loc_gltf_to_blender(list(pos[vert.index - offset_idx]))
+                    shape.x = obj.data.vertices[vert.index].co.x + co[0]
+                    shape.y = obj.data.vertices[vert.index].co.y + co[1]
+                    shape.z = obj.data.vertices[vert.index].co.z + co[2]
+
+                bm.to_mesh(obj.data)
+                bm.free()
+                offset_idx += prim.vertices_length
+
+        # set default weights for shape keys, and names
+        if pymesh.weights is not None:
+            for i in range(max_shape_to_create):
+                if i < len(pymesh.weights):
+                    obj.data.shape_keys.key_blocks[i + 1].value = pymesh.weights[i]
+                    if gltf.data.accessors[pymesh.primitives[0].targets[i]['POSITION']].name is not None:
+                        obj.data.shape_keys.key_blocks[i + 1].name = \
+                            gltf.data.accessors[pymesh.primitives[0].targets[i]['POSITION']].name
+
+        # Apply vertex color.
+        vertex_color = None
+        offset = 0
+        for prim in pymesh.primitives:
+            if 'COLOR_0' in prim.attributes.keys():
+                # Create vertex color, once only per object
+                if vertex_color is None:
+                    vertex_color = obj.data.vertex_colors.new("COLOR_0")
+
+                color_data = BinaryData.get_data_from_accessor(gltf, prim.attributes['COLOR_0'])
+
+                for poly in mesh.polygons:
+                    for loop_idx in range(poly.loop_start, poly.loop_start + poly.loop_total):
+                        vert_idx = mesh.loops[loop_idx].vertex_index
+                        if vert_idx in range(offset, offset + prim.vertices_length):
+                            cpt_idx = vert_idx - offset
+                            vertex_color.data[loop_idx].color = color_data[cpt_idx][0:3]
+                            # TODO : no alpha in vertex color
+            offset = offset + prim.vertices_length
+
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_node.py b/io_scene_gltf2/blender/imp/gltf2_blender_node.py
new file mode 100755
index 0000000000000000000000000000000000000000..a82f1db7793840e0a67ce79d8f71ce387274b61a
--- /dev/null
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_node.py
@@ -0,0 +1,184 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+from .gltf2_blender_mesh import BlenderMesh
+from .gltf2_blender_camera import BlenderCamera
+from .gltf2_blender_skin import BlenderSkin
+from ..com.gltf2_blender_conversion import scale_to_matrix, matrix_gltf_to_blender
+
+
+class BlenderNode():
+    """Blender Node."""
+    def __new__(cls, *args, **kwargs):
+        raise RuntimeError("%s should not be instantiated" % cls)
+
+    @staticmethod
+    def create(gltf, node_idx, parent):
+        """Node creation."""
+        pynode = gltf.data.nodes[node_idx]
+
+        # Blender attributes initialization
+        pynode.blender_object = ""
+        pynode.parent = parent
+
+        if pynode.mesh is not None:
+
+            if gltf.data.meshes[pynode.mesh].blender_name is not None:
+                # Mesh is already created, only create instance
+                mesh = bpy.data.meshes[gltf.data.meshes[pynode.mesh].blender_name]
+            else:
+                if pynode.name:
+                    gltf.log.info("Blender create Mesh node " + pynode.name)
+                else:
+                    gltf.log.info("Blender create Mesh node")
+
+                mesh = BlenderMesh.create(gltf, pynode.mesh, node_idx, parent)
+
+            if pynode.name:
+                name = pynode.name
+            else:
+                # Take mesh name if exist
+                if gltf.data.meshes[pynode.mesh].name:
+                    name = gltf.data.meshes[pynode.mesh].name
+                else:
+                    name = "Object_" + str(node_idx)
+
+            obj = bpy.data.objects.new(name, mesh)
+            obj.rotation_mode = 'QUATERNION'
+            bpy.data.scenes[gltf.blender_scene].collection.objects.link(obj)
+
+            # Transforms apply only if this mesh is not skinned
+            # See implementation node of gltf2 specification
+            if not (pynode.mesh and pynode.skin is not None):
+                BlenderNode.set_transforms(gltf, node_idx, pynode, obj, parent)
+            pynode.blender_object = obj.name
+            BlenderNode.set_parent(gltf, pynode, obj, parent)
+
+            BlenderMesh.set_mesh(gltf, gltf.data.meshes[pynode.mesh], mesh, obj)
+
+            if pynode.children:
+                for child_idx in pynode.children:
+                    BlenderNode.create(gltf, child_idx, node_idx)
+
+            return
+
+        if pynode.camera is not None:
+            if pynode.name:
+                gltf.log.info("Blender create Camera node " + pynode.name)
+            else:
+                gltf.log.info("Blender create Camera node")
+            obj = BlenderCamera.create(gltf, pynode.camera)
+            BlenderNode.set_transforms(gltf, node_idx, pynode, obj, parent)  # TODO default rotation of cameras ?
+            pynode.blender_object = obj.name
+            BlenderNode.set_parent(gltf, pynode, obj, parent)
+
+            return
+
+        if pynode.is_joint:
+            if pynode.name:
+                gltf.log.info("Blender create Bone node " + pynode.name)
+            else:
+                gltf.log.info("Blender create Bone node")
+            # Check if corresponding armature is already created, create it if needed
+            if gltf.data.skins[pynode.skin_id].blender_armature_name is None:
+                BlenderSkin.create_armature(gltf, pynode.skin_id, parent)
+
+            BlenderSkin.create_bone(gltf, pynode.skin_id, node_idx, parent)
+
+            if pynode.children:
+                for child_idx in pynode.children:
+                    BlenderNode.create(gltf, child_idx, node_idx)
+
+            return
+
+        # No mesh, no camera. For now, create empty #TODO
+
+        if pynode.name:
+            gltf.log.info("Blender create Empty node " + pynode.name)
+            obj = bpy.data.objects.new(pynode.name, None)
+        else:
+            gltf.log.info("Blender create Empty node")
+            obj = bpy.data.objects.new("Node", None)
+        obj.rotation_mode = 'QUATERNION'
+        bpy.data.scenes[gltf.blender_scene].collection.objects.link(obj)
+        BlenderNode.set_transforms(gltf, node_idx, pynode, obj, parent)
+        pynode.blender_object = obj.name
+        BlenderNode.set_parent(gltf, pynode, obj, parent)
+
+        if pynode.children:
+            for child_idx in pynode.children:
+                BlenderNode.create(gltf, child_idx, node_idx)
+
+    @staticmethod
+    def set_parent(gltf, pynode, obj, parent):
+        """Set parent."""
+        if parent is None:
+            return
+
+        for node_idx, node in enumerate(gltf.data.nodes):
+            if node_idx == parent:
+                if node.is_joint is True:
+                    bpy.ops.object.select_all(action='DESELECT')
+                    bpy.data.objects[node.blender_armature_name].select_set(True)
+                    bpy.context.view_layer.objects.active = bpy.data.objects[node.blender_armature_name]
+
+                    bpy.ops.object.mode_set(mode='EDIT')
+                    bpy.data.objects[node.blender_armature_name].data.edit_bones.active = \
+                        bpy.data.objects[node.blender_armature_name].data.edit_bones[node.blender_bone_name]
+                    bpy.ops.object.mode_set(mode='OBJECT')
+                    bpy.ops.object.select_all(action='DESELECT')
+                    obj.select_set(True)
+                    bpy.data.objects[node.blender_armature_name].select_set(True)
+                    bpy.context.view_layer.objects.active = bpy.data.objects[node.blender_armature_name]
+                    bpy.context.scene.update()
+                    bpy.ops.object.parent_set(type='BONE_RELATIVE', keep_transform=True)
+                    # From world transform to local (-armature transform -bone transform)
+                    bone_trans = bpy.data.objects[node.blender_armature_name] \
+                        .pose.bones[node.blender_bone_name].matrix.to_translation().copy()
+                    bone_rot = bpy.data.objects[node.blender_armature_name] \
+                        .pose.bones[node.blender_bone_name].matrix.to_quaternion().copy()
+                    bone_scale_mat = scale_to_matrix(node.blender_bone_matrix.to_scale())
+                    obj.location = bone_scale_mat @ obj.location
+                    obj.location = bone_rot @ obj.location
+                    obj.location += bone_trans
+                    obj.location = bpy.data.objects[node.blender_armature_name].matrix_world.to_quaternion() \
+                        @ obj.location
+                    obj.rotation_quaternion = obj.rotation_quaternion \
+                        @ bpy.data.objects[node.blender_armature_name].matrix_world.to_quaternion()
+                    obj.scale = bone_scale_mat @ obj.scale
+
+                    return
+                if node.blender_object:
+                    obj.parent = bpy.data.objects[node.blender_object]
+                    return
+
+        gltf.log.error("ERROR, parent not found")
+
+    @staticmethod
+    def set_transforms(gltf, node_idx, pynode, obj, parent):
+        """Set transforms."""
+        if parent is None:
+            obj.matrix_world = matrix_gltf_to_blender(pynode.transform)
+            return
+
+        for idx, node in enumerate(gltf.data.nodes):
+            if idx == parent:
+                if node.is_joint is True:
+                    obj.matrix_world = matrix_gltf_to_blender(pynode.transform)
+                    return
+                else:
+                    obj.matrix_world = matrix_gltf_to_blender(pynode.transform)
+                    return
+
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_pbrMetallicRoughness.py b/io_scene_gltf2/blender/imp/gltf2_blender_pbrMetallicRoughness.py
new file mode 100755
index 0000000000000000000000000000000000000000..cf1f48c17728d3d1ac9d7350faa4b8c1f007182e
--- /dev/null
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_pbrMetallicRoughness.py
@@ -0,0 +1,347 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+from .gltf2_blender_texture import BlenderTextureInfo
+
+
+class BlenderPbr():
+    """Blender Pbr."""
+    def __new__(cls, *args, **kwargs):
+        raise RuntimeError("%s should not be instantiated" % cls)
+
+    def create(gltf, pypbr, mat_name, vertex_color):
+        """Pbr creation."""
+        engine = bpy.context.scene.render.engine
+        if engine in ['CYCLES', 'BLENDER_EEVEE']:
+            BlenderPbr.create_nodetree(gltf, pypbr, mat_name, vertex_color)
+
+    def create_nodetree(gltf, pypbr, mat_name, vertex_color):
+        """Nodetree creation."""
+        material = bpy.data.materials[mat_name]
+        material.use_nodes = True
+        node_tree = material.node_tree
+
+        # If there is no diffuse texture, but only a color, wihtout
+        # vertex_color, we set this color in viewport color
+        if pypbr.color_type == gltf.SIMPLE and not vertex_color:
+            material.diffuse_color = pypbr.base_color_factor[:3]
+
+        # delete all nodes except output
+        for node in list(node_tree.nodes):
+            if not node.type == 'OUTPUT_MATERIAL':
+                node_tree.nodes.remove(node)
+
+        output_node = node_tree.nodes[0]
+        output_node.location = 1250, 0
+
+        # create PBR node
+        principled = node_tree.nodes.new('ShaderNodeBsdfPrincipled')
+        principled.location = 0, 0
+
+        if pypbr.color_type == gltf.SIMPLE:
+
+            if not vertex_color:
+
+                # change input values
+                principled.inputs[0].default_value = pypbr.base_color_factor
+                # TODO : currently set metallic & specular in same way
+                principled.inputs[5].default_value = pypbr.metallic_factor
+                principled.inputs[7].default_value = pypbr.roughness_factor
+
+            else:
+                # Create attribute node to get COLOR_0 data
+                attribute_node = node_tree.nodes.new('ShaderNodeAttribute')
+                attribute_node.attribute_name = 'COLOR_0'
+                attribute_node.location = -500, 0
+
+                # TODO : currently set metallic & specular in same way
+                principled.inputs[5].default_value = pypbr.metallic_factor
+                principled.inputs[7].default_value = pypbr.roughness_factor
+
+                # links
+                rgb_node = node_tree.nodes.new('ShaderNodeMixRGB')
+                rgb_node.blend_type = 'MULTIPLY'
+                rgb_node.inputs['Fac'].default_value = 1.0
+                rgb_node.inputs['Color1'].default_value = pypbr.base_color_factor
+                node_tree.links.new(rgb_node.inputs['Color2'], attribute_node.outputs[0])
+                node_tree.links.new(principled.inputs[0], rgb_node.outputs[0])
+
+        elif pypbr.color_type == gltf.TEXTURE_FACTOR:
+
+            # TODO alpha ?
+            if vertex_color:
+                # TODO tree locations
+                # Create attribute / separate / math nodes
+                attribute_node = node_tree.nodes.new('ShaderNodeAttribute')
+                attribute_node.attribute_name = 'COLOR_0'
+
+                separate_vertex_color = node_tree.nodes.new('ShaderNodeSeparateRGB')
+                math_vc_R = node_tree.nodes.new('ShaderNodeMath')
+                math_vc_R.operation = 'MULTIPLY'
+
+                math_vc_G = node_tree.nodes.new('ShaderNodeMath')
+                math_vc_G.operation = 'MULTIPLY'
+
+                math_vc_B = node_tree.nodes.new('ShaderNodeMath')
+                math_vc_B.operation = 'MULTIPLY'
+
+            BlenderTextureInfo.create(gltf, pypbr.base_color_texture.index)
+
+            # create UV Map / Mapping / Texture nodes / separate & math and combine
+            text_node = node_tree.nodes.new('ShaderNodeTexImage')
+            text_node.image = bpy.data.images[gltf.data.images[
+                gltf.data.textures[pypbr.base_color_texture.index].source
+            ].blender_image_name]
+            text_node.label = 'BASE COLOR'
+            text_node.location = -1000, 500
+
+            combine = node_tree.nodes.new('ShaderNodeCombineRGB')
+            combine.location = -250, 500
+
+            math_R = node_tree.nodes.new('ShaderNodeMath')
+            math_R.location = -500, 750
+            math_R.operation = 'MULTIPLY'
+            math_R.inputs[1].default_value = pypbr.base_color_factor[0]
+
+            math_G = node_tree.nodes.new('ShaderNodeMath')
+            math_G.location = -500, 500
+            math_G.operation = 'MULTIPLY'
+            math_G.inputs[1].default_value = pypbr.base_color_factor[1]
+
+            math_B = node_tree.nodes.new('ShaderNodeMath')
+            math_B.location = -500, 250
+            math_B.operation = 'MULTIPLY'
+            math_B.inputs[1].default_value = pypbr.base_color_factor[2]
+
+            separate = node_tree.nodes.new('ShaderNodeSeparateRGB')
+            separate.location = -750, 500
+
+            mapping = node_tree.nodes.new('ShaderNodeMapping')
+            mapping.location = -1500, 500
+
+            uvmap = node_tree.nodes.new('ShaderNodeUVMap')
+            uvmap.location = -2000, 500
+            if pypbr.base_color_texture.tex_coord is not None:
+                uvmap["gltf2_texcoord"] = pypbr.base_color_texture.tex_coord  # Set custom flag to retrieve TexCoord
+            else:
+                uvmap["gltf2_texcoord"] = 0  # TODO set in pre_compute instead of here
+            # UV Map will be set after object/UVMap creation
+
+            # Create links
+            if vertex_color:
+                node_tree.links.new(separate_vertex_color.inputs[0], attribute_node.outputs[0])
+                node_tree.links.new(math_vc_R.inputs[1], separate_vertex_color.outputs[0])
+                node_tree.links.new(math_vc_G.inputs[1], separate_vertex_color.outputs[1])
+                node_tree.links.new(math_vc_B.inputs[1], separate_vertex_color.outputs[2])
+                node_tree.links.new(math_vc_R.inputs[0], math_R.outputs[0])
+                node_tree.links.new(math_vc_G.inputs[0], math_G.outputs[0])
+                node_tree.links.new(math_vc_B.inputs[0], math_B.outputs[0])
+                node_tree.links.new(combine.inputs[0], math_vc_R.outputs[0])
+                node_tree.links.new(combine.inputs[1], math_vc_G.outputs[0])
+                node_tree.links.new(combine.inputs[2], math_vc_B.outputs[0])
+
+            else:
+                node_tree.links.new(combine.inputs[0], math_R.outputs[0])
+                node_tree.links.new(combine.inputs[1], math_G.outputs[0])
+                node_tree.links.new(combine.inputs[2], math_B.outputs[0])
+
+            # Common for both mode (non vertex color / vertex color)
+            node_tree.links.new(math_R.inputs[0], separate.outputs[0])
+            node_tree.links.new(math_G.inputs[0], separate.outputs[1])
+            node_tree.links.new(math_B.inputs[0], separate.outputs[2])
+
+            node_tree.links.new(mapping.inputs[0], uvmap.outputs[0])
+            node_tree.links.new(text_node.inputs[0], mapping.outputs[0])
+            node_tree.links.new(separate.inputs[0], text_node.outputs[0])
+
+            node_tree.links.new(principled.inputs[0], combine.outputs[0])
+
+        elif pypbr.color_type == gltf.TEXTURE:
+
+            BlenderTextureInfo.create(gltf, pypbr.base_color_texture.index)
+
+            # TODO alpha ?
+            if vertex_color:
+                # Create attribute / separate / math nodes
+                attribute_node = node_tree.nodes.new('ShaderNodeAttribute')
+                attribute_node.attribute_name = 'COLOR_0'
+                attribute_node.location = -2000, 250
+
+                separate_vertex_color = node_tree.nodes.new('ShaderNodeSeparateRGB')
+                separate_vertex_color.location = -1500, 250
+
+                math_vc_R = node_tree.nodes.new('ShaderNodeMath')
+                math_vc_R.operation = 'MULTIPLY'
+                math_vc_R.location = -1000, 750
+
+                math_vc_G = node_tree.nodes.new('ShaderNodeMath')
+                math_vc_G.operation = 'MULTIPLY'
+                math_vc_G.location = -1000, 500
+
+                math_vc_B = node_tree.nodes.new('ShaderNodeMath')
+                math_vc_B.operation = 'MULTIPLY'
+                math_vc_B.location = -1000, 250
+
+                combine = node_tree.nodes.new('ShaderNodeCombineRGB')
+                combine.location = -500, 500
+
+                separate = node_tree.nodes.new('ShaderNodeSeparateRGB')
+                separate.location = -1500, 500
+
+            # create UV Map / Mapping / Texture nodes / separate & math and combine
+            text_node = node_tree.nodes.new('ShaderNodeTexImage')
+            text_node.image = bpy.data.images[gltf.data.images[
+                gltf.data.textures[pypbr.base_color_texture.index].source
+            ].blender_image_name]
+            text_node.label = 'BASE COLOR'
+            if vertex_color:
+                text_node.location = -2000, 500
+            else:
+                text_node.location = -500, 500
+
+            mapping = node_tree.nodes.new('ShaderNodeMapping')
+            if vertex_color:
+                mapping.location = -2500, 500
+            else:
+                mapping.location = -1500, 500
+
+            uvmap = node_tree.nodes.new('ShaderNodeUVMap')
+            if vertex_color:
+                uvmap.location = -3000, 500
+            else:
+                uvmap.location = -2000, 500
+            if pypbr.base_color_texture.tex_coord is not None:
+                uvmap["gltf2_texcoord"] = pypbr.base_color_texture.tex_coord  # Set custom flag to retrieve TexCoord
+            else:
+                uvmap["gltf2_texcoord"] = 0  # TODO set in pre_compute instead of here
+            # UV Map will be set after object/UVMap creation
+
+            # Create links
+            if vertex_color:
+                node_tree.links.new(separate_vertex_color.inputs[0], attribute_node.outputs[0])
+
+                node_tree.links.new(math_vc_R.inputs[1], separate_vertex_color.outputs[0])
+                node_tree.links.new(math_vc_G.inputs[1], separate_vertex_color.outputs[1])
+                node_tree.links.new(math_vc_B.inputs[1], separate_vertex_color.outputs[2])
+
+                node_tree.links.new(combine.inputs[0], math_vc_R.outputs[0])
+                node_tree.links.new(combine.inputs[1], math_vc_G.outputs[0])
+                node_tree.links.new(combine.inputs[2], math_vc_B.outputs[0])
+
+                node_tree.links.new(separate.inputs[0], text_node.outputs[0])
+
+                node_tree.links.new(principled.inputs[0], combine.outputs[0])
+
+                node_tree.links.new(math_vc_R.inputs[0], separate.outputs[0])
+                node_tree.links.new(math_vc_G.inputs[0], separate.outputs[1])
+                node_tree.links.new(math_vc_B.inputs[0], separate.outputs[2])
+
+            else:
+                node_tree.links.new(principled.inputs[0], text_node.outputs[0])
+
+            # Common for both mode (non vertex color / vertex color)
+
+            node_tree.links.new(mapping.inputs[0], uvmap.outputs[0])
+            node_tree.links.new(text_node.inputs[0], mapping.outputs[0])
+
+        # Says metallic, but it means metallic & Roughness values
+        if pypbr.metallic_type == gltf.SIMPLE:
+            principled.inputs[4].default_value = pypbr.metallic_factor
+            principled.inputs[7].default_value = pypbr.roughness_factor
+
+        elif pypbr.metallic_type == gltf.TEXTURE:
+            BlenderTextureInfo.create(gltf, pypbr.metallic_roughness_texture.index)
+            metallic_text = node_tree.nodes.new('ShaderNodeTexImage')
+            metallic_text.image = bpy.data.images[gltf.data.images[
+                gltf.data.textures[pypbr.metallic_roughness_texture.index].source
+            ].blender_image_name]
+            metallic_text.color_space = 'NONE'
+            metallic_text.label = 'METALLIC ROUGHNESS'
+            metallic_text.location = -500, 0
+
+            metallic_separate = node_tree.nodes.new('ShaderNodeSeparateRGB')
+            metallic_separate.location = -250, 0
+
+            metallic_mapping = node_tree.nodes.new('ShaderNodeMapping')
+            metallic_mapping.location = -1000, 0
+
+            metallic_uvmap = node_tree.nodes.new('ShaderNodeUVMap')
+            metallic_uvmap.location = -1500, 0
+            if pypbr.metallic_roughness_texture.tex_coord is not None:
+                # Set custom flag to retrieve TexCoord
+                metallic_uvmap["gltf2_texcoord"] = pypbr.metallic_roughness_texture.tex_coord
+            else:
+                metallic_uvmap["gltf2_texcoord"] = 0  # TODO set in pre_compute instead of here
+
+            # links
+            node_tree.links.new(metallic_separate.inputs[0], metallic_text.outputs[0])
+            node_tree.links.new(principled.inputs[4], metallic_separate.outputs[2])  # metallic
+            node_tree.links.new(principled.inputs[7], metallic_separate.outputs[1])  # Roughness
+
+            node_tree.links.new(metallic_mapping.inputs[0], metallic_uvmap.outputs[0])
+            node_tree.links.new(metallic_text.inputs[0], metallic_mapping.outputs[0])
+
+        elif pypbr.metallic_type == gltf.TEXTURE_FACTOR:
+
+            BlenderTextureInfo.create(gltf, pypbr.metallic_roughness_texture.index)
+            metallic_text = node_tree.nodes.new('ShaderNodeTexImage')
+            metallic_text.image = bpy.data.images[gltf.data.images[
+                gltf.data.textures[pypbr.metallic_roughness_texture.index].source
+            ].blender_image_name]
+            metallic_text.color_space = 'NONE'
+            metallic_text.label = 'METALLIC ROUGHNESS'
+            metallic_text.location = -1000, 0
+
+            metallic_separate = node_tree.nodes.new('ShaderNodeSeparateRGB')
+            metallic_separate.location = -500, 0
+
+            metallic_math = node_tree.nodes.new('ShaderNodeMath')
+            metallic_math.operation = 'MULTIPLY'
+            metallic_math.inputs[1].default_value = pypbr.metallic_factor
+            metallic_math.location = -250, 100
+
+            roughness_math = node_tree.nodes.new('ShaderNodeMath')
+            roughness_math.operation = 'MULTIPLY'
+            roughness_math.inputs[1].default_value = pypbr.roughness_factor
+            roughness_math.location = -250, -100
+
+            metallic_mapping = node_tree.nodes.new('ShaderNodeMapping')
+            metallic_mapping.location = -1000, 0
+
+            metallic_uvmap = node_tree.nodes.new('ShaderNodeUVMap')
+            metallic_uvmap.location = -1500, 0
+            if pypbr.metallic_roughness_texture.tex_coord is not None:
+                # Set custom flag to retrieve TexCoord
+                metallic_uvmap["gltf2_texcoord"] = pypbr.metallic_roughness_texture.tex_coord
+            else:
+                metallic_uvmap["gltf2_texcoord"] = 0  # TODO set in pre_compute instead of here
+
+            # links
+            node_tree.links.new(metallic_separate.inputs[0], metallic_text.outputs[0])
+
+            # metallic
+            node_tree.links.new(metallic_math.inputs[0], metallic_separate.outputs[2])
+            node_tree.links.new(principled.inputs[4], metallic_math.outputs[0])
+
+            # roughness
+            node_tree.links.new(roughness_math.inputs[0], metallic_separate.outputs[1])
+            node_tree.links.new(principled.inputs[7], roughness_math.outputs[0])
+
+            node_tree.links.new(metallic_mapping.inputs[0], metallic_uvmap.outputs[0])
+            node_tree.links.new(metallic_text.inputs[0], metallic_mapping.outputs[0])
+
+        # link node to output
+        node_tree.links.new(output_node.inputs[0], principled.outputs[0])
+
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_primitive.py b/io_scene_gltf2/blender/imp/gltf2_blender_primitive.py
new file mode 100755
index 0000000000000000000000000000000000000000..bc12f437aa7268697c89a5771285a43edac28ae8
--- /dev/null
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_primitive.py
@@ -0,0 +1,170 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+from mathutils import Vector
+
+from .gltf2_blender_material import BlenderMaterial
+from ..com.gltf2_blender_conversion import loc_gltf_to_blender
+from ...io.imp.gltf2_io_binary import BinaryData
+
+
+class BlenderPrimitive():
+    """Blender Primitive."""
+    def __new__(cls, *args, **kwargs):
+        raise RuntimeError("%s should not be instantiated" % cls)
+
+    @staticmethod
+    def create(gltf, pyprimitive, verts, edges, faces):
+        """Primitive creation."""
+        pyprimitive.blender_texcoord = {}
+
+        # TODO mode of primitive 4 for now.
+        current_length = len(verts)
+        pos = BinaryData.get_data_from_accessor(gltf, pyprimitive.attributes['POSITION'])
+        if pyprimitive.indices is not None:
+            indices = BinaryData.get_data_from_accessor(gltf, pyprimitive.indices)
+        else:
+            indices = []
+            indices_ = range(0, len(pos))
+            for i in indices_:
+                indices.append((i,))
+
+        prim_verts = [loc_gltf_to_blender(vert) for vert in pos]
+        pyprimitive.vertices_length = len(prim_verts)
+        verts.extend(prim_verts)
+        prim_faces = []
+        for i in range(0, len(indices), 3):
+            vals = indices[i:i + 3]
+            new_vals = []
+            for y in vals:
+                new_vals.append(y[0] + current_length)
+            prim_faces.append(tuple(new_vals))
+        faces.extend(prim_faces)
+        pyprimitive.faces_length = len(prim_faces)
+
+        # manage material of primitive
+        if pyprimitive.material is not None:
+
+            # Create Blender material
+            # TODO, a same material can have difference COLOR_0 multiplicator
+            if gltf.data.materials[pyprimitive.material].blender_material is None:
+                vertex_color = None
+                if 'COLOR_0' in pyprimitive.attributes.keys():
+                    vertex_color = pyprimitive.attributes['COLOR_0']
+                BlenderMaterial.create(gltf, pyprimitive.material, vertex_color)
+
+        return verts, edges, faces
+
+    def set_normals(gltf, pyprimitive, mesh, offset):
+        """Set Normal."""
+        if 'NORMAL' in pyprimitive.attributes.keys():
+            normal_data = BinaryData.get_data_from_accessor(gltf, pyprimitive.attributes['NORMAL'])
+            for poly in mesh.polygons:
+                if gltf.import_settings['import_shading'] == "NORMALS":
+                    calc_norm_vertices = []
+                    for loop_idx in range(poly.loop_start, poly.loop_start + poly.loop_total):
+                        vert_idx = mesh.loops[loop_idx].vertex_index
+                        if vert_idx in range(offset, offset + pyprimitive.vertices_length):
+                            cpt_vert = vert_idx - offset
+                            mesh.vertices[vert_idx].normal = normal_data[cpt_vert]
+                            calc_norm_vertices.append(vert_idx)
+
+                        if len(calc_norm_vertices) == 3:
+                            # Calcul normal
+                            vert0 = mesh.vertices[calc_norm_vertices[0]].co
+                            vert1 = mesh.vertices[calc_norm_vertices[1]].co
+                            vert2 = mesh.vertices[calc_norm_vertices[2]].co
+                            calc_normal = (vert1 - vert0).cross(vert2 - vert0).normalized()
+
+                            # Compare normal to vertex normal
+                            for i in calc_norm_vertices:
+                                cpt_vert = vert_idx - offset
+                                vec = Vector(
+                                    (normal_data[cpt_vert][0], normal_data[cpt_vert][1], normal_data[cpt_vert][2])
+                                )
+                                if not calc_normal.dot(vec) > 0.9999999:
+                                    poly.use_smooth = True
+                                    break
+                elif gltf.import_settings['import_shading'] == "FLAT":
+                    poly.use_smooth = False
+                elif gltf.import_settings['import_shading'] == "SMOOTH":
+                    poly.use_smooth = True
+                else:
+                    pass  # Should not happend
+
+        offset = offset + pyprimitive.vertices_length
+        return offset
+
+    def set_UV(gltf, pyprimitive, obj, mesh, offset):
+        """Set UV Map."""
+        for texcoord in [attr for attr in pyprimitive.attributes.keys() if attr[:9] == "TEXCOORD_"]:
+            if texcoord not in mesh.uv_layers:
+                mesh.uv_layers.new(name=texcoord)
+                pyprimitive.blender_texcoord[int(texcoord[9:])] = texcoord
+
+            texcoord_data = BinaryData.get_data_from_accessor(gltf, pyprimitive.attributes[texcoord])
+            for poly in mesh.polygons:
+                for loop_idx in range(poly.loop_start, poly.loop_start + poly.loop_total):
+                    vert_idx = mesh.loops[loop_idx].vertex_index
+                    if vert_idx in range(offset, offset + pyprimitive.vertices_length):
+                        obj.data.uv_layers[texcoord].data[loop_idx].uv = \
+                            Vector((texcoord_data[vert_idx - offset][0], 1 - texcoord_data[vert_idx - offset][1]))
+
+        offset = offset + pyprimitive.vertices_length
+        return offset
+
+    def set_UV_in_mat(gltf, pyprimitive, obj):
+        """After nodetree creation, set UVMap in nodes."""
+        if pyprimitive.material is None:
+            return
+        if gltf.data.materials[pyprimitive.material].extensions \
+                and "KHR_materials_pbrSpecularGlossiness" in \
+                    gltf.data.materials[pyprimitive.material].extensions.keys():
+            if pyprimitive.material is not None \
+                    and gltf.data.materials[pyprimitive.material].extensions[
+                        'KHR_materials_pbrSpecularGlossiness'
+                    ]['diffuse_type'] in [gltf.TEXTURE, gltf.TEXTURE_FACTOR]:
+                BlenderMaterial.set_uvmap(gltf, pyprimitive.material, pyprimitive, obj)
+            else:
+                if pyprimitive.material is not None \
+                        and gltf.data.materials[pyprimitive.material].extensions[
+                            'KHR_materials_pbrSpecularGlossiness'
+                        ]['specgloss_type'] in [gltf.TEXTURE, gltf.TEXTURE_FACTOR]:
+                    BlenderMaterial.set_uvmap(gltf, pyprimitive.material, pyprimitive, obj)
+
+        else:
+            if pyprimitive.material is not None \
+                    and gltf.data.materials[pyprimitive.material].pbr_metallic_roughness.color_type in \
+                    [gltf.TEXTURE, gltf.TEXTURE_FACTOR]:
+                BlenderMaterial.set_uvmap(gltf, pyprimitive.material, pyprimitive, obj)
+            else:
+                if pyprimitive.material is not None \
+                        and gltf.data.materials[pyprimitive.material].pbr_metallic_roughness.metallic_type in \
+                        [gltf.TEXTURE, gltf.TEXTURE_FACTOR]:
+                    BlenderMaterial.set_uvmap(gltf, pyprimitive.material, pyprimitive, obj)
+
+    def assign_material(gltf, pyprimitive, obj, bm, offset, cpt_index_mat):
+        """Assign material to faces of primitives."""
+        if pyprimitive.material is not None:
+            obj.data.materials.append(bpy.data.materials[gltf.data.materials[pyprimitive.material].blender_material])
+            for vert in bm.verts:
+                if vert.index in range(offset, offset + pyprimitive.vertices_length):
+                    for loop in vert.link_loops:
+                        face = loop.face.index
+                        bm.faces[face].material_index = cpt_index_mat
+            cpt_index_mat += 1
+        offset = offset + pyprimitive.vertices_length
+        return offset, cpt_index_mat
+
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_scene.py b/io_scene_gltf2/blender/imp/gltf2_blender_scene.py
new file mode 100755
index 0000000000000000000000000000000000000000..c5e89659b0217a3d6c71320372e63e3490259454
--- /dev/null
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_scene.py
@@ -0,0 +1,94 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bpy
+from math import sqrt
+from mathutils import Quaternion
+from .gltf2_blender_node import BlenderNode
+from .gltf2_blender_skin import BlenderSkin
+from .gltf2_blender_animation import BlenderAnimation
+
+
+class BlenderScene():
+    """Blender Scene."""
+    def __new__(cls, *args, **kwargs):
+        raise RuntimeError("%s should not be instantiated" % cls)
+
+    @staticmethod
+    def create(gltf, scene_idx):
+        """Scene creation."""
+        pyscene = gltf.data.scenes[scene_idx]
+
+    # Create a new scene only if not already exists in .blend file
+    # TODO : put in current scene instead ?
+        if pyscene.name not in [scene.name for scene in bpy.data.scenes]:
+            # TODO: There is a bug in 2.8 alpha that break CLEAR_KEEP_TRANSFORM
+            # if we are creating a new scene
+            scene = bpy.context.scene
+            if bpy.app.version < (2, 80, 0):
+                scene.render.engine = "CYCLES"
+            else:
+                scene.render.engine = "BLENDER_EEVEE"
+
+            gltf.blender_scene = scene.name
+        else:
+            gltf.blender_scene = pyscene.name
+
+        # Create Yup2Zup empty
+        obj_rotation = bpy.data.objects.new("Yup2Zup", None)
+        obj_rotation.rotation_mode = 'QUATERNION'
+        obj_rotation.rotation_quaternion = Quaternion((sqrt(2) / 2, sqrt(2) / 2, 0.0, 0.0))
+
+        bpy.data.scenes[gltf.blender_scene].collection.objects.link(obj_rotation)
+
+        if pyscene.nodes is not None:
+            for node_idx in pyscene.nodes:
+                BlenderNode.create(gltf, node_idx, None)  # None => No parent
+
+        # Now that all mesh / bones are created, create vertex groups on mesh
+        if gltf.data.skins:
+            for skin_id, skin in enumerate(gltf.data.skins):
+                if hasattr(skin, "node_ids"):
+                    BlenderSkin.create_vertex_groups(gltf, skin_id)
+
+            for skin_id, skin in enumerate(gltf.data.skins):
+                if hasattr(skin, "node_ids"):
+                    BlenderSkin.assign_vertex_groups(gltf, skin_id)
+
+            for skin_id, skin in enumerate(gltf.data.skins):
+                if hasattr(skin, "node_ids"):
+                    BlenderSkin.create_armature_modifiers(gltf, skin_id)
+
+        if gltf.data.animations:
+            for anim_idx, anim in enumerate(gltf.data.animations):
+                for node_idx in pyscene.nodes:
+                    BlenderAnimation.anim(gltf, anim_idx, node_idx)
+
+        # Parent root node to rotation object
+        if pyscene.nodes is not None:
+            for node_idx in pyscene.nodes:
+                bpy.data.objects[gltf.data.nodes[node_idx].blender_object].parent = obj_rotation
+
+
+            for node_idx in pyscene.nodes:
+                for obj_ in bpy.context.scene.objects:
+                    obj_.select_set(False)
+                bpy.data.objects[gltf.data.nodes[node_idx].blender_object].select_set(True)
+                bpy.context.view_layer.objects.active = bpy.data.objects[gltf.data.nodes[node_idx].blender_object]
+
+                bpy.ops.object.parent_clear(type='CLEAR_KEEP_TRANSFORM')
+
+                bpy.context.scene.collection.objects.unlink(obj_rotation)
+                bpy.data.objects.remove(obj_rotation)
+
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_skin.py b/io_scene_gltf2/blender/imp/gltf2_blender_skin.py
new file mode 100755
index 0000000000000000000000000000000000000000..db0e50f95266360c971b646bc8a89cf5a8277c6d
--- /dev/null
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_skin.py
@@ -0,0 +1,209 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import bpy
+from mathutils import Vector, Matrix
+from ..com.gltf2_blender_conversion import matrix_gltf_to_blender, scale_to_matrix
+from ...io.imp.gltf2_io_binary import BinaryData
+
+
+class BlenderSkin():
+    """Blender Skinning / Armature."""
+    def __new__(cls, *args, **kwargs):
+        raise RuntimeError("%s should not be instantiated" % cls)
+
+    @staticmethod
+    def create_armature(gltf, skin_id, parent):
+        """Armature creation."""
+        pyskin = gltf.data.skins[skin_id]
+
+        if pyskin.name is not None:
+            name = pyskin.name
+        else:
+            name = "Armature_" + str(skin_id)
+
+        armature = bpy.data.armatures.new(name)
+        obj = bpy.data.objects.new(name, armature)
+        bpy.data.scenes[gltf.blender_scene].collection.objects.link(obj)
+        pyskin.blender_armature_name = obj.name
+        if parent is not None:
+            obj.parent = bpy.data.objects[gltf.data.nodes[parent].blender_object]
+
+    @staticmethod
+    def set_bone_transforms(gltf, skin_id, bone, node_id, parent):
+        """Set bone transformations."""
+        pyskin = gltf.data.skins[skin_id]
+        pynode = gltf.data.nodes[node_id]
+
+        obj = bpy.data.objects[pyskin.blender_armature_name]
+
+        # Set bone bind_pose by inverting bindpose matrix
+        if node_id in pyskin.joints:
+            index_in_skel = pyskin.joints.index(node_id)
+            inverse_bind_matrices = BinaryData.get_data_from_accessor(gltf, pyskin.inverse_bind_matrices)
+            # Needed to keep scale in matrix, as bone.matrix seems to drop it
+            if index_in_skel < len(inverse_bind_matrices):
+                pynode.blender_bone_matrix = matrix_gltf_to_blender(
+                    inverse_bind_matrices[index_in_skel]
+                ).inverted()
+                bone.matrix = pynode.blender_bone_matrix
+            else:
+                gltf.log.error("Error with inverseBindMatrix for skin " + pyskin)
+        else:
+            print('No invBindMatrix for bone ' + str(node_id))
+            pynode.blender_bone_matrix = Matrix()
+
+        # Parent the bone
+        if parent is not None and hasattr(gltf.data.nodes[parent], "blender_bone_name"):
+            bone.parent = obj.data.edit_bones[gltf.data.nodes[parent].blender_bone_name]  # TODO if in another scene
+
+        # Switch to Pose mode
+        bpy.ops.object.mode_set(mode="POSE")
+        obj.data.pose_position = 'POSE'
+
+        # Set posebone location/rotation/scale (in armature space)
+        # location is actual bone location minus it's original (bind) location
+        bind_location = Matrix.Translation(pynode.blender_bone_matrix.to_translation())
+        bind_rotation = pynode.blender_bone_matrix.to_quaternion()
+        bind_scale = scale_to_matrix(pynode.blender_bone_matrix.to_scale())
+
+        location, rotation, scale = matrix_gltf_to_blender(pynode.transform).decompose()
+        if parent is not None and hasattr(gltf.data.nodes[parent], "blender_bone_matrix"):
+            parent_mat = gltf.data.nodes[parent].blender_bone_matrix
+
+            # Get armature space location (bindpose + pose)
+            # Then, remove original bind location from armspace location, and bind rotation
+            final_location = (bind_location.inverted() @ parent_mat @ Matrix.Translation(location)).to_translation()
+            obj.pose.bones[pynode.blender_bone_name].location = \
+                bind_rotation.inverted().to_matrix().to_4x4() @ final_location
+
+            # Do the same for rotation
+            obj.pose.bones[pynode.blender_bone_name].rotation_quaternion = \
+                (bind_rotation.to_matrix().to_4x4().inverted() @ parent_mat @
+                    rotation.to_matrix().to_4x4()).to_quaternion()
+            obj.pose.bones[pynode.blender_bone_name].scale = \
+                (bind_scale.inverted() @ parent_mat @ scale_to_matrix(scale)).to_scale()
+
+        else:
+            obj.pose.bones[pynode.blender_bone_name].location = bind_location.inverted() @ location
+            obj.pose.bones[pynode.blender_bone_name].rotation_quaternion = bind_rotation.inverted() @ rotation
+            obj.pose.bones[pynode.blender_bone_name].scale = bind_scale.inverted() @ scale
+
+    @staticmethod
+    def create_bone(gltf, skin_id, node_id, parent):
+        """Bone creation."""
+        pyskin = gltf.data.skins[skin_id]
+        pynode = gltf.data.nodes[node_id]
+
+        scene = bpy.data.scenes[gltf.blender_scene]
+        obj = bpy.data.objects[pyskin.blender_armature_name]
+
+        bpy.context.window.scene = scene
+        bpy.context.view_layer.objects.active = obj
+        bpy.ops.object.mode_set(mode="EDIT")
+
+        if pynode.name:
+            name = pynode.name
+        else:
+            name = "Bone_" + str(node_id)
+
+        bone = obj.data.edit_bones.new(name)
+        pynode.blender_bone_name = bone.name
+        pynode.blender_armature_name = pyskin.blender_armature_name
+        bone.tail = Vector((0.0, 1.0, 0.0))  # Needed to keep bone alive
+
+        # set bind and pose transforms
+        BlenderSkin.set_bone_transforms(gltf, skin_id, bone, node_id, parent)
+        bpy.ops.object.mode_set(mode="OBJECT")
+
+    @staticmethod
+    def create_vertex_groups(gltf, skin_id):
+        """Vertex Group creation."""
+        pyskin = gltf.data.skins[skin_id]
+        for node_id in pyskin.node_ids:
+            obj = bpy.data.objects[gltf.data.nodes[node_id].blender_object]
+            for bone in pyskin.joints:
+                obj.vertex_groups.new(name=gltf.data.nodes[bone].blender_bone_name)
+
+    @staticmethod
+    def assign_vertex_groups(gltf, skin_id):
+        """Assign vertex groups to vertices."""
+        pyskin = gltf.data.skins[skin_id]
+        for node_id in pyskin.node_ids:
+            node = gltf.data.nodes[node_id]
+            obj = bpy.data.objects[node.blender_object]
+
+            offset = 0
+            for prim in gltf.data.meshes[node.mesh].primitives:
+                idx_already_done = {}
+
+                if 'JOINTS_0' in prim.attributes.keys() and 'WEIGHTS_0' in prim.attributes.keys():
+                    joint_ = BinaryData.get_data_from_accessor(gltf, prim.attributes['JOINTS_0'])
+                    weight_ = BinaryData.get_data_from_accessor(gltf, prim.attributes['WEIGHTS_0'])
+
+                    for poly in obj.data.polygons:
+                        for loop_idx in range(poly.loop_start, poly.loop_start + poly.loop_total):
+                            vert_idx = obj.data.loops[loop_idx].vertex_index
+
+                            if vert_idx in idx_already_done.keys():
+                                continue
+                            idx_already_done[vert_idx] = True
+
+                            if vert_idx in range(offset, offset + prim.vertices_length):
+
+                                tab_index = vert_idx - offset
+                                cpt = 0
+                                for joint_idx in joint_[tab_index]:
+                                    weight_val = weight_[tab_index][cpt]
+                                    if weight_val != 0.0:   # It can be a problem to assign weights of 0
+                                                            # for bone index 0, if there is always 4 indices in joint_
+                                                            # tuple
+                                        group = obj.vertex_groups[gltf.data.nodes[
+                                            pyskin.joints[joint_idx]
+                                        ].blender_bone_name]
+                                        group.add([vert_idx], weight_val, 'REPLACE')
+                                    cpt += 1
+                else:
+                    gltf.log.error("No Skinning ?????")  # TODO
+
+                offset = offset + prim.vertices_length
+
+    @staticmethod
+    def create_armature_modifiers(gltf, skin_id):
+        """Create Armature modifier."""
+        pyskin = gltf.data.skins[skin_id]
+
+        if pyskin.blender_armature_name is None:
+            # TODO seems something is wrong
+            # For example, some joints are in skin 0, and are in another skin too
+            # Not sure this is glTF compliant, will check it
+            return
+
+        for node_id in pyskin.node_ids:
+            node = gltf.data.nodes[node_id]
+            obj = bpy.data.objects[node.blender_object]
+
+            for obj_sel in bpy.context.scene.objects:
+                obj_sel.select_set(False)
+            obj.select_set(True)
+            bpy.context.view_layer.objects.active = obj
+
+            # bpy.ops.object.parent_clear(type='CLEAR_KEEP_TRANSFORM')
+            # Reparent skinned mesh to it's armature to avoid breaking
+            # skinning with interleaved transforms
+            obj.parent = bpy.data.objects[pyskin.blender_armature_name]
+            arma = obj.modifiers.new(name="Armature", type="ARMATURE")
+            arma.object = bpy.data.objects[pyskin.blender_armature_name]
+
diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_texture.py b/io_scene_gltf2/blender/imp/gltf2_blender_texture.py
new file mode 100755
index 0000000000000000000000000000000000000000..c8983d9c26ca23ca8f67720db583f75457372ba3
--- /dev/null
+++ b/io_scene_gltf2/blender/imp/gltf2_blender_texture.py
@@ -0,0 +1,39 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from .gltf2_blender_image import BlenderImage
+
+
+class BlenderTextureInfo():
+    """Blender Texture info."""
+    def __new__(cls, *args, **kwargs):
+        raise RuntimeError("%s should not be instantiated" % cls)
+
+    @staticmethod
+    def create(gltf, pytextureinfo_idx):
+        """Create Texture info."""
+        BlenderTexture.create(gltf, pytextureinfo_idx)
+
+
+class BlenderTexture():
+    """Blender Texture."""
+    def __new__(cls, *args, **kwargs):
+        raise RuntimeError("%s should not be instantiated" % cls)
+
+    @staticmethod
+    def create(gltf, pytexture_idx):
+        """Create texture."""
+        pytexture = gltf.data.textures[pytexture_idx]
+        BlenderImage.create(gltf, pytexture.source)
+
diff --git a/io_scene_gltf2/io/__init__.py b/io_scene_gltf2/io/__init__.py
new file mode 100755
index 0000000000000000000000000000000000000000..10973240e68dba9d149027b10536b1a7beeabdbc
--- /dev/null
+++ b/io_scene_gltf2/io/__init__.py
@@ -0,0 +1,16 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from .imp import *
+
diff --git a/io_scene_gltf2/io/com/gltf2_io.py b/io_scene_gltf2/io/com/gltf2_io.py
new file mode 100755
index 0000000000000000000000000000000000000000..1332adf60b777309f415bdd53e1a769f1c8ff857
--- /dev/null
+++ b/io_scene_gltf2/io/com/gltf2_io.py
@@ -0,0 +1,1200 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# NOTE: Generated from latest glTF 2.0 JSON Scheme specs using quicktype (https://github.com/quicktype/quicktype)
+# command used:
+# quicktype --src glTF.schema.json --src-lang schema -t gltf --lang python --python-version 3.5
+
+# TODO: add __slots__ to all classes by extending the generator
+
+# TODO: REMOVE traceback import
+import sys
+import traceback
+
+from io_scene_gltf2.io.com import gltf2_io_debug
+
+
+def from_int(x):
+    assert isinstance(x, int) and not isinstance(x, bool)
+    return x
+
+
+def from_none(x):
+    assert x is None
+    return x
+
+
+def from_union(fs, x):
+    tracebacks = []
+    for f in fs:
+        try:
+            return f(x)
+        except AssertionError:
+            _, _, tb = sys.exc_info()
+            tracebacks.append(tb)
+    for tb in tracebacks:
+        traceback.print_tb(tb)  # Fixed format
+        tb_info = traceback.extract_tb(tb)
+        for tbi in tb_info:
+            filename, line, func, text = tbi
+            gltf2_io_debug.print_console('ERROR', 'An error occurred on line {} in statement {}'.format(line, text))
+    assert False
+
+
+def from_dict(f, x):
+    assert isinstance(x, dict)
+    return {k: f(v) for (k, v) in x.items()}
+
+
+def to_class(c, x):
+    assert isinstance(x, c)
+    return x.to_dict()
+
+
+def from_list(f, x):
+    assert isinstance(x, list)
+    return [f(y) for y in x]
+
+
+def from_float(x):
+    assert isinstance(x, (float, int)) and not isinstance(x, bool)
+    return float(x)
+
+
+def from_str(x):
+    assert isinstance(x, str)
+    return x
+
+
+def from_bool(x):
+    assert isinstance(x, bool)
+    return x
+
+
+def to_float(x):
+    assert isinstance(x, float)
+    return x
+
+
+class AccessorSparseIndices:
+    """Index array of size `count` that points to those accessor attributes that deviate from
+    their initialization value. Indices must strictly increase.
+
+    Indices of those attributes that deviate from their initialization value.
+    """
+
+    def __init__(self, buffer_view, byte_offset, component_type, extensions, extras):
+        self.buffer_view = buffer_view
+        self.byte_offset = byte_offset
+        self.component_type = component_type
+        self.extensions = extensions
+        self.extras = extras
+
+    @staticmethod
+    def from_dict(obj):
+        assert isinstance(obj, dict)
+        buffer_view = from_int(obj.get("bufferView"))
+        byte_offset = from_union([from_int, from_none], obj.get("byteOffset"))
+        component_type = from_int(obj.get("componentType"))
+        extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                obj.get("extensions"))
+        extras = obj.get("extras")
+        return AccessorSparseIndices(buffer_view, byte_offset, component_type, extensions, extras)
+
+    def to_dict(self):
+        result = {}
+        result["bufferView"] = from_int(self.buffer_view)
+        result["byteOffset"] = from_union([from_int, from_none], self.byte_offset)
+        result["componentType"] = from_int(self.component_type)
+        result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                          self.extensions)
+        result["extras"] = self.extras
+        return result
+
+
+class AccessorSparseValues:
+    """Array of size `count` times number of components, storing the displaced accessor
+    attributes pointed by `indices`. Substituted values must have the same `componentType`
+    and number of components as the base accessor.
+
+    Array of size `accessor.sparse.count` times number of components storing the displaced
+    accessor attributes pointed by `accessor.sparse.indices`.
+    """
+
+    def __init__(self, buffer_view, byte_offset, extensions, extras):
+        self.buffer_view = buffer_view
+        self.byte_offset = byte_offset
+        self.extensions = extensions
+        self.extras = extras
+
+    @staticmethod
+    def from_dict(obj):
+        assert isinstance(obj, dict)
+        buffer_view = from_int(obj.get("bufferView"))
+        byte_offset = from_union([from_int, from_none], obj.get("byteOffset"))
+        extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                obj.get("extensions"))
+        extras = obj.get("extras")
+        return AccessorSparseValues(buffer_view, byte_offset, extensions, extras)
+
+    def to_dict(self):
+        result = {}
+        result["bufferView"] = from_int(self.buffer_view)
+        result["byteOffset"] = from_union([from_int, from_none], self.byte_offset)
+        result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                          self.extensions)
+        result["extras"] = self.extras
+        return result
+
+
+class AccessorSparse:
+    """Sparse storage of attributes that deviate from their initialization value."""
+
+    def __init__(self, count, extensions, extras, indices, values):
+        self.count = count
+        self.extensions = extensions
+        self.extras = extras
+        self.indices = indices
+        self.values = values
+
+    @staticmethod
+    def from_dict(obj):
+        assert isinstance(obj, dict)
+        count = from_int(obj.get("count"))
+        extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                obj.get("extensions"))
+        extras = obj.get("extras")
+        indices = AccessorSparseIndices.from_dict(obj.get("indices"))
+        values = AccessorSparseValues.from_dict(obj.get("values"))
+        return AccessorSparse(count, extensions, extras, indices, values)
+
+    def to_dict(self):
+        result = {}
+        result["count"] = from_int(self.count)
+        result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                          self.extensions)
+        result["extras"] = self.extras
+        result["indices"] = to_class(AccessorSparseIndices, self.indices)
+        result["values"] = to_class(AccessorSparseValues, self.values)
+        return result
+
+
+class Accessor:
+    """A typed view into a bufferView.  A bufferView contains raw binary data.  An accessor
+    provides a typed view into a bufferView or a subset of a bufferView similar to how
+    WebGL's `vertexAttribPointer()` defines an attribute in a buffer.
+    """
+
+    def __init__(self, buffer_view, byte_offset, component_type, count, extensions, extras, max, min, name, normalized,
+                 sparse, type):
+        self.buffer_view = buffer_view
+        self.byte_offset = byte_offset
+        self.component_type = component_type
+        self.count = count
+        self.extensions = extensions
+        self.extras = extras
+        self.max = max
+        self.min = min
+        self.name = name
+        self.normalized = normalized
+        self.sparse = sparse
+        self.type = type
+
+    @staticmethod
+    def from_dict(obj):
+        assert isinstance(obj, dict)
+        buffer_view = from_union([from_int, from_none], obj.get("bufferView"))
+        byte_offset = from_union([from_int, from_none], obj.get("byteOffset"))
+        component_type = from_int(obj.get("componentType"))
+        count = from_int(obj.get("count"))
+        extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                obj.get("extensions"))
+        extras = obj.get("extras")
+        max = from_union([lambda x: from_list(from_float, x), from_none], obj.get("max"))
+        min = from_union([lambda x: from_list(from_float, x), from_none], obj.get("min"))
+        name = from_union([from_str, from_none], obj.get("name"))
+        normalized = from_union([from_bool, from_none], obj.get("normalized"))
+        sparse = from_union([AccessorSparse.from_dict, from_none], obj.get("sparse"))
+        type = from_str(obj.get("type"))
+        return Accessor(buffer_view, byte_offset, component_type, count, extensions, extras, max, min, name, normalized,
+                        sparse, type)
+
+    def to_dict(self):
+        result = {}
+        result["bufferView"] = from_union([from_int, from_none], self.buffer_view)
+        result["byteOffset"] = from_union([from_int, from_none], self.byte_offset)
+        result["componentType"] = from_int(self.component_type)
+        result["count"] = from_int(self.count)
+        result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                          self.extensions)
+        result["extras"] = self.extras
+        result["max"] = from_union([lambda x: from_list(to_float, x), from_none], self.max)
+        result["min"] = from_union([lambda x: from_list(to_float, x), from_none], self.min)
+        result["name"] = from_union([from_str, from_none], self.name)
+        result["normalized"] = from_union([from_bool, from_none], self.normalized)
+        result["sparse"] = from_union([lambda x: to_class(AccessorSparse, x), from_none], self.sparse)
+        result["type"] = from_str(self.type)
+        return result
+
+
+class AnimationChannelTarget:
+    """The index of the node and TRS property to target.
+
+    The index of the node and TRS property that an animation channel targets.
+    """
+
+    def __init__(self, extensions, extras, node, path):
+        self.extensions = extensions
+        self.extras = extras
+        self.node = node
+        self.path = path
+
+    @staticmethod
+    def from_dict(obj):
+        assert isinstance(obj, dict)
+        extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                obj.get("extensions"))
+        extras = obj.get("extras")
+        node = from_union([from_int, from_none], obj.get("node"))
+        path = from_str(obj.get("path"))
+        return AnimationChannelTarget(extensions, extras, node, path)
+
+    def to_dict(self):
+        result = {}
+        result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                          self.extensions)
+        result["extras"] = self.extras
+        result["node"] = from_union([from_int, from_none], self.node)
+        result["path"] = from_str(self.path)
+        return result
+
+
+class AnimationChannel:
+    """Targets an animation's sampler at a node's property."""
+
+    def __init__(self, extensions, extras, sampler, target):
+        self.extensions = extensions
+        self.extras = extras
+        self.sampler = sampler
+        self.target = target
+
+    @staticmethod
+    def from_dict(obj):
+        assert isinstance(obj, dict)
+        extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                obj.get("extensions"))
+        extras = obj.get("extras")
+        sampler = from_int(obj.get("sampler"))
+        target = AnimationChannelTarget.from_dict(obj.get("target"))
+        return AnimationChannel(extensions, extras, sampler, target)
+
+    def to_dict(self):
+        result = {}
+        result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                          self.extensions)
+        result["extras"] = self.extras
+        result["sampler"] = from_int(self.sampler)
+        result["target"] = to_class(AnimationChannelTarget, self.target)
+        return result
+
+
+class AnimationSampler:
+    """Combines input and output accessors with an interpolation algorithm to define a keyframe
+    graph (but not its target).
+    """
+
+    def __init__(self, extensions, extras, input, interpolation, output):
+        self.extensions = extensions
+        self.extras = extras
+        self.input = input
+        self.interpolation = interpolation
+        self.output = output
+
+    @staticmethod
+    def from_dict(obj):
+        assert isinstance(obj, dict)
+        extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                obj.get("extensions"))
+        extras = obj.get("extras")
+        input = from_int(obj.get("input"))
+        interpolation = from_union([from_str, from_none], obj.get("interpolation"))
+        output = from_int(obj.get("output"))
+        return AnimationSampler(extensions, extras, input, interpolation, output)
+
+    def to_dict(self):
+        result = {}
+        result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                          self.extensions)
+        result["extras"] = self.extras
+        result["input"] = from_int(self.input)
+        result["interpolation"] = from_union([from_str, from_none], self.interpolation)
+        result["output"] = from_int(self.output)
+        return result
+
+
+class Animation:
+    """A keyframe animation."""
+
+    def __init__(self, channels, extensions, extras, name, samplers):
+        self.channels = channels
+        self.extensions = extensions
+        self.extras = extras
+        self.name = name
+        self.samplers = samplers
+
+    @staticmethod
+    def from_dict(obj):
+        assert isinstance(obj, dict)
+        channels = from_list(AnimationChannel.from_dict, obj.get("channels"))
+        extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                obj.get("extensions"))
+        extras = obj.get("extras")
+        name = from_union([from_str, from_none], obj.get("name"))
+        samplers = from_list(AnimationSampler.from_dict, obj.get("samplers"))
+        return Animation(channels, extensions, extras, name, samplers)
+
+    def to_dict(self):
+        result = {}
+        result["channels"] = from_list(lambda x: to_class(AnimationChannel, x), self.channels)
+        result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                          self.extensions)
+        result["extras"] = self.extras
+        result["name"] = from_union([from_str, from_none], self.name)
+        result["samplers"] = from_list(lambda x: to_class(AnimationSampler, x), self.samplers)
+        return result
+
+
+class Asset:
+    """Metadata about the glTF asset."""
+
+    def __init__(self, copyright, extensions, extras, generator, min_version, version):
+        self.copyright = copyright
+        self.extensions = extensions
+        self.extras = extras
+        self.generator = generator
+        self.min_version = min_version
+        self.version = version
+
+    @staticmethod
+    def from_dict(obj):
+        assert isinstance(obj, dict)
+        copyright = from_union([from_str, from_none], obj.get("copyright"))
+        extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                obj.get("extensions"))
+        extras = obj.get("extras")
+        generator = from_union([from_str, from_none], obj.get("generator"))
+        min_version = from_union([from_str, from_none], obj.get("minVersion"))
+        version = from_str(obj.get("version"))
+        return Asset(copyright, extensions, extras, generator, min_version, version)
+
+    def to_dict(self):
+        result = {}
+        result["copyright"] = from_union([from_str, from_none], self.copyright)
+        result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                          self.extensions)
+        result["extras"] = self.extras
+        result["generator"] = from_union([from_str, from_none], self.generator)
+        result["minVersion"] = from_union([from_str, from_none], self.min_version)
+        result["version"] = from_str(self.version)
+        return result
+
+
+class BufferView:
+    """A view into a buffer generally representing a subset of the buffer."""
+
+    def __init__(self, buffer, byte_length, byte_offset, byte_stride, extensions, extras, name, target):
+        self.buffer = buffer
+        self.byte_length = byte_length
+        self.byte_offset = byte_offset
+        self.byte_stride = byte_stride
+        self.extensions = extensions
+        self.extras = extras
+        self.name = name
+        self.target = target
+
+    @staticmethod
+    def from_dict(obj):
+        assert isinstance(obj, dict)
+        buffer = from_int(obj.get("buffer"))
+        byte_length = from_int(obj.get("byteLength"))
+        byte_offset = from_union([from_int, from_none], obj.get("byteOffset"))
+        byte_stride = from_union([from_int, from_none], obj.get("byteStride"))
+        extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                obj.get("extensions"))
+        extras = obj.get("extras")
+        name = from_union([from_str, from_none], obj.get("name"))
+        target = from_union([from_int, from_none], obj.get("target"))
+        return BufferView(buffer, byte_length, byte_offset, byte_stride, extensions, extras, name, target)
+
+    def to_dict(self):
+        result = {}
+        result["buffer"] = from_int(self.buffer)
+        result["byteLength"] = from_int(self.byte_length)
+        result["byteOffset"] = from_union([from_int, from_none], self.byte_offset)
+        result["byteStride"] = from_union([from_int, from_none], self.byte_stride)
+        result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                          self.extensions)
+        result["extras"] = self.extras
+        result["name"] = from_union([from_str, from_none], self.name)
+        result["target"] = from_union([from_int, from_none], self.target)
+        return result
+
+
+class Buffer:
+    """A buffer points to binary geometry, animation, or skins."""
+
+    def __init__(self, byte_length, extensions, extras, name, uri):
+        self.byte_length = byte_length
+        self.extensions = extensions
+        self.extras = extras
+        self.name = name
+        self.uri = uri
+
+    @staticmethod
+    def from_dict(obj):
+        assert isinstance(obj, dict)
+        byte_length = from_int(obj.get("byteLength"))
+        extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                obj.get("extensions"))
+        extras = obj.get("extras")
+        name = from_union([from_str, from_none], obj.get("name"))
+        uri = from_union([from_str, from_none], obj.get("uri"))
+        return Buffer(byte_length, extensions, extras, name, uri)
+
+    def to_dict(self):
+        result = {}
+        result["byteLength"] = from_int(self.byte_length)
+        result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                          self.extensions)
+        result["extras"] = self.extras
+        result["name"] = from_union([from_str, from_none], self.name)
+        result["uri"] = from_union([from_str, from_none], self.uri)
+        return result
+
+
+class CameraOrthographic:
+    """An orthographic camera containing properties to create an orthographic projection matrix."""
+
+    def __init__(self, extensions, extras, xmag, ymag, zfar, znear):
+        self.extensions = extensions
+        self.extras = extras
+        self.xmag = xmag
+        self.ymag = ymag
+        self.zfar = zfar
+        self.znear = znear
+
+    @staticmethod
+    def from_dict(obj):
+        assert isinstance(obj, dict)
+        extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                obj.get("extensions"))
+        extras = obj.get("extras")
+        xmag = from_float(obj.get("xmag"))
+        ymag = from_float(obj.get("ymag"))
+        zfar = from_float(obj.get("zfar"))
+        znear = from_float(obj.get("znear"))
+        return CameraOrthographic(extensions, extras, xmag, ymag, zfar, znear)
+
+    def to_dict(self):
+        result = {}
+        result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                          self.extensions)
+        result["extras"] = self.extras
+        result["xmag"] = to_float(self.xmag)
+        result["ymag"] = to_float(self.ymag)
+        result["zfar"] = to_float(self.zfar)
+        result["znear"] = to_float(self.znear)
+        return result
+
+
+class CameraPerspective:
+    """A perspective camera containing properties to create a perspective projection matrix."""
+
+    def __init__(self, aspect_ratio, extensions, extras, yfov, zfar, znear):
+        self.aspect_ratio = aspect_ratio
+        self.extensions = extensions
+        self.extras = extras
+        self.yfov = yfov
+        self.zfar = zfar
+        self.znear = znear
+
+    @staticmethod
+    def from_dict(obj):
+        assert isinstance(obj, dict)
+        aspect_ratio = from_union([from_float, from_none], obj.get("aspectRatio"))
+        extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                obj.get("extensions"))
+        extras = obj.get("extras")
+        yfov = from_float(obj.get("yfov"))
+        zfar = from_union([from_float, from_none], obj.get("zfar"))
+        znear = from_float(obj.get("znear"))
+        return CameraPerspective(aspect_ratio, extensions, extras, yfov, zfar, znear)
+
+    def to_dict(self):
+        result = {}
+        result["aspectRatio"] = from_union([to_float, from_none], self.aspect_ratio)
+        result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                          self.extensions)
+        result["extras"] = self.extras
+        result["yfov"] = to_float(self.yfov)
+        result["zfar"] = from_union([to_float, from_none], self.zfar)
+        result["znear"] = to_float(self.znear)
+        return result
+
+
+class Camera:
+    """A camera's projection.  A node can reference a camera to apply a transform to place the
+    camera in the scene.
+    """
+
+    def __init__(self, extensions, extras, name, orthographic, perspective, type):
+        self.extensions = extensions
+        self.extras = extras
+        self.name = name
+        self.orthographic = orthographic
+        self.perspective = perspective
+        self.type = type
+
+    @staticmethod
+    def from_dict(obj):
+        assert isinstance(obj, dict)
+        extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                obj.get("extensions"))
+        extras = obj.get("extras")
+        name = from_union([from_str, from_none], obj.get("name"))
+        orthographic = from_union([CameraOrthographic.from_dict, from_none], obj.get("orthographic"))
+        perspective = from_union([CameraPerspective.from_dict, from_none], obj.get("perspective"))
+        type = from_str(obj.get("type"))
+        return Camera(extensions, extras, name, orthographic, perspective, type)
+
+    def to_dict(self):
+        result = {}
+        result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                          self.extensions)
+        result["extras"] = self.extras
+        result["name"] = from_union([from_str, from_none], self.name)
+        result["orthographic"] = from_union([lambda x: to_class(CameraOrthographic, x), from_none], self.orthographic)
+        result["perspective"] = from_union([lambda x: to_class(CameraPerspective, x), from_none], self.perspective)
+        result["type"] = from_str(self.type)
+        return result
+
+
+class Image:
+    """Image data used to create a texture. Image can be referenced by URI or `bufferView`
+    index. `mimeType` is required in the latter case.
+    """
+
+    def __init__(self, buffer_view, extensions, extras, mime_type, name, uri):
+        self.buffer_view = buffer_view
+        self.extensions = extensions
+        self.extras = extras
+        self.mime_type = mime_type
+        self.name = name
+        self.uri = uri
+
+    @staticmethod
+    def from_dict(obj):
+        assert isinstance(obj, dict)
+        buffer_view = from_union([from_int, from_none], obj.get("bufferView"))
+        extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                obj.get("extensions"))
+        extras = obj.get("extras")
+        mime_type = from_union([from_str, from_none], obj.get("mimeType"))
+        name = from_union([from_str, from_none], obj.get("name"))
+        uri = from_union([from_str, from_none], obj.get("uri"))
+        return Image(buffer_view, extensions, extras, mime_type, name, uri)
+
+    def to_dict(self):
+        result = {}
+        result["bufferView"] = from_union([from_int, from_none], self.buffer_view)
+        result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                          self.extensions)
+        result["extras"] = self.extras
+        result["mimeType"] = from_union([from_str, from_none], self.mime_type)
+        result["name"] = from_union([from_str, from_none], self.name)
+        result["uri"] = from_union([from_str, from_none], self.uri)
+        return result
+
+
+class TextureInfo:
+    """The emissive map texture.
+
+    The base color texture.
+
+    The metallic-roughness texture.
+
+    Reference to a texture.
+    """
+
+    def __init__(self, extensions, extras, index, tex_coord):
+        self.extensions = extensions
+        self.extras = extras
+        self.index = index
+        self.tex_coord = tex_coord
+
+    @staticmethod
+    def from_dict(obj):
+        assert isinstance(obj, dict)
+        extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                obj.get("extensions"))
+        extras = obj.get("extras")
+        index = from_int(obj.get("index"))
+        tex_coord = from_union([from_int, from_none], obj.get("texCoord"))
+        return TextureInfo(extensions, extras, index, tex_coord)
+
+    def to_dict(self):
+        result = {}
+        result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                          self.extensions)
+        result["extras"] = self.extras
+        result["index"] = from_int(self.index)
+        result["texCoord"] = from_union([from_int, from_none], self.tex_coord)
+        return result
+
+
+class MaterialNormalTextureInfoClass:
+    """The normal map texture.
+
+    Reference to a texture.
+    """
+
+    def __init__(self, extensions, extras, index, scale, tex_coord):
+        self.extensions = extensions
+        self.extras = extras
+        self.index = index
+        self.scale = scale
+        self.tex_coord = tex_coord
+
+    @staticmethod
+    def from_dict(obj):
+        assert isinstance(obj, dict)
+        extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                obj.get("extensions"))
+        extras = obj.get("extras")
+        index = from_int(obj.get("index"))
+        scale = from_union([from_float, from_none], obj.get("scale"))
+        tex_coord = from_union([from_int, from_none], obj.get("texCoord"))
+        return MaterialNormalTextureInfoClass(extensions, extras, index, scale, tex_coord)
+
+    def to_dict(self):
+        result = {}
+        result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                          self.extensions)
+        result["extras"] = self.extras
+        result["index"] = from_int(self.index)
+        result["scale"] = from_union([to_float, from_none], self.scale)
+        result["texCoord"] = from_union([from_int, from_none], self.tex_coord)
+        return result
+
+
+class MaterialOcclusionTextureInfoClass:
+    """The occlusion map texture.
+
+    Reference to a texture.
+    """
+
+    def __init__(self, extensions, extras, index, strength, tex_coord):
+        self.extensions = extensions
+        self.extras = extras
+        self.index = index
+        self.strength = strength
+        self.tex_coord = tex_coord
+
+    @staticmethod
+    def from_dict(obj):
+        assert isinstance(obj, dict)
+        extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                obj.get("extensions"))
+        extras = obj.get("extras")
+        index = from_int(obj.get("index"))
+        strength = from_union([from_float, from_none], obj.get("strength"))
+        tex_coord = from_union([from_int, from_none], obj.get("texCoord"))
+        return MaterialOcclusionTextureInfoClass(extensions, extras, index, strength, tex_coord)
+
+    def to_dict(self):
+        result = {}
+        result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                          self.extensions)
+        result["extras"] = self.extras
+        result["index"] = from_int(self.index)
+        result["strength"] = from_union([to_float, from_none], self.strength)
+        result["texCoord"] = from_union([from_int, from_none], self.tex_coord)
+        return result
+
+
+class MaterialPBRMetallicRoughness:
+    """A set of parameter values that are used to define the metallic-roughness material model
+    from Physically-Based Rendering (PBR) methodology. When not specified, all the default
+    values of `pbrMetallicRoughness` apply.
+
+    A set of parameter values that are used to define the metallic-roughness material model
+    from Physically-Based Rendering (PBR) methodology.
+    """
+
+    def __init__(self, base_color_factor, base_color_texture, extensions, extras, metallic_factor,
+                 metallic_roughness_texture, roughness_factor):
+        self.base_color_factor = base_color_factor
+        self.base_color_texture = base_color_texture
+        self.extensions = extensions
+        self.extras = extras
+        self.metallic_factor = metallic_factor
+        self.metallic_roughness_texture = metallic_roughness_texture
+        self.roughness_factor = roughness_factor
+
+    @staticmethod
+    def from_dict(obj):
+        assert isinstance(obj, dict)
+        base_color_factor = from_union([lambda x: from_list(from_float, x), from_none], obj.get("baseColorFactor"))
+        base_color_texture = from_union([TextureInfo.from_dict, from_none], obj.get("baseColorTexture"))
+        extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                obj.get("extensions"))
+        extras = obj.get("extras")
+        metallic_factor = from_union([from_float, from_none], obj.get("metallicFactor"))
+        metallic_roughness_texture = from_union([TextureInfo.from_dict, from_none], obj.get("metallicRoughnessTexture"))
+        roughness_factor = from_union([from_float, from_none], obj.get("roughnessFactor"))
+        return MaterialPBRMetallicRoughness(base_color_factor, base_color_texture, extensions, extras, metallic_factor,
+                                            metallic_roughness_texture, roughness_factor)
+
+    def to_dict(self):
+        result = {}
+        result["baseColorFactor"] = from_union([lambda x: from_list(to_float, x), from_none], self.base_color_factor)
+        result["baseColorTexture"] = from_union([lambda x: to_class(TextureInfo, x), from_none],
+                                                self.base_color_texture)
+        result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                          self.extensions)
+        result["extras"] = self.extras
+        result["metallicFactor"] = from_union([to_float, from_none], self.metallic_factor)
+        result["metallicRoughnessTexture"] = from_union([lambda x: to_class(TextureInfo, x), from_none],
+                                                        self.metallic_roughness_texture)
+        result["roughnessFactor"] = from_union([to_float, from_none], self.roughness_factor)
+        return result
+
+
+class Material:
+    """The material appearance of a primitive."""
+
+    def __init__(self, alpha_cutoff, alpha_mode, double_sided, emissive_factor, emissive_texture, extensions, extras,
+                 name, normal_texture, occlusion_texture, pbr_metallic_roughness):
+        self.alpha_cutoff = alpha_cutoff
+        self.alpha_mode = alpha_mode
+        self.double_sided = double_sided
+        self.emissive_factor = emissive_factor
+        self.emissive_texture = emissive_texture
+        self.extensions = extensions
+        self.extras = extras
+        self.name = name
+        self.normal_texture = normal_texture
+        self.occlusion_texture = occlusion_texture
+        self.pbr_metallic_roughness = pbr_metallic_roughness
+
+    @staticmethod
+    def from_dict(obj):
+        assert isinstance(obj, dict)
+        alpha_cutoff = from_union([from_float, from_none], obj.get("alphaCutoff"))
+        alpha_mode = from_union([from_str, from_none], obj.get("alphaMode"))
+        double_sided = from_union([from_bool, from_none], obj.get("doubleSided"))
+        emissive_factor = from_union([lambda x: from_list(from_float, x), from_none], obj.get("emissiveFactor"))
+        emissive_texture = from_union([TextureInfo.from_dict, from_none], obj.get("emissiveTexture"))
+        extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                obj.get("extensions"))
+        extras = obj.get("extras")
+        name = from_union([from_str, from_none], obj.get("name"))
+        normal_texture = from_union([MaterialNormalTextureInfoClass.from_dict, from_none], obj.get("normalTexture"))
+        occlusion_texture = from_union([MaterialOcclusionTextureInfoClass.from_dict, from_none],
+                                       obj.get("occlusionTexture"))
+        pbr_metallic_roughness = from_union([MaterialPBRMetallicRoughness.from_dict, from_none],
+                                            obj.get("pbrMetallicRoughness"))
+        return Material(alpha_cutoff, alpha_mode, double_sided, emissive_factor, emissive_texture, extensions, extras,
+                        name, normal_texture, occlusion_texture, pbr_metallic_roughness)
+
+    def to_dict(self):
+        result = {}
+        result["alphaCutoff"] = from_union([to_float, from_none], self.alpha_cutoff)
+        result["alphaMode"] = from_union([from_str, from_none], self.alpha_mode)
+        result["doubleSided"] = from_union([from_bool, from_none], self.double_sided)
+        result["emissiveFactor"] = from_union([lambda x: from_list(to_float, x), from_none], self.emissive_factor)
+        result["emissiveTexture"] = from_union([lambda x: to_class(TextureInfo, x), from_none], self.emissive_texture)
+        result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                          self.extensions)
+        result["extras"] = self.extras
+        result["name"] = from_union([from_str, from_none], self.name)
+        result["normalTexture"] = from_union([lambda x: to_class(MaterialNormalTextureInfoClass, x), from_none],
+                                             self.normal_texture)
+        result["occlusionTexture"] = from_union([lambda x: to_class(MaterialOcclusionTextureInfoClass, x), from_none],
+                                                self.occlusion_texture)
+        result["pbrMetallicRoughness"] = from_union([lambda x: to_class(MaterialPBRMetallicRoughness, x), from_none],
+                                                    self.pbr_metallic_roughness)
+        return result
+
+
+class MeshPrimitive:
+    """Geometry to be rendered with the given material."""
+
+    def __init__(self, attributes, extensions, extras, indices, material, mode, targets):
+        self.attributes = attributes
+        self.extensions = extensions
+        self.extras = extras
+        self.indices = indices
+        self.material = material
+        self.mode = mode
+        self.targets = targets
+
+    @staticmethod
+    def from_dict(obj):
+        assert isinstance(obj, dict)
+        attributes = from_dict(from_int, obj.get("attributes"))
+        extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                obj.get("extensions"))
+        extras = obj.get("extras")
+        indices = from_union([from_int, from_none], obj.get("indices"))
+        material = from_union([from_int, from_none], obj.get("material"))
+        mode = from_union([from_int, from_none], obj.get("mode"))
+        targets = from_union([lambda x: from_list(lambda x: from_dict(from_int, x), x), from_none], obj.get("targets"))
+        return MeshPrimitive(attributes, extensions, extras, indices, material, mode, targets)
+
+    def to_dict(self):
+        result = {}
+        result["attributes"] = from_dict(from_int, self.attributes)
+        result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                          self.extensions)
+        result["extras"] = self.extras
+        result["indices"] = from_union([from_int, from_none], self.indices)
+        result["material"] = from_union([from_int, from_none], self.material)
+        result["mode"] = from_union([from_int, from_none], self.mode)
+        result["targets"] = from_union([lambda x: from_list(lambda x: from_dict(from_int, x), x), from_none],
+                                       self.targets)
+        return result
+
+
+class Mesh:
+    """A set of primitives to be rendered.  A node can contain one mesh.  A node's transform
+    places the mesh in the scene.
+    """
+
+    def __init__(self, extensions, extras, name, primitives, weights):
+        self.extensions = extensions
+        self.extras = extras
+        self.name = name
+        self.primitives = primitives
+        self.weights = weights
+
+    @staticmethod
+    def from_dict(obj):
+        assert isinstance(obj, dict)
+        extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                obj.get("extensions"))
+        extras = obj.get("extras")
+        name = from_union([from_str, from_none], obj.get("name"))
+        primitives = from_list(MeshPrimitive.from_dict, obj.get("primitives"))
+        weights = from_union([lambda x: from_list(from_float, x), from_none], obj.get("weights"))
+        return Mesh(extensions, extras, name, primitives, weights)
+
+    def to_dict(self):
+        result = {}
+        result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                          self.extensions)
+        result["extras"] = self.extras
+        result["name"] = from_union([from_str, from_none], self.name)
+        result["primitives"] = from_list(lambda x: to_class(MeshPrimitive, x), self.primitives)
+        result["weights"] = from_union([lambda x: from_list(to_float, x), from_none], self.weights)
+        return result
+
+
+class Node:
+    """A node in the node hierarchy.  When the node contains `skin`, all `mesh.primitives` must
+    contain `JOINTS_0` and `WEIGHTS_0` attributes.  A node can have either a `matrix` or any
+    combination of `translation`/`rotation`/`scale` (TRS) properties. TRS properties are
+    converted to matrices and postmultiplied in the `T * R * S` order to compose the
+    transformation matrix; first the scale is applied to the vertices, then the rotation, and
+    then the translation. If none are provided, the transform is the identity. When a node is
+    targeted for animation (referenced by an animation.channel.target), only TRS properties
+    may be present; `matrix` will not be present.
+    """
+
+    def __init__(self, camera, children, extensions, extras, matrix, mesh, name, rotation, scale, skin, translation,
+                 weights):
+        self.camera = camera
+        self.children = children
+        self.extensions = extensions
+        self.extras = extras
+        self.matrix = matrix
+        self.mesh = mesh
+        self.name = name
+        self.rotation = rotation
+        self.scale = scale
+        self.skin = skin
+        self.translation = translation
+        self.weights = weights
+
+    @staticmethod
+    def from_dict(obj):
+        assert isinstance(obj, dict)
+        camera = from_union([from_int, from_none], obj.get("camera"))
+        children = from_union([lambda x: from_list(from_int, x), from_none], obj.get("children"))
+        extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                obj.get("extensions"))
+        extras = obj.get("extras")
+        matrix = from_union([lambda x: from_list(from_float, x), from_none], obj.get("matrix"))
+        mesh = from_union([from_int, from_none], obj.get("mesh"))
+        name = from_union([from_str, from_none], obj.get("name"))
+        rotation = from_union([lambda x: from_list(from_float, x), from_none], obj.get("rotation"))
+        scale = from_union([lambda x: from_list(from_float, x), from_none], obj.get("scale"))
+        skin = from_union([from_int, from_none], obj.get("skin"))
+        translation = from_union([lambda x: from_list(from_float, x), from_none], obj.get("translation"))
+        weights = from_union([lambda x: from_list(from_float, x), from_none], obj.get("weights"))
+        return Node(camera, children, extensions, extras, matrix, mesh, name, rotation, scale, skin, translation,
+                    weights)
+
+    def to_dict(self):
+        result = {}
+        result["camera"] = from_union([from_int, from_none], self.camera)
+        result["children"] = from_union([lambda x: from_list(from_int, x), from_none], self.children)
+        result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                          self.extensions)
+        result["extras"] = self.extras
+        result["matrix"] = from_union([lambda x: from_list(to_float, x), from_none], self.matrix)
+        result["mesh"] = from_union([from_int, from_none], self.mesh)
+        result["name"] = from_union([from_str, from_none], self.name)
+        result["rotation"] = from_union([lambda x: from_list(to_float, x), from_none], self.rotation)
+        result["scale"] = from_union([lambda x: from_list(to_float, x), from_none], self.scale)
+        result["skin"] = from_union([from_int, from_none], self.skin)
+        result["translation"] = from_union([lambda x: from_list(to_float, x), from_none], self.translation)
+        result["weights"] = from_union([lambda x: from_list(to_float, x), from_none], self.weights)
+        return result
+
+
+class Sampler:
+    """Texture sampler properties for filtering and wrapping modes."""
+
+    def __init__(self, extensions, extras, mag_filter, min_filter, name, wrap_s, wrap_t):
+        self.extensions = extensions
+        self.extras = extras
+        self.mag_filter = mag_filter
+        self.min_filter = min_filter
+        self.name = name
+        self.wrap_s = wrap_s
+        self.wrap_t = wrap_t
+
+    @staticmethod
+    def from_dict(obj):
+        assert isinstance(obj, dict)
+        extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                obj.get("extensions"))
+        extras = obj.get("extras")
+        mag_filter = from_union([from_int, from_none], obj.get("magFilter"))
+        min_filter = from_union([from_int, from_none], obj.get("minFilter"))
+        name = from_union([from_str, from_none], obj.get("name"))
+        wrap_s = from_union([from_int, from_none], obj.get("wrapS"))
+        wrap_t = from_union([from_int, from_none], obj.get("wrapT"))
+        return Sampler(extensions, extras, mag_filter, min_filter, name, wrap_s, wrap_t)
+
+    def to_dict(self):
+        result = {}
+        result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                          self.extensions)
+        result["extras"] = self.extras
+        result["magFilter"] = from_union([from_int, from_none], self.mag_filter)
+        result["minFilter"] = from_union([from_int, from_none], self.min_filter)
+        result["name"] = from_union([from_str, from_none], self.name)
+        result["wrapS"] = from_union([from_int, from_none], self.wrap_s)
+        result["wrapT"] = from_union([from_int, from_none], self.wrap_t)
+        return result
+
+
+class Scene:
+    """The root nodes of a scene."""
+
+    def __init__(self, extensions, extras, name, nodes):
+        self.extensions = extensions
+        self.extras = extras
+        self.name = name
+        self.nodes = nodes
+
+    @staticmethod
+    def from_dict(obj):
+        assert isinstance(obj, dict)
+        extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                obj.get("extensions"))
+        extras = obj.get("extras")
+        name = from_union([from_str, from_none], obj.get("name"))
+        nodes = from_union([lambda x: from_list(from_int, x), from_none], obj.get("nodes"))
+        return Scene(extensions, extras, name, nodes)
+
+    def to_dict(self):
+        result = {}
+        result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                          self.extensions)
+        result["extras"] = self.extras
+        result["name"] = from_union([from_str, from_none], self.name)
+        result["nodes"] = from_union([lambda x: from_list(from_int, x), from_none], self.nodes)
+        return result
+
+
+class Skin:
+    """Joints and matrices defining a skin."""
+
+    def __init__(self, extensions, extras, inverse_bind_matrices, joints, name, skeleton):
+        self.extensions = extensions
+        self.extras = extras
+        self.inverse_bind_matrices = inverse_bind_matrices
+        self.joints = joints
+        self.name = name
+        self.skeleton = skeleton
+
+    @staticmethod
+    def from_dict(obj):
+        assert isinstance(obj, dict)
+        extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                obj.get("extensions"))
+        extras = obj.get("extras")
+        inverse_bind_matrices = from_union([from_int, from_none], obj.get("inverseBindMatrices"))
+        joints = from_list(from_int, obj.get("joints"))
+        name = from_union([from_str, from_none], obj.get("name"))
+        skeleton = from_union([from_int, from_none], obj.get("skeleton"))
+        return Skin(extensions, extras, inverse_bind_matrices, joints, name, skeleton)
+
+    def to_dict(self):
+        result = {}
+        result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                          self.extensions)
+        result["extras"] = self.extras
+        result["inverseBindMatrices"] = from_union([from_int, from_none], self.inverse_bind_matrices)
+        result["joints"] = from_list(from_int, self.joints)
+        result["name"] = from_union([from_str, from_none], self.name)
+        result["skeleton"] = from_union([from_int, from_none], self.skeleton)
+        return result
+
+
+class Texture:
+    """A texture and its sampler."""
+
+    def __init__(self, extensions, extras, name, sampler, source):
+        self.extensions = extensions
+        self.extras = extras
+        self.name = name
+        self.sampler = sampler
+        self.source = source
+
+    @staticmethod
+    def from_dict(obj):
+        assert isinstance(obj, dict)
+        extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                obj.get("extensions"))
+        extras = obj.get("extras")
+        name = from_union([from_str, from_none], obj.get("name"))
+        sampler = from_union([from_int, from_none], obj.get("sampler"))
+        source = from_int(obj.get("source"))
+        return Texture(extensions, extras, name, sampler, source)
+
+    def to_dict(self):
+        result = {}
+        result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                          self.extensions)
+        result["extras"] = self.extras
+        result["name"] = from_union([from_str, from_none], self.name)
+        result["sampler"] = from_union([from_int, from_none], self.sampler)
+        result["source"] = from_int(self.source)
+        return result
+
+
+class Gltf:
+    """The root object for a glTF asset."""
+
+    def __init__(self, accessors, animations, asset, buffers, buffer_views, cameras, extensions, extensions_required,
+                 extensions_used, extras, images, materials, meshes, nodes, samplers, scene, scenes, skins, textures):
+        self.accessors = accessors
+        self.animations = animations
+        self.asset = asset
+        self.buffers = buffers
+        self.buffer_views = buffer_views
+        self.cameras = cameras
+        self.extensions = extensions
+        self.extensions_required = extensions_required
+        self.extensions_used = extensions_used
+        self.extras = extras
+        self.images = images
+        self.materials = materials
+        self.meshes = meshes
+        self.nodes = nodes
+        self.samplers = samplers
+        self.scene = scene
+        self.scenes = scenes
+        self.skins = skins
+        self.textures = textures
+
+    @staticmethod
+    def from_dict(obj):
+        assert isinstance(obj, dict)
+        accessors = from_union([lambda x: from_list(Accessor.from_dict, x), from_none], obj.get("accessors"))
+        animations = from_union([lambda x: from_list(Animation.from_dict, x), from_none], obj.get("animations"))
+        asset = Asset.from_dict(obj.get("asset"))
+        buffers = from_union([lambda x: from_list(Buffer.from_dict, x), from_none], obj.get("buffers"))
+        buffer_views = from_union([lambda x: from_list(BufferView.from_dict, x), from_none], obj.get("bufferViews"))
+        cameras = from_union([lambda x: from_list(Camera.from_dict, x), from_none], obj.get("cameras"))
+        extensions = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                obj.get("extensions"))
+        extensions_required = from_union([lambda x: from_list(from_str, x), from_none], obj.get("extensionsRequired"))
+        extensions_used = from_union([lambda x: from_list(from_str, x), from_none], obj.get("extensionsUsed"))
+        extras = obj.get("extras")
+        images = from_union([lambda x: from_list(Image.from_dict, x), from_none], obj.get("images"))
+        materials = from_union([lambda x: from_list(Material.from_dict, x), from_none], obj.get("materials"))
+        meshes = from_union([lambda x: from_list(Mesh.from_dict, x), from_none], obj.get("meshes"))
+        nodes = from_union([lambda x: from_list(Node.from_dict, x), from_none], obj.get("nodes"))
+        samplers = from_union([lambda x: from_list(Sampler.from_dict, x), from_none], obj.get("samplers"))
+        scene = from_union([from_int, from_none], obj.get("scene"))
+        scenes = from_union([lambda x: from_list(Scene.from_dict, x), from_none], obj.get("scenes"))
+        skins = from_union([lambda x: from_list(Skin.from_dict, x), from_none], obj.get("skins"))
+        textures = from_union([lambda x: from_list(Texture.from_dict, x), from_none], obj.get("textures"))
+        return Gltf(accessors, animations, asset, buffers, buffer_views, cameras, extensions, extensions_required,
+                    extensions_used, extras, images, materials, meshes, nodes, samplers, scene, scenes, skins, textures)
+
+    def to_dict(self):
+        result = {}
+        result["accessors"] = from_union([lambda x: from_list(lambda x: to_class(Accessor, x), x), from_none],
+                                         self.accessors)
+        result["animations"] = from_union([lambda x: from_list(lambda x: to_class(Animation, x), x), from_none],
+                                          self.animations)
+        result["asset"] = to_class(Asset, self.asset)
+        result["buffers"] = from_union([lambda x: from_list(lambda x: to_class(Buffer, x), x), from_none], self.buffers)
+        result["bufferViews"] = from_union([lambda x: from_list(lambda x: to_class(BufferView, x), x), from_none],
+                                           self.buffer_views)
+        result["cameras"] = from_union([lambda x: from_list(lambda x: to_class(Camera, x), x), from_none], self.cameras)
+        result["extensions"] = from_union([lambda x: from_dict(lambda x: from_dict(lambda x: x, x), x), from_none],
+                                          self.extensions)
+        result["extensionsRequired"] = from_union([lambda x: from_list(from_str, x), from_none],
+                                                  self.extensions_required)
+        result["extensionsUsed"] = from_union([lambda x: from_list(from_str, x), from_none], self.extensions_used)
+        result["extras"] = self.extras
+        result["images"] = from_union([lambda x: from_list(lambda x: to_class(Image, x), x), from_none], self.images)
+        result["materials"] = from_union([lambda x: from_list(lambda x: to_class(Material, x), x), from_none],
+                                         self.materials)
+        result["meshes"] = from_union([lambda x: from_list(lambda x: to_class(Mesh, x), x), from_none], self.meshes)
+        result["nodes"] = from_union([lambda x: from_list(lambda x: to_class(Node, x), x), from_none], self.nodes)
+        result["samplers"] = from_union([lambda x: from_list(lambda x: to_class(Sampler, x), x), from_none],
+                                        self.samplers)
+        result["scene"] = from_union([from_int, from_none], self.scene)
+        result["scenes"] = from_union([lambda x: from_list(lambda x: to_class(Scene, x), x), from_none], self.scenes)
+        result["skins"] = from_union([lambda x: from_list(lambda x: to_class(Skin, x), x), from_none], self.skins)
+        result["textures"] = from_union([lambda x: from_list(lambda x: to_class(Texture, x), x), from_none],
+                                        self.textures)
+        return result
+
+
+def gltf_from_dict(s):
+    return Gltf.from_dict(s)
+
+
+def gltf_to_dict(x):
+    return to_class(Gltf, x)
+
diff --git a/io_scene_gltf2/io/com/gltf2_io_constants.py b/io_scene_gltf2/io/com/gltf2_io_constants.py
new file mode 100755
index 0000000000000000000000000000000000000000..c97908cd0e915812c94499889edc8202b15ca009
--- /dev/null
+++ b/io_scene_gltf2/io/com/gltf2_io_constants.py
@@ -0,0 +1,132 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from enum import IntEnum
+
+
+class ComponentType(IntEnum):
+    Byte = 5120
+    UnsignedByte = 5121
+    Short = 5122
+    UnsignedShort = 5123
+    UnsignedInt = 5125
+    Float = 5126
+
+    @classmethod
+    def to_type_code(cls, component_type):
+        return {
+            ComponentType.Byte: 'b',
+            ComponentType.UnsignedByte: 'B',
+            ComponentType.Short: 'h',
+            ComponentType.UnsignedShort: 'H',
+            ComponentType.UnsignedInt: 'I',
+            ComponentType.Float: 'f'
+        }[component_type]
+
+    @classmethod
+    def from_legacy_define(cls, type_define):
+        return {
+            GLTF_COMPONENT_TYPE_BYTE: ComponentType.Byte,
+            GLTF_COMPONENT_TYPE_UNSIGNED_BYTE: ComponentType.UnsignedByte,
+            GLTF_COMPONENT_TYPE_SHORT: ComponentType.Short,
+            GLTF_COMPONENT_TYPE_UNSIGNED_SHORT: ComponentType.UnsignedShort,
+            GLTF_COMPONENT_TYPE_UNSIGNED_INT: ComponentType.UnsignedInt,
+            GLTF_COMPONENT_TYPE_FLOAT: ComponentType.Float
+        }[type_define]
+
+    @classmethod
+    def get_size(cls, component_type):
+        return {
+            ComponentType.Byte: 1,
+            ComponentType.UnsignedByte: 1,
+            ComponentType.Short: 2,
+            ComponentType.UnsignedShort: 2,
+            ComponentType.UnsignedInt: 4,
+            ComponentType.Float: 4
+        }[component_type]
+
+
+class DataType:
+    Scalar = "SCALAR"
+    Vec2 = "VEC2"
+    Vec3 = "VEC3"
+    Vec4 = "VEC4"
+    Mat2 = "MAT2"
+    Mat3 = "MAT3"
+    Mat4 = "MAT4"
+
+    def __new__(cls, *args, **kwargs):
+        raise RuntimeError("{} should not be instantiated".format(cls.__name__))
+
+    @classmethod
+    def num_elements(cls, data_type):
+        return {
+            DataType.Scalar: 1,
+            DataType.Vec2: 2,
+            DataType.Vec3: 3,
+            DataType.Vec4: 4,
+            DataType.Mat2: 4,
+            DataType.Mat3: 9,
+            DataType.Mat4: 16
+        }[data_type]
+
+    @classmethod
+    def vec_type_from_num(cls, num_elems):
+        if not (0 < num_elems < 5):
+            raise ValueError("No vector type with {} elements".format(num_elems))
+        return {
+            1: DataType.Scalar,
+            2: DataType.Vec2,
+            3: DataType.Vec3,
+            4: DataType.Vec4
+        }[num_elems]
+
+    @classmethod
+    def mat_type_from_num(cls, num_elems):
+        if not (4 <= num_elems <= 16):
+            raise ValueError("No matrix type with {} elements".format(num_elems))
+        return {
+            4: DataType.Mat2,
+            9: DataType.Mat3,
+            16: DataType.Mat4
+        }[num_elems]
+
+
+#################
+# LEGACY DEFINES
+
+GLTF_VERSION = "2.0"
+
+#
+# Component Types
+#
+GLTF_COMPONENT_TYPE_BYTE = "BYTE"
+GLTF_COMPONENT_TYPE_UNSIGNED_BYTE = "UNSIGNED_BYTE"
+GLTF_COMPONENT_TYPE_SHORT = "SHORT"
+GLTF_COMPONENT_TYPE_UNSIGNED_SHORT = "UNSIGNED_SHORT"
+GLTF_COMPONENT_TYPE_UNSIGNED_INT = "UNSIGNED_INT"
+GLTF_COMPONENT_TYPE_FLOAT = "FLOAT"
+
+
+#
+# Data types
+#
+GLTF_DATA_TYPE_SCALAR = "SCALAR"
+GLTF_DATA_TYPE_VEC2 = "VEC2"
+GLTF_DATA_TYPE_VEC3 = "VEC3"
+GLTF_DATA_TYPE_VEC4 = "VEC4"
+GLTF_DATA_TYPE_MAT2 = "MAT2"
+GLTF_DATA_TYPE_MAT3 = "MAT3"
+GLTF_DATA_TYPE_MAT4 = "MAT4"
+
diff --git a/io_scene_gltf2/io/com/gltf2_io_debug.py b/io_scene_gltf2/io/com/gltf2_io_debug.py
new file mode 100755
index 0000000000000000000000000000000000000000..a7df8fed6d615147537b410927e847fb869950d0
--- /dev/null
+++ b/io_scene_gltf2/io/com/gltf2_io_debug.py
@@ -0,0 +1,138 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Imports
+#
+
+import time
+import logging
+
+#
+# Globals
+#
+
+OUTPUT_LEVELS = ['ERROR', 'WARNING', 'INFO', 'PROFILE', 'DEBUG', 'VERBOSE']
+
+g_current_output_level = 'DEBUG'
+g_profile_started = False
+g_profile_start = 0.0
+g_profile_end = 0.0
+g_profile_delta = 0.0
+
+#
+# Functions
+#
+
+
+def set_output_level(level):
+    """Set an output debug level."""
+    global g_current_output_level
+
+    if OUTPUT_LEVELS.index(level) < 0:
+        return
+
+    g_current_output_level = level
+
+
+def print_console(level, output):
+    """Print to Blender console with a given header and output."""
+    global OUTPUT_LEVELS
+    global g_current_output_level
+
+    if OUTPUT_LEVELS.index(level) > OUTPUT_LEVELS.index(g_current_output_level):
+        return
+
+    print(level + ': ' + output)
+
+
+def print_newline():
+    """Print a new line to Blender console."""
+    print()
+
+
+def print_timestamp(label=None):
+    """Print a timestamp to Blender console."""
+    output = 'Timestamp: ' + str(time.time())
+
+    if label is not None:
+        output = output + ' (' + label + ')'
+
+    print_console('PROFILE', output)
+
+
+def profile_start():
+    """Start profiling by storing the current time."""
+    global g_profile_start
+    global g_profile_started
+
+    if g_profile_started:
+        print_console('ERROR', 'Profiling already started')
+        return
+
+    g_profile_started = True
+
+    g_profile_start = time.time()
+
+
+def profile_end(label=None):
+    """Stop profiling and printing out the delta time since profile start."""
+    global g_profile_end
+    global g_profile_delta
+    global g_profile_started
+
+    if not g_profile_started:
+        print_console('ERROR', 'Profiling not started')
+        return
+
+    g_profile_started = False
+
+    g_profile_end = time.time()
+    g_profile_delta = g_profile_end - g_profile_start
+
+    output = 'Delta time: ' + str(g_profile_delta)
+
+    if label is not None:
+        output = output + ' (' + label + ')'
+
+    print_console('PROFILE', output)
+
+
+# TODO: need to have a unique system for logging importer/exporter
+# TODO: this logger is used for importer, but in io and in blender part, but is written here in a _io_ file
+class Log:
+    def __init__(self, loglevel):
+        self.logger = logging.getLogger('glTFImporter')
+        self.hdlr = logging.StreamHandler()
+        formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
+        self.hdlr.setFormatter(formatter)
+        self.logger.addHandler(self.hdlr)
+        self.logger.setLevel(int(loglevel))
+
+    @staticmethod
+    def get_levels():
+        levels = [
+            (str(logging.CRITICAL), "Critical", "", logging.CRITICAL),
+            (str(logging.ERROR), "Error", "", logging.ERROR),
+            (str(logging.WARNING), "Warning", "", logging.WARNING),
+            (str(logging.INFO), "Info", "", logging.INFO),
+            (str(logging.NOTSET), "NotSet", "", logging.NOTSET)
+        ]
+
+        return levels
+
+    @staticmethod
+    def default():
+        return str(logging.ERROR)
+
diff --git a/io_scene_gltf2/io/com/gltf2_io_functional.py b/io_scene_gltf2/io/com/gltf2_io_functional.py
new file mode 100755
index 0000000000000000000000000000000000000000..eb65112f5384e03e29b0056c5943c6c534c3b49a
--- /dev/null
+++ b/io_scene_gltf2/io/com/gltf2_io_functional.py
@@ -0,0 +1,41 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import typing
+
+
+def chunks(lst: typing.Sequence[typing.Any], n: int) -> typing.List[typing.Any]:
+    """
+    Generator that yields successive n sized chunks of the list l
+    :param lst: the list to be split
+    :param n: the length of the chunks
+    :return: a sublist of at most length n
+    """
+    result = []
+    for i in range(0, len(lst), n):
+        result.append(lst[i:i + n])
+    return result
+
+
+def unzip(*args: typing.Iterable[typing.Any]) -> typing.Iterable[typing.Iterable[typing.Any]]:
+    """
+    Unzip the list. Inverse of the builtin zip
+    :param args: a list of lists or multiple list arguments
+    :return: a list of unzipped lists
+    """
+    if len(args) == 1:
+        args = args[0]
+
+    return zip(*args)
+
diff --git a/io_scene_gltf2/io/com/gltf2_io_image.py b/io_scene_gltf2/io/com/gltf2_io_image.py
new file mode 100755
index 0000000000000000000000000000000000000000..af86daeb365039dc7d6e91c32b7b32dd9abd3dd9
--- /dev/null
+++ b/io_scene_gltf2/io/com/gltf2_io_image.py
@@ -0,0 +1,154 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Imports
+#
+
+import struct
+import zlib
+
+
+class Image:
+    """
+    Image object class to represent a 4-channel RGBA image.
+
+    Pixel values are expected to be floating point in the range of [0.0 to 1.0]
+    """
+
+    def __init__(self, width, height, pixels):
+        self.width = width
+        self.height = height
+        self.channels = 4
+        self.pixels = pixels
+        self.name = ""
+        self.file_format = "PNG"
+
+    def to_png_data(self):
+        buf = bytearray([int(channel * 255.0) for channel in self.pixels])
+
+        #
+        # Taken from 'blender-thumbnailer.py' in Blender.
+        #
+
+        # reverse the vertical line order and add null bytes at the start
+        width_byte_4 = self.width * 4
+        raw_data = b"".join(
+            b'\x00' + buf[span:span + width_byte_4] for span in range(
+                (self.height - 1) * self.width * 4, -1, - width_byte_4))
+
+        def png_pack(png_tag, data):
+            chunk_head = png_tag + data
+            return struct.pack("!I", len(data)) + chunk_head + struct.pack("!I", 0xFFFFFFFF & zlib.crc32(chunk_head))
+
+        return b"".join([
+            b'\x89PNG\r\n\x1a\n',
+            png_pack(b'IHDR', struct.pack("!2I5B", self.width, self.height, 8, 6, 0, 0, 0)),
+            png_pack(b'IDAT', zlib.compress(raw_data, 9)),
+            png_pack(b'IEND', b'')])
+
+    def to_image_data(self, mime_type):
+        if mime_type == 'image/png':
+            return self.to_png_data()
+        raise ValueError("Unsupported image file type {}".format(mime_type))
+
+    def save_png(self, dst_path):
+        data = self.to_png_data()
+        with open(dst_path, 'wb') as f:
+            f.write(data)
+
+
+def create_img(width, height, r=0.0, g=0.0, b=0.0, a=1.0):
+    """
+    Create a new image object with 4 channels and initialize it with the given default values.
+
+    (if no arguments are given, these default to R=0, G=0, B=0, A=1.0)
+    Return the created image object.
+    """
+    return Image(width, height, [r, g, b, a] * (width * height))
+
+
+def create_img_from_pixels(width, height, pixels):
+    """
+    Create a new image object with 4 channels and initialize it using the given array of pixel data.
+
+    Return the created image object.
+    """
+    if pixels is None or len(pixels) != width * height * 4:
+        return None
+
+    return Image(width, height, pixels)
+
+
+def copy_img_channel(dst_image, dst_channel, src_image, src_channel):
+    """
+    Copy a single channel (identified by src_channel) from src_image to dst_image (overwriting dst_channel).
+
+    src_image and dst_image are expected to be image objects created using create_img.
+    Return True on success, False otherwise.
+    """
+    if dst_image is None or src_image is None:
+        return False
+
+    if dst_channel < 0 or dst_channel >= dst_image.channels or src_channel < 0 or src_channel >= src_image.channels:
+        return False
+
+    if src_image.width != dst_image.width or \
+            src_image.height != dst_image.height or \
+            src_image.channels != dst_image.channels:
+        return False
+
+    for i in range(0, len(dst_image.pixels), dst_image.channels):
+        dst_image.pixels[i + dst_channel] = src_image.pixels[i + src_channel]
+
+    return True
+
+
+def test_save_img(image, path):
+    """
+    Save the given image to a PNG file (specified by path).
+
+    Return True on success, False otherwise.
+    """
+    if image is None or image.channels != 4:
+        return False
+
+    width = image.width
+    height = image.height
+
+    buf = bytearray([int(channel * 255.0) for channel in image.pixels])
+
+    #
+    # Taken from 'blender-thumbnailer.py' in Blender.
+    #
+
+    # reverse the vertical line order and add null bytes at the start
+    width_byte_4 = width * 4
+    raw_data = b"".join(
+        b'\x00' + buf[span:span + width_byte_4] for span in range((height - 1) * width * 4, -1, - width_byte_4))
+
+    def png_pack(png_tag, data):
+        chunk_head = png_tag + data
+        return struct.pack("!I", len(data)) + chunk_head + struct.pack("!I", 0xFFFFFFFF & zlib.crc32(chunk_head))
+
+    data = b"".join([
+        b'\x89PNG\r\n\x1a\n',
+        png_pack(b'IHDR', struct.pack("!2I5B", width, height, 8, 6, 0, 0, 0)),
+        png_pack(b'IDAT', zlib.compress(raw_data, 9)),
+        png_pack(b'IEND', b'')])
+
+    with open(path, 'wb') as f:
+        f.write(data)
+        return True
+
diff --git a/io_scene_gltf2/io/com/gltf2_io_trs.py b/io_scene_gltf2/io/com/gltf2_io_trs.py
new file mode 100755
index 0000000000000000000000000000000000000000..59f30830e56f45278db2990caf1b6cc07f0b7450
--- /dev/null
+++ b/io_scene_gltf2/io/com/gltf2_io_trs.py
@@ -0,0 +1,68 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+class TRS:
+
+    def __new__(cls, *args, **kwargs):
+        raise RuntimeError("{} should not be instantiated".format(cls.__name__))
+
+    @staticmethod
+    def scale_to_matrix(scale):
+        # column major !
+        return [scale[0], 0, 0, 0,
+                0, scale[1], 0, 0,
+                0, 0, scale[2], 0,
+                0, 0, 0, 1]
+
+    @staticmethod
+    def quaternion_to_matrix(q):
+        x, y, z, w = q
+        # TODO : is q normalized ? --> if not, multiply by 1/(w*w + x*x + y*y + z*z)
+        # column major !
+        return [
+            1 - 2 * y * y - 2 * z * z, 2 * x * y + 2 * w * z, 2 * x * z - 2 * w * y, 0,
+            2 * x * y - 2 * w * z, 1 - 2 * x * x - 2 * z * z, 2 * y * z + 2 * w * x, 0,
+            2 * x * z + 2 * y * w, 2 * y * z - 2 * w * x, 1 - 2 * x * x - 2 * y * y, 0,
+            0, 0, 0, 1]
+
+    @staticmethod
+    def matrix_multiply(m, n):
+        # column major !
+
+        return [
+            m[0] * n[0] + m[4] * n[1] + m[8] * n[2] + m[12] * n[3],
+            m[1] * n[0] + m[5] * n[1] + m[9] * n[2] + m[13] * n[3],
+            m[2] * n[0] + m[6] * n[1] + m[10] * n[2] + m[14] * n[3],
+            m[3] * n[0] + m[7] * n[1] + m[11] * n[2] + m[15] * n[3],
+            m[0] * n[4] + m[4] * n[5] + m[8] * n[6] + m[12] * n[7],
+            m[1] * n[4] + m[5] * n[5] + m[9] * n[6] + m[13] * n[7],
+            m[2] * n[4] + m[6] * n[5] + m[10] * n[6] + m[14] * n[7],
+            m[3] * n[4] + m[7] * n[5] + m[11] * n[6] + m[15] * n[7],
+            m[0] * n[8] + m[4] * n[9] + m[8] * n[10] + m[12] * n[11],
+            m[1] * n[8] + m[5] * n[9] + m[9] * n[10] + m[13] * n[11],
+            m[2] * n[8] + m[6] * n[9] + m[10] * n[10] + m[14] * n[11],
+            m[3] * n[8] + m[7] * n[9] + m[11] * n[10] + m[15] * n[11],
+            m[0] * n[12] + m[4] * n[13] + m[8] * n[14] + m[12] * n[15],
+            m[1] * n[12] + m[5] * n[13] + m[9] * n[14] + m[13] * n[15],
+            m[2] * n[12] + m[6] * n[13] + m[10] * n[14] + m[14] * n[15],
+            m[3] * n[12] + m[7] * n[13] + m[11] * n[14] + m[15] * n[15],
+        ]
+
+    @staticmethod
+    def translation_to_matrix(translation):
+        # column major !
+        return [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0,
+                translation[0], translation[1], translation[2], 1.0]
+
diff --git a/io_scene_gltf2/io/exp/gltf2_io_binary_data.py b/io_scene_gltf2/io/exp/gltf2_io_binary_data.py
new file mode 100755
index 0000000000000000000000000000000000000000..42f6d5d76e29cf8d33cb262f66439874c6bb4404
--- /dev/null
+++ b/io_scene_gltf2/io/exp/gltf2_io_binary_data.py
@@ -0,0 +1,36 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import typing
+import array
+from io_scene_gltf2.io.com import gltf2_io_constants
+
+
+class BinaryData:
+    """Store for gltf binary data that can later be stored in a buffer."""
+
+    def __init__(self, data: bytes):
+        if not isinstance(data, bytes):
+            raise TypeError("Data is not a bytes array")
+        self.data = data
+
+    @classmethod
+    def from_list(cls, lst: typing.List[typing.Any], gltf_component_type: gltf2_io_constants.ComponentType):
+        format_char = gltf2_io_constants.ComponentType.to_type_code(gltf_component_type)
+        return BinaryData(array.array(format_char, lst).tobytes())
+
+    @property
+    def byte_length(self):
+        return len(self.data)
+
diff --git a/io_scene_gltf2/io/exp/gltf2_io_buffer.py b/io_scene_gltf2/io/exp/gltf2_io_buffer.py
new file mode 100755
index 0000000000000000000000000000000000000000..694be11e47eeb3972ccabc00671e8d8f7672a5cf
--- /dev/null
+++ b/io_scene_gltf2/io/exp/gltf2_io_buffer.py
@@ -0,0 +1,61 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import base64
+
+from io_scene_gltf2.io.com import gltf2_io
+from io_scene_gltf2.io.exp import gltf2_io_binary_data
+
+
+class Buffer:
+    """Class representing binary data for use in a glTF file as 'buffer' property."""
+
+    def __init__(self, buffer_index=0):
+        self.__data = b""
+        self.__buffer_index = buffer_index
+
+    def add_and_get_view(self, binary_data: gltf2_io_binary_data.BinaryData) -> gltf2_io.BufferView:
+        """Add binary data to the buffer. Return a glTF BufferView."""
+        offset = len(self.__data)
+        self.__data += binary_data.data
+
+        # offsets should be a multiple of 4 --> therefore add padding if necessary
+        padding = (4 - (binary_data.byte_length % 4)) % 4
+        self.__data += b"\x00" * padding
+
+        buffer_view = gltf2_io.BufferView(
+            buffer=self.__buffer_index,
+            byte_length=binary_data.byte_length,
+            byte_offset=offset,
+            byte_stride=None,
+            extensions=None,
+            extras=None,
+            name=None,
+            target=None
+        )
+        return buffer_view
+
+    @property
+    def byte_length(self):
+        return len(self.__data)
+
+    def to_bytes(self):
+        return self.__data
+
+    def to_embed_string(self):
+        return 'data:application/octet-stream;base64,' + base64.b64encode(self.__data).decode('ascii')
+
+    def clear(self):
+        self.__data = b""
+
diff --git a/io_scene_gltf2/io/exp/gltf2_io_export.py b/io_scene_gltf2/io/exp/gltf2_io_export.py
new file mode 100755
index 0000000000000000000000000000000000000000..561b2ac143553b0a679c16e5896937f225b31e82
--- /dev/null
+++ b/io_scene_gltf2/io/exp/gltf2_io_export.py
@@ -0,0 +1,97 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Imports
+#
+
+import json
+import struct
+
+#
+# Globals
+#
+
+#
+# Functions
+#
+
+
+def save_gltf(glTF, export_settings, encoder, glb_buffer):
+    indent = None
+    separators = separators = (',', ':')
+
+    if export_settings['gltf_format'] == 'ASCII' and not export_settings['gltf_strip']:
+        indent = 4
+        # The comma is typically followed by a newline, so no trailing whitespace is needed on it.
+        separators = separators = (',', ' : ')
+
+    glTF_encoded = json.dumps(glTF, indent=indent, separators=separators, sort_keys=True, cls=encoder, allow_nan=False)
+
+    #
+
+    if export_settings['gltf_format'] == 'ASCII':
+        file = open(export_settings['gltf_filepath'], "w", encoding="utf8", newline="\n")
+        file.write(glTF_encoded)
+        file.write("\n")
+        file.close()
+
+        binary = export_settings['gltf_binary']
+        if len(binary) > 0 and not export_settings['gltf_embed_buffers']:
+            file = open(export_settings['gltf_filedirectory'] + export_settings['gltf_binaryfilename'], "wb")
+            file.write(binary)
+            file.close()
+
+    else:
+        file = open(export_settings['gltf_filepath'], "wb")
+
+        glTF_data = glTF_encoded.encode()
+        binary = glb_buffer
+
+        length_gtlf = len(glTF_data)
+        spaces_gltf = (4 - (length_gtlf & 3)) & 3
+        length_gtlf += spaces_gltf
+
+        length_bin = len(binary)
+        zeros_bin = (4 - (length_bin & 3)) & 3
+        length_bin += zeros_bin
+
+        length = 12 + 8 + length_gtlf
+        if length_bin > 0:
+            length += 8 + length_bin
+
+        # Header (Version 2)
+        file.write('glTF'.encode())
+        file.write(struct.pack("I", 2))
+        file.write(struct.pack("I", length))
+
+        # Chunk 0 (JSON)
+        file.write(struct.pack("I", length_gtlf))
+        file.write('JSON'.encode())
+        file.write(glTF_data)
+        for i in range(0, spaces_gltf):
+            file.write(' '.encode())
+
+        # Chunk 1 (BIN)
+        if length_bin > 0:
+            file.write(struct.pack("I", length_bin))
+            file.write('BIN\0'.encode())
+            file.write(binary)
+            for i in range(0, zeros_bin):
+                file.write('\0'.encode())
+
+        file.close()
+
+    return True
+
diff --git a/io_scene_gltf2/io/exp/gltf2_io_get.py b/io_scene_gltf2/io/exp/gltf2_io_get.py
new file mode 100755
index 0000000000000000000000000000000000000000..35c65615d99829e1776ae991327d45a23935aff1
--- /dev/null
+++ b/io_scene_gltf2/io/exp/gltf2_io_get.py
@@ -0,0 +1,316 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Imports
+#
+
+import os
+
+#
+# Globals
+#
+
+#
+# Functions
+#
+
+
+def get_material_requires_texcoords(glTF, index):
+    """Query function, if a material "needs" texture coordinates. This is the case, if a texture is present and used."""
+    if glTF.materials is None:
+        return False
+
+    materials = glTF.materials
+
+    if index < 0 or index >= len(materials):
+        return False
+
+    material = materials[index]
+
+    # General
+
+    if material.emissive_texture is not None:
+        return True
+
+    if material.normal_texture is not None:
+        return True
+
+    if material.occlusion_texture is not None:
+        return True
+
+    # Metallic roughness
+
+    if material.pbr_metallic_roughness is not None and \
+            material.pbr_metallic_roughness.base_color_texture is not None:
+        return True
+
+    if material.pbr_metallic_roughness is not None and \
+            material.pbr_metallic_roughness.metallic_roughness_texture is not None:
+        return True
+
+    return False
+
+
+def get_material_requires_normals(glTF, index):
+    """
+    Query function, if a material "needs" normals. This is the case, if a texture is present and used.
+
+    At point of writing, same function as for texture coordinates.
+    """
+    return get_material_requires_texcoords(glTF, index)
+
+
+def get_material_index(glTF, name):
+    """Return the material index in the glTF array."""
+    if name is None:
+        return -1
+
+    if glTF.materials is None:
+        return -1
+
+    index = 0
+    for material in glTF.materials:
+        if material.name == name:
+            return index
+
+        index += 1
+
+    return -1
+
+
+def get_mesh_index(glTF, name):
+    """Return the mesh index in the glTF array."""
+    if glTF.meshes is None:
+        return -1
+
+    index = 0
+    for mesh in glTF.meshes:
+        if mesh.name == name:
+            return index
+
+        index += 1
+
+    return -1
+
+
+def get_skin_index(glTF, name, index_offset):
+    """Return the skin index in the glTF array."""
+    if glTF.skins is None:
+        return -1
+
+    skeleton = get_node_index(glTF, name)
+
+    index = 0
+    for skin in glTF.skins:
+        if skin.skeleton == skeleton:
+            return index + index_offset
+
+        index += 1
+
+    return -1
+
+
+def get_camera_index(glTF, name):
+    """Return the camera index in the glTF array."""
+    if glTF.cameras is None:
+        return -1
+
+    index = 0
+    for camera in glTF.cameras:
+        if camera.name == name:
+            return index
+
+        index += 1
+
+    return -1
+
+
+def get_light_index(glTF, name):
+    """Return the light index in the glTF array."""
+    if glTF.extensions is None:
+        return -1
+
+    extensions = glTF.extensions
+
+    if extensions.get('KHR_lights_punctual') is None:
+        return -1
+
+    khr_lights_punctual = extensions['KHR_lights_punctual']
+
+    if khr_lights_punctual.get('lights') is None:
+        return -1
+
+    lights = khr_lights_punctual['lights']
+
+    index = 0
+    for light in lights:
+        if light['name'] == name:
+            return index
+
+        index += 1
+
+    return -1
+
+
+def get_node_index(glTF, name):
+    """Return the node index in the glTF array."""
+    if glTF.nodes is None:
+        return -1
+
+    index = 0
+    for node in glTF.nodes:
+        if node.name == name:
+            return index
+
+        index += 1
+
+    return -1
+
+
+def get_scene_index(glTF, name):
+    """Return the scene index in the glTF array."""
+    if glTF.scenes is None:
+        return -1
+
+    index = 0
+    for scene in glTF.scenes:
+        if scene.name == name:
+            return index
+
+        index += 1
+
+    return -1
+
+
+def get_texture_index(glTF, filename):
+    """Return the texture index in the glTF array by a given file path."""
+    if glTF.textures is None:
+        return -1
+
+    image_index = get_image_index(glTF, filename)
+
+    if image_index == -1:
+        return -1
+
+    for texture_index, texture in enumerate(glTF.textures):
+        if image_index == texture.source:
+            return texture_index
+
+    return -1
+
+
+def get_image_index(glTF, filename):
+    """Return the image index in the glTF array."""
+    if glTF.images is None:
+        return -1
+
+    image_name = get_image_name(filename)
+
+    for index, current_image in enumerate(glTF.images):
+        if image_name == current_image.name:
+            return index
+
+    return -1
+
+
+def get_image_name(filename):
+    """Return user-facing, extension-agnostic name for image."""
+    return os.path.splitext(filename)[0]
+
+
+def get_scalar(default_value, init_value=0.0):
+    """Return scalar with a given default/fallback value."""
+    return_value = init_value
+
+    if default_value is None:
+        return return_value
+
+    return_value = default_value
+
+    return return_value
+
+
+def get_vec2(default_value, init_value=[0.0, 0.0]):
+    """Return vec2 with a given default/fallback value."""
+    return_value = init_value
+
+    if default_value is None or len(default_value) < 2:
+        return return_value
+
+    index = 0
+    for number in default_value:
+        return_value[index] = number
+
+        index += 1
+        if index == 2:
+            return return_value
+
+    return return_value
+
+
+def get_vec3(default_value, init_value=[0.0, 0.0, 0.0]):
+    """Return vec3 with a given default/fallback value."""
+    return_value = init_value
+
+    if default_value is None or len(default_value) < 3:
+        return return_value
+
+    index = 0
+    for number in default_value:
+        return_value[index] = number
+
+        index += 1
+        if index == 3:
+            return return_value
+
+    return return_value
+
+
+def get_vec4(default_value, init_value=[0.0, 0.0, 0.0, 1.0]):
+    """Return vec4 with a given default/fallback value."""
+    return_value = init_value
+
+    if default_value is None or len(default_value) < 4:
+        return return_value
+
+    index = 0
+    for number in default_value:
+        return_value[index] = number
+
+        index += 1
+        if index == 4:
+            return return_value
+
+    return return_value
+
+
+def get_index(elements, name):
+    """Return index of a glTF element by a given name."""
+    if elements is None or name is None:
+        return -1
+
+    index = 0
+    for element in elements:
+        if isinstance(element, dict):
+            if element.get('name') == name:
+                return index
+        else:
+            if element.name == name:
+                return index
+
+        index += 1
+
+    return -1
+
diff --git a/io_scene_gltf2/io/exp/gltf2_io_image_data.py b/io_scene_gltf2/io/exp/gltf2_io_image_data.py
new file mode 100755
index 0000000000000000000000000000000000000000..23a2843eed2a136400d188a938ff1d1976acfaaa
--- /dev/null
+++ b/io_scene_gltf2/io/exp/gltf2_io_image_data.py
@@ -0,0 +1,106 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import typing
+import struct
+import zlib
+import numpy as np
+
+class ImageData:
+    """Contains channels of an image with raw pixel data."""
+    # TODO: refactor to only operate on numpy arrays
+    # FUTURE_WORK: as a method to allow the node graph to be better supported, we could model some of
+    # the node graph elements with numpy functions
+
+    def __init__(self, name: str, width: int, height: int, channels: typing.Optional[typing.List[np.ndarray]] = []):
+        if width <= 0 or height <= 0:
+            raise ValueError("Image data can not have zero width or height")
+        self.name = name
+        self.channels = channels
+        self.width = width
+        self.height = height
+
+    def add_to_image(self, image_data):
+        if self.width != image_data.width or self.height != image_data.height:
+            raise ValueError("Image dimensions do not match")
+        if len(self.channels) + len(image_data.channels) > 4:
+            raise ValueError("Can't append image: channels full")
+        self.name += image_data.name
+        self.channels += image_data.channels
+
+    @property
+    def r(self):
+        if len(self.channels) <= 0:
+            return None
+        return self.channels[0]
+
+    @property
+    def g(self):
+        if len(self.channels) <= 1:
+            return None
+        return self.channels[1]
+
+    @property
+    def b(self):
+        if len(self.channels) <= 2:
+            return None
+        return self.channels[2]
+
+    @property
+    def a(self):
+        if len(self.channels) <= 3:
+            return None
+        return self.channels[3]
+
+    def to_image_data(self, mime_type: str) -> bytes:
+        if mime_type == 'image/png':
+            return self.to_png_data()
+        raise ValueError("Unsupported image file type {}".format(mime_type))
+
+    def to_png_data(self) -> bytes:
+        channels = self.channels
+
+        # if there is no data, create a single pixel image
+        if not channels:
+            channels = np.zeros((1, 1))
+
+        # fill all channels of the png
+        for _ in range(4 - len(channels)):
+            channels.append(np.ones_like(channels[0]))
+
+        image = np.concatenate(self.channels, axis=1)
+        image = image.flatten()
+        image = (image * 255.0).astype(np.uint8)
+        buf = image.tobytes()
+
+        #
+        # Taken from 'blender-thumbnailer.py' in Blender.
+        #
+
+        # reverse the vertical line order and add null bytes at the start
+        width_byte_4 = self.width * 4
+        raw_data = b"".join(
+            b'\x00' + buf[span:span + width_byte_4] for span in range(
+                (self.height - 1) * self.width * 4, -1, - width_byte_4))
+
+        def png_pack(png_tag, data):
+            chunk_head = png_tag + data
+            return struct.pack("!I", len(data)) + chunk_head + struct.pack("!I", 0xFFFFFFFF & zlib.crc32(chunk_head))
+
+        return b"".join([
+            b'\x89PNG\r\n\x1a\n',
+            png_pack(b'IHDR', struct.pack("!2I5B", self.width, self.height, 8, 6, 0, 0, 0)),
+            png_pack(b'IDAT', zlib.compress(raw_data, 9)),
+            png_pack(b'IEND', b'')])
+
diff --git a/io_scene_gltf2/io/imp/__init__.py b/io_scene_gltf2/io/imp/__init__.py
new file mode 100755
index 0000000000000000000000000000000000000000..d3c53771b2764e155012f535807e451c52f8a369
--- /dev/null
+++ b/io_scene_gltf2/io/imp/__init__.py
@@ -0,0 +1,16 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""IO imp package."""
+
diff --git a/io_scene_gltf2/io/imp/gltf2_io_binary.py b/io_scene_gltf2/io/imp/gltf2_io_binary.py
new file mode 100755
index 0000000000000000000000000000000000000000..5f51d95d7bbecffd6ed8879b7a0154bd696a2f28
--- /dev/null
+++ b/io_scene_gltf2/io/imp/gltf2_io_binary.py
@@ -0,0 +1,178 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import struct
+import base64
+from os.path import dirname, join, isfile, basename
+
+
+class BinaryData():
+    """Binary reader."""
+    def __new__(cls, *args, **kwargs):
+        raise RuntimeError("%s should not be instantiated" % cls)
+
+    @staticmethod
+    def get_binary_from_accessor(gltf, accessor_idx):
+        """Get binary from accessor."""
+        accessor = gltf.data.accessors[accessor_idx]
+        bufferView = gltf.data.buffer_views[accessor.buffer_view]  # TODO initialize with 0 when not present!
+        if bufferView.buffer in gltf.buffers.keys():
+            buffer = gltf.buffers[bufferView.buffer]
+        else:
+            # load buffer
+            gltf.load_buffer(bufferView.buffer)
+            buffer = gltf.buffers[bufferView.buffer]
+
+        accessor_offset = accessor.byte_offset
+        bufferview_offset = bufferView.byte_offset
+
+        if accessor_offset is None:
+            accessor_offset = 0
+        if bufferview_offset is None:
+            bufferview_offset = 0
+
+        return buffer[accessor_offset + bufferview_offset:accessor_offset + bufferview_offset + bufferView.byte_length]
+
+    @staticmethod
+    def get_data_from_accessor(gltf, accessor_idx):
+        """Get data from accessor."""
+        accessor = gltf.data.accessors[accessor_idx]
+
+        bufferView = gltf.data.buffer_views[accessor.buffer_view]  # TODO initialize with 0 when not present!
+        buffer_data = BinaryData.get_binary_from_accessor(gltf, accessor_idx)
+
+        fmt_char = gltf.fmt_char_dict[accessor.component_type]
+        component_nb = gltf.component_nb_dict[accessor.type]
+        fmt = '<' + (fmt_char * component_nb)
+        stride_ = struct.calcsize(fmt)
+        # TODO data alignment stuff
+
+        if bufferView.byte_stride:
+            stride = bufferView.byte_stride
+        else:
+            stride = stride_
+
+        data = []
+        offset = 0
+        while len(data) < accessor.count:
+            element = struct.unpack_from(fmt, buffer_data, offset)
+            data.append(element)
+            offset += stride
+
+        if accessor.sparse:
+            sparse_indices_data = BinaryData.get_data_from_sparse(gltf, accessor.sparse, "indices")
+            sparse_values_values = BinaryData.get_data_from_sparse(
+                gltf,
+                accessor.sparse,
+                "values",
+                accessor.type,
+                accessor.component_type
+            )
+
+            # apply sparse
+            for cpt_idx, idx in enumerate(sparse_indices_data):
+                data[idx[0]] = sparse_values_values[cpt_idx]
+
+        # Normalization
+        if accessor.normalized:
+            for idx, tuple in enumerate(data):
+                new_tuple = ()
+                for i in tuple:
+                    new_tuple += (float(i),)
+                data[idx] = new_tuple
+
+        return data
+
+    @staticmethod
+    def get_data_from_sparse(gltf, sparse, type_, type_val=None, comp_type=None):
+        """Get data from sparse."""
+        if type_ == "indices":
+            bufferView = gltf.data.buffer_views[sparse.indices.buffer_view]
+            offset = sparse.indices.byte_offset
+            component_nb = gltf.component_nb_dict['SCALAR']
+            fmt_char = gltf.fmt_char_dict[sparse.indices.component_type]
+        elif type_ == "values":
+            bufferView = gltf.data.buffer_views[sparse.values.buffer_view]
+            offset = sparse.values.byte_offset
+            component_nb = gltf.component_nb_dict[type_val]
+            fmt_char = gltf.fmt_char_dict[comp_type]
+
+        if bufferView.buffer in gltf.buffers.keys():
+            buffer = gltf.buffers[bufferView.buffer]
+        else:
+            # load buffer
+            gltf.load_buffer(bufferView.buffer)
+            buffer = gltf.buffers[bufferView.buffer]
+
+        bin_data = buffer[bufferView.byte_offset + offset:bufferView.byte_offset + offset + bufferView.byte_length]
+
+        fmt = '<' + (fmt_char * component_nb)
+        stride_ = struct.calcsize(fmt)
+        # TODO data alignment stuff ?
+
+        if bufferView.byte_stride:
+            stride = bufferView.byte_stride
+        else:
+            stride = stride_
+
+        data = []
+        offset = 0
+        while len(data) < sparse.count:
+            element = struct.unpack_from(fmt, bin_data, offset)
+            data.append(element)
+            offset += stride
+
+        return data
+
+    @staticmethod
+    def get_image_data(gltf, img_idx):
+        """Get data from image."""
+        pyimage = gltf.data.images[img_idx]
+
+        image_name = "Image_" + str(img_idx)
+
+        if pyimage.uri:
+            sep = ';base64,'
+            if pyimage.uri[:5] == 'data:':
+                idx = pyimage.uri.find(sep)
+                if idx != -1:
+                    data = pyimage.uri[idx + len(sep):]
+                    return base64.b64decode(data), image_name
+
+            if isfile(join(dirname(gltf.filename), pyimage.uri)):
+                with open(join(dirname(gltf.filename), pyimage.uri), 'rb') as f_:
+                    return f_.read(), basename(join(dirname(gltf.filename), pyimage.uri))
+            else:
+                pyimage.gltf.log.error("Missing file (index " + str(img_idx) + "): " + pyimage.uri)
+                return None, None
+
+        if pyimage.buffer_view is None:
+            return None, None
+
+        bufferView = gltf.data.buffer_views[pyimage.buffer_view]
+
+        if bufferView.buffer in gltf.buffers.keys():
+            buffer = gltf.buffers[bufferView.buffer]
+        else:
+            # load buffer
+            gltf.load_buffer(bufferView.buffer)
+            buffer = gltf.buffers[bufferView.buffer]
+
+        bufferview_offset = bufferView.byte_offset
+
+        if bufferview_offset is None:
+            bufferview_offset = 0
+
+        return buffer[bufferview_offset:bufferview_offset + bufferView.byte_length], image_name
+
diff --git a/io_scene_gltf2/io/imp/gltf2_io_gltf.py b/io_scene_gltf2/io/imp/gltf2_io_gltf.py
new file mode 100755
index 0000000000000000000000000000000000000000..1c9e67a2d72221d275912a4cd917ff86af07ec9e
--- /dev/null
+++ b/io_scene_gltf2/io/imp/gltf2_io_gltf.py
@@ -0,0 +1,199 @@
+# Copyright 2018 The glTF-Blender-IO authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from ..com.gltf2_io import gltf_from_dict
+from ..com.gltf2_io_debug import Log
+import logging
+import json
+import struct
+import base64
+from os.path import dirname, join, getsize, isfile
+
+
+class glTFImporter():
+    """glTF Importer class."""
+
+    def __init__(self, filename, import_settings):
+        """initialization."""
+        self.filename = filename
+        self.import_settings = import_settings
+        self.buffers = {}
+
+        if 'loglevel' not in self.import_settings.keys():
+            self.import_settings['loglevel'] = logging.ERROR
+
+        log = Log(import_settings['loglevel'])
+        self.log = log.logger
+        self.log_handler = log.hdlr
+
+        self.SIMPLE = 1
+        self.TEXTURE = 2
+        self.TEXTURE_FACTOR = 3
+
+        # TODO: move to a com place?
+        self.extensions_managed = [
+            'KHR_materials_pbrSpecularGlossiness'
+        ]
+
+        # TODO : merge with io_constants
+        self.fmt_char_dict = {}
+        self.fmt_char_dict[5120] = 'b'  # Byte
+        self.fmt_char_dict[5121] = 'B'  # Unsigned Byte
+        self.fmt_char_dict[5122] = 'h'  # Short
+        self.fmt_char_dict[5123] = 'H'  # Unsigned Short
+        self.fmt_char_dict[5125] = 'I'  # Unsigned Int
+        self.fmt_char_dict[5126] = 'f'  # Float
+
+        self.component_nb_dict = {}
+        self.component_nb_dict['SCALAR'] = 1
+        self.component_nb_dict['VEC2'] = 2
+        self.component_nb_dict['VEC3'] = 3
+        self.component_nb_dict['VEC4'] = 4
+        self.component_nb_dict['MAT2'] = 4
+        self.component_nb_dict['MAT3'] = 9
+        self.component_nb_dict['MAT4'] = 16
+
+    @staticmethod
+    def bad_json_value(val):
+        """Bad Json value."""
+        raise ValueError('Json contains some unauthorized values')
+
+    def checks(self):
+        """Some checks."""
+        if self.data.asset.version != "2.0":
+            return False, "glTF version must be 2"
+
+        if self.data.extensions_required is not None:
+            for extension in self.data.extensions_required:
+                if extension not in self.data.extensions_used:
+                    return False, "Extension required must be in Extension Used too"
+                if extension not in self.extensions_managed:
+                    return False, "Extension " + extension + " is not available on this addon version"
+
+        if self.data.extensions_used is not None:
+            for extension in self.data.extensions_used:
+                if extension not in self.extensions_managed:
+                    # Non blocking error #TODO log
+                    pass
+
+        return True, None
+
+    def load_glb(self):
+        """Load binary glb."""
+        header = struct.unpack_from('<4sII', self.content)
+        self.format = header[0]
+        self.version = header[1]
+        self.file_size = header[2]
+
+        if self.format != b'glTF':
+            return False, "This file is not a glTF/glb file"
+
+        if self.version != 2:
+            return False, "glTF version doesn't match to 2"
+
+        if self.file_size != getsize(self.filename):
+            return False, "File size doesn't match"
+
+        offset = 12  # header size = 12
+
+        # TODO check json type for chunk 0, and BIN type for next ones
+
+        # json
+        type, len_, str_json, offset = self.load_chunk(offset)
+        if len_ != len(str_json):
+            return False, "Length of json part doesn't match"
+        try:
+            json_ = json.loads(str_json.decode('utf-8'), parse_constant=glTFImporter.bad_json_value)
+            self.data = gltf_from_dict(json_)
+        except ValueError as e:
+            return False, e.args[0]
+
+        # binary data
+        chunk_cpt = 0
+        while offset < len(self.content):
+            type, len_, data, offset = self.load_chunk(offset)
+            if len_ != len(data):
+                return False, "Length of bin buffer " + str(chunk_cpt) + " doesn't match"
+
+            self.buffers[chunk_cpt] = data
+            chunk_cpt += 1
+
+        self.content = None
+        return True, None
+
+    def load_chunk(self, offset):
+        """Load chunk."""
+        chunk_header = struct.unpack_from('<I4s', self.content, offset)
+        data_length = chunk_header[0]
+        data_type = chunk_header[1]
+        data = self.content[offset + 8: offset + 8 + data_length]
+
+        return data_type, data_length, data, offset + 8 + data_length
+
+    def read(self):
+        """Read file."""
+        # Check this is a file
+        if not isfile(self.filename):
+            return False, "Please select a file"
+
+        # Check if file is gltf or glb
+        with open(self.filename, 'rb') as f:
+            self.content = f.read()
+
+        self.is_glb_format = self.content[:4] == b'glTF'
+
+        # glTF file
+        if not self.is_glb_format:
+            self.content = None
+            with open(self.filename, 'r') as f:
+                content = f.read()
+                try:
+                    self.data = gltf_from_dict(json.loads(content, parse_constant=glTFImporter.bad_json_value))
+                    return True, None
+                except ValueError as e:
+                    return False, e.args[0]
+
+        # glb file
+        else:
+            # Parsing glb file
+            success, txt = self.load_glb()
+            return success, txt
+
+    def is_node_joint(self, node_idx):
+        """Check if node is a joint."""
+        if not self.data.skins:  # if no skin in gltf file
+            return False, None
+
+        for skin_idx, skin in enumerate(self.data.skins):
+            if node_idx in skin.joints:
+                return True, skin_idx
+
+        return False, None
+
+    def load_buffer(self, buffer_idx):
+        """Load buffer."""
+        buffer = self.data.buffers[buffer_idx]
+
+        if buffer.uri:
+            sep = ';base64,'
+            if buffer.uri[:5] == 'data:':
+                idx = buffer.uri.find(sep)
+                if idx != -1:
+                    data = buffer.uri[idx + len(sep):]
+                    self.buffers[buffer_idx] = base64.b64decode(data)
+                    return
+
+            with open(join(dirname(self.filename), buffer.uri), 'rb') as f_:
+                self.buffers[buffer_idx] = f_.read()
+