From 51e15a9db4ce463e01c291f6ec9152efe629bab5 Mon Sep 17 00:00:00 2001
From: Julien Duroure <julien.duroure@gmail.com>
Date: Sun, 25 Sep 2022 17:08:07 +0200
Subject: [PATCH] glTF exporter: Big gltf primitive extraction refactoring +
 Blender attributes export

---
 io_scene_gltf2/__init__.py                    |  10 +-
 .../blender/com/gltf2_blender_conversion.py   |  54 ++
 .../blender/exp/gltf2_blender_extract.py      | 619 -------------
 ...tf2_blender_gather_primitive_attributes.py | 186 ++--
 .../exp/gltf2_blender_gather_primitives.py    |  10 +-
 ...gltf2_blender_gather_primitives_extract.py | 863 ++++++++++++++++++
 .../blender/exp/gltf2_blender_gather_tree.py  |   1 -
 7 files changed, 997 insertions(+), 746 deletions(-)
 delete mode 100755 io_scene_gltf2/blender/exp/gltf2_blender_extract.py
 create mode 100644 io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives_extract.py

diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py
index 4d8acd68c..610c061ac 100755
--- a/io_scene_gltf2/__init__.py
+++ b/io_scene_gltf2/__init__.py
@@ -4,7 +4,7 @@
 bl_info = {
     'name': 'glTF 2.0 format',
     'author': 'Julien Duroure, Scurest, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin SchmithĂĽsen, Jim Eckerlein, and many external contributors',
-    "version": (3, 4, 22),
+    "version": (3, 4, 23),
     'blender': (3, 3, 0),
     'location': 'File > Import-Export',
     'description': 'Import-Export as glTF 2.0',
@@ -273,6 +273,12 @@ class ExportGLTF2_Base:
         default=True
     )
 
+    export_attributes: BoolProperty(
+        name='Attributes',
+        description='Export Attributes',
+        default=False
+    )
+
     use_mesh_edges: BoolProperty(
         name='Loose Edges',
         description=(
@@ -579,6 +585,7 @@ class ExportGLTF2_Base:
 
         export_settings['gltf_materials'] = self.export_materials
         export_settings['gltf_colors'] = self.export_colors
+        export_settings['gltf_attributes'] = self.export_attributes
         export_settings['gltf_cameras'] = self.export_cameras
 
         export_settings['gltf_original_specular'] = self.export_original_specular
@@ -808,6 +815,7 @@ class GLTF_PT_export_geometry_mesh(bpy.types.Panel):
         col.active = operator.export_normals
         col.prop(operator, 'export_tangents')
         layout.prop(operator, 'export_colors')
+        layout.prop(operator, 'export_attributes')
 
         col = layout.column()
         col.prop(operator, 'use_mesh_edges')
diff --git a/io_scene_gltf2/blender/com/gltf2_blender_conversion.py b/io_scene_gltf2/blender/com/gltf2_blender_conversion.py
index ecb91c8fb..85ab654a6 100755
--- a/io_scene_gltf2/blender/com/gltf2_blender_conversion.py
+++ b/io_scene_gltf2/blender/com/gltf2_blender_conversion.py
@@ -2,6 +2,8 @@
 # Copyright 2018-2021 The glTF-Blender-IO authors.
 
 from math import sin, cos
+import numpy as np
+from io_scene_gltf2.io.com import gltf2_io_constants
 
 def texture_transform_blender_to_gltf(mapping_transform):
     """
@@ -48,3 +50,55 @@ def get_target(property):
         "scale": "scale",
         "value": "weights"
     }.get(property)
+
+def get_component_type(attribute_component_type):
+    return {
+        "INT8": gltf2_io_constants.ComponentType.Float,
+        "BYTE_COLOR": gltf2_io_constants.ComponentType.UnsignedShort,
+        "FLOAT2": gltf2_io_constants.ComponentType.Float,
+        "FLOAT_COLOR": gltf2_io_constants.ComponentType.Float,
+        "FLOAT_VECTOR": gltf2_io_constants.ComponentType.Float,
+        "FLOAT_VECTOR_4": gltf2_io_constants.ComponentType.Float,
+        "INT": gltf2_io_constants.ComponentType.Float, # No signed Int in glTF accessor
+        "FLOAT": gltf2_io_constants.ComponentType.Float,
+        "BOOLEAN": gltf2_io_constants.ComponentType.Float
+    }.get(attribute_component_type)
+
+def get_data_type(attribute_component_type):
+    return {
+        "INT8": gltf2_io_constants.DataType.Scalar,
+        "BYTE_COLOR": gltf2_io_constants.DataType.Vec4,
+        "FLOAT2": gltf2_io_constants.DataType.Vec2,
+        "FLOAT_COLOR": gltf2_io_constants.DataType.Vec4,
+        "FLOAT_VECTOR": gltf2_io_constants.DataType.Vec3,
+        "FLOAT_VECTOR_4": gltf2_io_constants.DataType.Vec4,
+        "INT": gltf2_io_constants.DataType.Scalar,
+        "FLOAT": gltf2_io_constants.DataType.Scalar,
+        "BOOLEAN": gltf2_io_constants.DataType.Scalar,
+    }.get(attribute_component_type)
+
+def get_data_length(attribute_component_type):
+    return {
+        "INT8": 1,
+        "BYTE_COLOR": 4,
+        "FLOAT2": 2,
+        "FLOAT_COLOR": 4,
+        "FLOAT_VECTOR": 3,
+        "FLOAT_VECTOR_4": 4,
+        "INT": 1,
+        "FLOAT": 1,
+        "BOOLEAN": 1
+    }.get(attribute_component_type)
+
+def get_numpy_type(attribute_component_type):
+    return {
+        "INT8": np.float32,
+        "BYTE_COLOR": np.float32,
+        "FLOAT2": np.float32,
+        "FLOAT_COLOR": np.float32,
+        "FLOAT_VECTOR": np.float32,
+        "FLOAT_VECTOR_4": np.float32,
+        "INT": np.float32, #signed integer are not supported by glTF
+        "FLOAT": np.float32,
+        "BOOLEAN": np.float32
+    }.get(attribute_component_type)
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_extract.py b/io_scene_gltf2/blender/exp/gltf2_blender_extract.py
deleted file mode 100755
index bdea2c6fc..000000000
--- a/io_scene_gltf2/blender/exp/gltf2_blender_extract.py
+++ /dev/null
@@ -1,619 +0,0 @@
-# SPDX-License-Identifier: Apache-2.0
-# Copyright 2018-2021 The glTF-Blender-IO authors.
-
-import numpy as np
-from mathutils import Vector
-
-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_nodes
-
-
-def extract_primitives(blender_mesh, uuid_for_skined_data, blender_vertex_groups, modifiers, export_settings):
-    """Extract primitives from a mesh."""
-    print_console('INFO', 'Extracting primitive: ' + blender_mesh.name)
-
-    blender_object = None
-    if uuid_for_skined_data:
-        blender_object = export_settings['vtree'].nodes[uuid_for_skined_data].blender_object
-
-    use_normals = export_settings[gltf2_blender_export_keys.NORMALS]
-    if use_normals:
-        blender_mesh.calc_normals_split()
-
-    use_tangents = False
-    if use_normals and export_settings[gltf2_blender_export_keys.TANGENTS]:
-        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.')
-
-    tex_coord_max = 0
-    if export_settings[gltf2_blender_export_keys.TEX_COORDS]:
-        if blender_mesh.uv_layers.active:
-            tex_coord_max = len(blender_mesh.uv_layers)
-
-    color_max = 0
-    if export_settings[gltf2_blender_export_keys.COLORS]:
-        color_max = len(blender_mesh.vertex_colors)
-
-    colors_attributes = []
-    rendered_color_idx = blender_mesh.attributes.render_color_index
-
-    if color_max > 0:
-        colors_attributes.append(rendered_color_idx)
-        # Then find other ones
-        colors_attributes.extend([
-            i for i in range(len(blender_mesh.color_attributes)) if i != rendered_color_idx \
-                and blender_mesh.vertex_colors.find(blender_mesh.color_attributes[i].name) != -1
-        ])
-
-
-    armature = None
-    skin = None
-    if blender_vertex_groups and export_settings[gltf2_blender_export_keys.SKINS]:
-        if modifiers is not None:
-            modifiers_dict = {m.type: m for m in modifiers}
-            if "ARMATURE" in modifiers_dict:
-                modifier = modifiers_dict["ARMATURE"]
-                armature = modifier.object
-
-        # Skin must be ignored if the object is parented to a bone of the armature
-        # (This creates an infinite recursive error)
-        # So ignoring skin in that case
-        is_child_of_arma = (
-            armature and
-            blender_object and
-            blender_object.parent_type == "BONE" and
-            blender_object.parent.name == armature.name
-        )
-        if is_child_of_arma:
-            armature = None
-
-        if armature:
-            skin = gltf2_blender_gather_nodes.gather_skin(uuid_for_skined_data, export_settings)
-            if not skin:
-                armature = None
-
-    use_morph_normals = use_normals and export_settings[gltf2_blender_export_keys.MORPH_NORMAL]
-    use_morph_tangents = use_morph_normals and use_tangents and export_settings[gltf2_blender_export_keys.MORPH_TANGENT]
-
-    key_blocks = []
-    # Shape Keys can't be retrieve when using Apply Modifiers (Blender/bpy limitation)
-    if export_settings[gltf2_blender_export_keys.APPLY] is False and blender_mesh.shape_keys and export_settings[gltf2_blender_export_keys.MORPH]:
-        key_blocks = [
-            key_block
-            for key_block in blender_mesh.shape_keys.key_blocks
-            if not (key_block == key_block.relative_key or key_block.mute)
-        ]
-
-    use_materials = export_settings[gltf2_blender_export_keys.MATERIALS]
-
-    # Fetch vert positions and bone data (joint,weights)
-
-    locs, morph_locs = __get_positions(blender_mesh, key_blocks, armature, blender_object, export_settings)
-    if skin:
-        vert_bones, num_joint_sets, need_neutral_bone = __get_bone_data(blender_mesh, skin, blender_vertex_groups)
-        if need_neutral_bone is True:
-            # Need to create a fake joint at root of armature
-            # In order to assign not assigned vertices to it
-            # But for now, this is not yet possible, we need to wait the armature node is created
-            # Just store this, to be used later
-            armature_uuid = export_settings['vtree'].nodes[uuid_for_skined_data].armature
-            export_settings['vtree'].nodes[armature_uuid].need_neutral_bone = True
-
-    # In Blender there is both per-vert data, like position, and also per-loop
-    # (loop=corner-of-poly) data, like normals or UVs. glTF only has per-vert
-    # data, so we need to split Blender verts up into potentially-multiple glTF
-    # verts.
-    #
-    # First, we'll collect a "dot" for every loop: a struct that stores all the
-    # attributes at that loop, namely the vertex index (which determines all
-    # per-vert data), and all the per-loop data like UVs, etc.
-    #
-    # Each unique dot will become one unique glTF vert.
-
-    # List all fields the dot struct needs.
-    dot_fields = [('vertex_index', np.uint32)]
-    if use_normals:
-        dot_fields += [('nx', np.float32), ('ny', np.float32), ('nz', np.float32)]
-    if use_tangents:
-        dot_fields += [('tx', np.float32), ('ty', np.float32), ('tz', np.float32), ('tw', np.float32)]
-    for uv_i in range(tex_coord_max):
-        dot_fields += [('uv%dx' % uv_i, np.float32), ('uv%dy' % uv_i, np.float32)]
-    for col_i, _ in enumerate(colors_attributes):
-        dot_fields += [
-            ('color%dr' % col_i, np.float32),
-            ('color%dg' % col_i, np.float32),
-            ('color%db' % col_i, np.float32),
-            ('color%da' % col_i, np.float32),
-        ]
-    if use_morph_normals:
-        for morph_i, _ in enumerate(key_blocks):
-            dot_fields += [
-                ('morph%dnx' % morph_i, np.float32),
-                ('morph%dny' % morph_i, np.float32),
-                ('morph%dnz' % morph_i, np.float32),
-            ]
-
-    dots = np.empty(len(blender_mesh.loops), dtype=np.dtype(dot_fields))
-
-    vidxs = np.empty(len(blender_mesh.loops))
-    blender_mesh.loops.foreach_get('vertex_index', vidxs)
-    dots['vertex_index'] = vidxs
-    del vidxs
-
-    if use_normals:
-        kbs = key_blocks if use_morph_normals else []
-        normals, morph_normals = __get_normals(
-            blender_mesh, kbs, armature, blender_object, export_settings
-        )
-        dots['nx'] = normals[:, 0]
-        dots['ny'] = normals[:, 1]
-        dots['nz'] = normals[:, 2]
-        del normals
-        for morph_i, ns in enumerate(morph_normals):
-            dots['morph%dnx' % morph_i] = ns[:, 0]
-            dots['morph%dny' % morph_i] = ns[:, 1]
-            dots['morph%dnz' % morph_i] = ns[:, 2]
-        del morph_normals
-
-    if use_tangents:
-        tangents = __get_tangents(blender_mesh, armature, blender_object, export_settings)
-        dots['tx'] = tangents[:, 0]
-        dots['ty'] = tangents[:, 1]
-        dots['tz'] = tangents[:, 2]
-        del tangents
-        signs = __get_bitangent_signs(blender_mesh, armature, blender_object, export_settings)
-        dots['tw'] = signs
-        del signs
-
-    for uv_i in range(tex_coord_max):
-        uvs = __get_uvs(blender_mesh, uv_i)
-        dots['uv%dx' % uv_i] = uvs[:, 0]
-        dots['uv%dy' % uv_i] = uvs[:, 1]
-        del uvs
-
-    colors_types = []
-    for col_i, blender_col_i in enumerate(colors_attributes):
-        colors, colors_type, domain = __get_colors(blender_mesh, col_i, blender_col_i)
-        if domain == "POINT":
-            colors = colors[dots['vertex_index']]
-        colors_types.append(colors_type)
-        dots['color%dr' % col_i] = colors[:, 0]
-        dots['color%dg' % col_i] = colors[:, 1]
-        dots['color%db' % col_i] = colors[:, 2]
-        dots['color%da' % col_i] = colors[:, 3]
-        del colors
-
-    # Calculate triangles and sort them into primitives.
-
-    blender_mesh.calc_loop_triangles()
-    loop_indices = np.empty(len(blender_mesh.loop_triangles) * 3, dtype=np.uint32)
-    blender_mesh.loop_triangles.foreach_get('loops', loop_indices)
-
-    prim_indices = {}  # maps material index to TRIANGLES-style indices into dots
-
-    if use_materials == "NONE": # Only for None. For placeholder and export, keep primitives
-        # Put all vertices into one primitive
-        prim_indices[-1] = loop_indices
-
-    else:
-        # Bucket by material index.
-
-        tri_material_idxs = np.empty(len(blender_mesh.loop_triangles), dtype=np.uint32)
-        blender_mesh.loop_triangles.foreach_get('material_index', tri_material_idxs)
-        loop_material_idxs = np.repeat(tri_material_idxs, 3)  # material index for every loop
-        unique_material_idxs = np.unique(tri_material_idxs)
-        del tri_material_idxs
-
-        for material_idx in unique_material_idxs:
-            prim_indices[material_idx] = loop_indices[loop_material_idxs == material_idx]
-
-    # Create all the primitives.
-
-    primitives = []
-
-    for material_idx, dot_indices in prim_indices.items():
-        # Extract just dots used by this primitive, deduplicate them, and
-        # calculate indices into this deduplicated list.
-        prim_dots = dots[dot_indices]
-        prim_dots, indices = np.unique(prim_dots, return_inverse=True)
-
-        if len(prim_dots) == 0:
-            continue
-
-        # Now just move all the data for prim_dots into attribute arrays
-
-        attributes = {}
-
-        blender_idxs = prim_dots['vertex_index']
-
-        attributes['POSITION'] = locs[blender_idxs]
-
-        for morph_i, vs in enumerate(morph_locs):
-            attributes['MORPH_POSITION_%d' % morph_i] = vs[blender_idxs]
-
-        if use_normals:
-            normals = np.empty((len(prim_dots), 3), dtype=np.float32)
-            normals[:, 0] = prim_dots['nx']
-            normals[:, 1] = prim_dots['ny']
-            normals[:, 2] = prim_dots['nz']
-            attributes['NORMAL'] = normals
-
-        if use_tangents:
-            tangents = np.empty((len(prim_dots), 4), dtype=np.float32)
-            tangents[:, 0] = prim_dots['tx']
-            tangents[:, 1] = prim_dots['ty']
-            tangents[:, 2] = prim_dots['tz']
-            tangents[:, 3] = prim_dots['tw']
-            attributes['TANGENT'] = tangents
-
-        if use_morph_normals:
-            for morph_i, _ in enumerate(key_blocks):
-                ns = np.empty((len(prim_dots), 3), dtype=np.float32)
-                ns[:, 0] = prim_dots['morph%dnx' % morph_i]
-                ns[:, 1] = prim_dots['morph%dny' % morph_i]
-                ns[:, 2] = prim_dots['morph%dnz' % morph_i]
-                attributes['MORPH_NORMAL_%d' % morph_i] = ns
-
-                if use_morph_tangents:
-                    attributes['MORPH_TANGENT_%d' % morph_i] = __calc_morph_tangents(normals, ns, tangents)
-
-        for tex_coord_i in range(tex_coord_max):
-            uvs = np.empty((len(prim_dots), 2), dtype=np.float32)
-            uvs[:, 0] = prim_dots['uv%dx' % tex_coord_i]
-            uvs[:, 1] = prim_dots['uv%dy' % tex_coord_i]
-            attributes['TEXCOORD_%d' % tex_coord_i] = uvs
-
-        for color_i, _ in enumerate(colors_attributes):
-            colors = np.empty((len(prim_dots), 4), dtype=np.float32)
-            colors[:, 0] = prim_dots['color%dr' % color_i]
-            colors[:, 1] = prim_dots['color%dg' % color_i]
-            colors[:, 2] = prim_dots['color%db' % color_i]
-            colors[:, 3] = prim_dots['color%da' % color_i]
-            attributes['COLOR_%d' % color_i] = {}
-            attributes['COLOR_%d' % color_i]["data"] = colors
-
-            attributes['COLOR_%d' % color_i]["norm"] = colors_types[color_i] == "BYTE_COLOR"
-
-        if skin:
-            joints = [[] for _ in range(num_joint_sets)]
-            weights = [[] for _ in range(num_joint_sets)]
-
-            for vi in blender_idxs:
-                bones = vert_bones[vi]
-                for j in range(0, 4 * num_joint_sets):
-                    if j < len(bones):
-                        joint, weight = bones[j]
-                    else:
-                        joint, weight = 0, 0.0
-                    joints[j//4].append(joint)
-                    weights[j//4].append(weight)
-
-            for i, (js, ws) in enumerate(zip(joints, weights)):
-                attributes['JOINTS_%d' % i] = js
-                attributes['WEIGHTS_%d' % i] = ws
-
-        primitives.append({
-            'attributes': attributes,
-            'indices': indices,
-            'material': material_idx,
-        })
-
-    if export_settings['gltf_loose_edges']:
-        # Find loose edges
-        loose_edges = [e for e in blender_mesh.edges if e.is_loose]
-        blender_idxs = [vi for e in loose_edges for vi in e.vertices]
-
-        if blender_idxs:
-            # Export one glTF vert per unique Blender vert in a loose edge
-            blender_idxs = np.array(blender_idxs, dtype=np.uint32)
-            blender_idxs, indices = np.unique(blender_idxs, return_inverse=True)
-
-            attributes = {}
-
-            attributes['POSITION'] = locs[blender_idxs]
-
-            for morph_i, vs in enumerate(morph_locs):
-                attributes['MORPH_POSITION_%d' % morph_i] = vs[blender_idxs]
-
-            if skin:
-                joints = [[] for _ in range(num_joint_sets)]
-                weights = [[] for _ in range(num_joint_sets)]
-
-                for vi in blender_idxs:
-                    bones = vert_bones[vi]
-                    for j in range(0, 4 * num_joint_sets):
-                        if j < len(bones):
-                            joint, weight = bones[j]
-                        else:
-                            joint, weight = 0, 0.0
-                        joints[j//4].append(joint)
-                        weights[j//4].append(weight)
-
-                for i, (js, ws) in enumerate(zip(joints, weights)):
-                    attributes['JOINTS_%d' % i] = js
-                    attributes['WEIGHTS_%d' % i] = ws
-
-            primitives.append({
-                'attributes': attributes,
-                'indices': indices,
-                'mode': 1,  # LINES
-                'material': 0,
-            })
-
-    if export_settings['gltf_loose_points']:
-        # Find loose points
-        verts_in_edge = set(vi for e in blender_mesh.edges for vi in e.vertices)
-        blender_idxs = [
-            vi for vi, _ in enumerate(blender_mesh.vertices)
-            if vi not in verts_in_edge
-        ]
-
-        if blender_idxs:
-            blender_idxs = np.array(blender_idxs, dtype=np.uint32)
-
-            attributes = {}
-
-            attributes['POSITION'] = locs[blender_idxs]
-
-            for morph_i, vs in enumerate(morph_locs):
-                attributes['MORPH_POSITION_%d' % morph_i] = vs[blender_idxs]
-
-            if skin:
-                joints = [[] for _ in range(num_joint_sets)]
-                weights = [[] for _ in range(num_joint_sets)]
-
-                for vi in blender_idxs:
-                    bones = vert_bones[vi]
-                    for j in range(0, 4 * num_joint_sets):
-                        if j < len(bones):
-                            joint, weight = bones[j]
-                        else:
-                            joint, weight = 0, 0.0
-                        joints[j//4].append(joint)
-                        weights[j//4].append(weight)
-
-                for i, (js, ws) in enumerate(zip(joints, weights)):
-                    attributes['JOINTS_%d' % i] = js
-                    attributes['WEIGHTS_%d' % i] = ws
-
-            primitives.append({
-                'attributes': attributes,
-                'mode': 0,  # POINTS
-                'material': 0,
-            })
-
-    print_console('INFO', 'Primitives created: %d' % len(primitives))
-
-    return primitives
-
-
-def __get_positions(blender_mesh, key_blocks, armature, blender_object, export_settings):
-    locs = np.empty(len(blender_mesh.vertices) * 3, dtype=np.float32)
-    source = key_blocks[0].relative_key.data if key_blocks else blender_mesh.vertices
-    source.foreach_get('co', locs)
-    locs = locs.reshape(len(blender_mesh.vertices), 3)
-
-    morph_locs = []
-    for key_block in key_blocks:
-        vs = np.empty(len(blender_mesh.vertices) * 3, dtype=np.float32)
-        key_block.data.foreach_get('co', vs)
-        vs = vs.reshape(len(blender_mesh.vertices), 3)
-        morph_locs.append(vs)
-
-    # Transform for skinning
-    if armature and blender_object:
-        # apply_matrix = armature.matrix_world.inverted_safe() @ blender_object.matrix_world
-        # loc_transform = armature.matrix_world @ apply_matrix
-
-        loc_transform = blender_object.matrix_world
-        locs[:] = __apply_mat_to_all(loc_transform, locs)
-        for vs in morph_locs:
-            vs[:] = __apply_mat_to_all(loc_transform, vs)
-
-    # glTF stores deltas in morph targets
-    for vs in morph_locs:
-        vs -= locs
-
-    if export_settings[gltf2_blender_export_keys.YUP]:
-        __zup2yup(locs)
-        for vs in morph_locs:
-            __zup2yup(vs)
-
-    return locs, morph_locs
-
-
-def __get_normals(blender_mesh, key_blocks, armature, blender_object, export_settings):
-    """Get normal for each loop."""
-    if key_blocks:
-        normals = key_blocks[0].relative_key.normals_split_get()
-        normals = np.array(normals, dtype=np.float32)
-    else:
-        normals = np.empty(len(blender_mesh.loops) * 3, dtype=np.float32)
-        blender_mesh.calc_normals_split()
-        blender_mesh.loops.foreach_get('normal', normals)
-
-    normals = normals.reshape(len(blender_mesh.loops), 3)
-
-    morph_normals = []
-    for key_block in key_blocks:
-        ns = np.array(key_block.normals_split_get(), dtype=np.float32)
-        ns = ns.reshape(len(blender_mesh.loops), 3)
-        morph_normals.append(ns)
-
-    # Transform for skinning
-    if armature and blender_object:
-        apply_matrix = (armature.matrix_world.inverted_safe() @ blender_object.matrix_world)
-        apply_matrix = apply_matrix.to_3x3().inverted_safe().transposed()
-        normal_transform = armature.matrix_world.to_3x3() @ apply_matrix
-
-        normals[:] = __apply_mat_to_all(normal_transform, normals)
-        __normalize_vecs(normals)
-        for ns in morph_normals:
-            ns[:] = __apply_mat_to_all(normal_transform, ns)
-            __normalize_vecs(ns)
-
-    for ns in [normals, *morph_normals]:
-        # Replace zero normals with the unit UP vector.
-        # Seems to happen sometimes with degenerate tris?
-        is_zero = ~ns.any(axis=1)
-        ns[is_zero, 2] = 1
-
-    # glTF stores deltas in morph targets
-    for ns in morph_normals:
-        ns -= normals
-
-    if export_settings[gltf2_blender_export_keys.YUP]:
-        __zup2yup(normals)
-        for ns in morph_normals:
-            __zup2yup(ns)
-
-    return normals, morph_normals
-
-
-def __get_tangents(blender_mesh, armature, blender_object, export_settings):
-    """Get an array of the tangent for each loop."""
-    tangents = np.empty(len(blender_mesh.loops) * 3, dtype=np.float32)
-    blender_mesh.loops.foreach_get('tangent', tangents)
-    tangents = tangents.reshape(len(blender_mesh.loops), 3)
-
-    # Transform for skinning
-    if armature and blender_object:
-        apply_matrix = armature.matrix_world.inverted_safe() @ blender_object.matrix_world
-        tangent_transform = apply_matrix.to_quaternion().to_matrix()
-        tangents = __apply_mat_to_all(tangent_transform, tangents)
-        __normalize_vecs(tangents)
-
-    if export_settings[gltf2_blender_export_keys.YUP]:
-        __zup2yup(tangents)
-
-    return tangents
-
-
-def __get_bitangent_signs(blender_mesh, armature, blender_object, export_settings):
-    signs = np.empty(len(blender_mesh.loops), dtype=np.float32)
-    blender_mesh.loops.foreach_get('bitangent_sign', signs)
-
-    # Transform for skinning
-    if armature and blender_object:
-        # Bitangent signs should flip when handedness changes
-        # TODO: confirm
-        apply_matrix = armature.matrix_world.inverted_safe() @ blender_object.matrix_world
-        tangent_transform = apply_matrix.to_quaternion().to_matrix()
-        flipped = tangent_transform.determinant() < 0
-        if flipped:
-            signs *= -1
-
-    # No change for Zup -> Yup
-
-    return signs
-
-
-def __calc_morph_tangents(normals, morph_normal_deltas, tangents):
-    # TODO: check if this works
-    morph_tangent_deltas = np.empty((len(normals), 3), dtype=np.float32)
-
-    for i in range(len(normals)):
-        n = Vector(normals[i])
-        morph_n = n + Vector(morph_normal_deltas[i])  # convert back to non-delta
-        t = Vector(tangents[i, :3])
-
-        rotation = morph_n.rotation_difference(n)
-
-        t_morph = Vector(t)
-        t_morph.rotate(rotation)
-        morph_tangent_deltas[i] = t_morph - t  # back to delta
-
-    return morph_tangent_deltas
-
-
-def __get_uvs(blender_mesh, uv_i):
-    layer = blender_mesh.uv_layers[uv_i]
-    uvs = np.empty(len(blender_mesh.loops) * 2, dtype=np.float32)
-    layer.data.foreach_get('uv', uvs)
-    uvs = uvs.reshape(len(blender_mesh.loops), 2)
-
-    # Blender UV space -> glTF UV space
-    # u,v -> u,1-v
-    uvs[:, 1] *= -1
-    uvs[:, 1] += 1
-
-    return uvs
-
-
-def __get_colors(blender_mesh, color_i, blender_color_i):
-    if blender_mesh.color_attributes[blender_color_i].domain == "POINT":
-        colors = np.empty(len(blender_mesh.vertices) * 4, dtype=np.float32) #POINT
-    else:
-        colors = np.empty(len(blender_mesh.loops) * 4, dtype=np.float32) #CORNER
-    blender_mesh.color_attributes[blender_color_i].data.foreach_get('color', colors)
-    colors = colors.reshape(-1, 4)
-    # colors are already linear, no need to switch color space
-    return colors, blender_mesh.color_attributes[blender_color_i].data_type, blender_mesh.color_attributes[blender_color_i].domain
-
-
-def __get_bone_data(blender_mesh, skin, blender_vertex_groups):
-
-    need_neutral_bone = False
-    min_influence = 0.0001
-
-    joint_name_to_index = {joint.name: index for index, joint in enumerate(skin.joints)}
-    group_to_joint = [joint_name_to_index.get(g.name) for g in blender_vertex_groups]
-
-    # List of (joint, weight) pairs for each vert
-    vert_bones = []
-    max_num_influences = 0
-
-    for vertex in blender_mesh.vertices:
-        bones = []
-        if vertex.groups:
-            for group_element in vertex.groups:
-                weight = group_element.weight
-                if weight <= min_influence:
-                    continue
-                try:
-                    joint = group_to_joint[group_element.group]
-                except Exception:
-                    continue
-                if joint is None:
-                    continue
-                bones.append((joint, weight))
-        bones.sort(key=lambda x: x[1], reverse=True)
-        if not bones:
-            # Is not assign to any bone
-            bones = ((len(skin.joints), 1.0),)  # Assign to a joint that will be created later
-            need_neutral_bone = True
-        vert_bones.append(bones)
-        if len(bones) > max_num_influences:
-            max_num_influences = len(bones)
-
-    # How many joint sets do we need? 1 set = 4 influences
-    num_joint_sets = (max_num_influences + 3) // 4
-
-    return vert_bones, num_joint_sets, need_neutral_bone
-
-
-def __zup2yup(array):
-    # x,y,z -> x,z,-y
-    array[:, [1,2]] = array[:, [2,1]]  # x,z,y
-    array[:, 2] *= -1  # x,z,-y
-
-
-def __apply_mat_to_all(matrix, vectors):
-    """Given matrix m and vectors [v1,v2,...], computes [m@v1,m@v2,...]"""
-    # Linear part
-    m = matrix.to_3x3() if len(matrix) == 4 else matrix
-    res = np.matmul(vectors, np.array(m.transposed()))
-    # Translation part
-    if len(matrix) == 4:
-        res += np.array(matrix.translation)
-    return res
-
-
-def __normalize_vecs(vectors):
-    norms = np.linalg.norm(vectors, axis=1, keepdims=True)
-    np.divide(vectors, norms, out=vectors, where=norms != 0)
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
index 1af588b9a..ce2f9a59f 100755
--- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py
@@ -10,32 +10,34 @@ from io_scene_gltf2.io.com import gltf2_io_debug
 from io_scene_gltf2.io.exp import gltf2_io_binary_data
 
 
+
 def gather_primitive_attributes(blender_primitive, export_settings):
     """
-    Gathers the attributes, such as POSITION, NORMAL, TANGENT from a blender primitive.
+    Gathers the attributes, such as POSITION, NORMAL, TANGENT, and all custom attributes 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
 
+    # loop on each attribute extracted
+    # for skinning, all linked attributes (WEIGHTS_ and JOINTS_) need to be calculated
+    # in one shot (because of normalization), so we need to check that it is called only once.
 
-def array_to_accessor(array, component_type, data_type, include_max_and_min=False):
-    dtype = gltf2_io_constants.ComponentType.to_numpy_dtype(component_type)
-    num_elems = gltf2_io_constants.DataType.num_elements(data_type)
+    skin_done = False
+
+    for attribute in blender_primitive["attributes"]:
+        if (attribute.startswith("JOINTS_") or attribute.startswith("WEIGHTS_")) and skin_done is True:
+            continue
+        if attribute.startswith("MORPH_"):
+            continue # Target for morphs will be managed later
+        attributes.update(__gather_attribute(blender_primitive, attribute, export_settings))
+        if (attribute.startswith("JOINTS_") or attribute.startswith("WEIGHTS_")):
+            skin_done = True
+
+    return attributes
 
-    if type(array) is not np.ndarray:
-        array = np.array(array, dtype=dtype)
-        array = array.reshape(len(array) // num_elems, num_elems)
 
-    assert array.dtype == dtype
-    assert array.shape[1] == num_elems
+def array_to_accessor(array, component_type, data_type, include_max_and_min=False):
 
     amax = None
     amin = None
@@ -58,109 +60,6 @@ def array_to_accessor(array, component_type, data_type, include_max_and_min=Fals
         type=data_type,
     )
 
-
-def __gather_position(blender_primitive, export_settings):
-    position = blender_primitive["attributes"]["POSITION"]
-    return {
-        "POSITION": array_to_accessor(
-            position,
-            component_type=gltf2_io_constants.ComponentType.Float,
-            data_type=gltf2_io_constants.DataType.Vec3,
-            include_max_and_min=True
-        )
-    }
-
-
-def __gather_normal(blender_primitive, export_settings):
-    if not export_settings[gltf2_blender_export_keys.NORMALS]:
-        return {}
-    if 'NORMAL' not in blender_primitive["attributes"]:
-        return {}
-    normal = blender_primitive["attributes"]['NORMAL']
-    return {
-        "NORMAL": array_to_accessor(
-            normal,
-            component_type=gltf2_io_constants.ComponentType.Float,
-            data_type=gltf2_io_constants.DataType.Vec3,
-        )
-    }
-
-
-def __gather_tangent(blender_primitive, export_settings):
-    if not export_settings[gltf2_blender_export_keys.TANGENTS]:
-        return {}
-    if 'TANGENT' not in blender_primitive["attributes"]:
-        return {}
-    tangent = blender_primitive["attributes"]['TANGENT']
-    return {
-        "TANGENT": array_to_accessor(
-            tangent,
-            component_type=gltf2_io_constants.ComponentType.Float,
-            data_type=gltf2_io_constants.DataType.Vec4,
-        )
-    }
-
-
-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] = array_to_accessor(
-                tex_coord,
-                component_type=gltf2_io_constants.ComponentType.Float,
-                data_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:
-            colors = blender_primitive["attributes"][color_id]["data"]
-
-            if type(colors) is not np.ndarray:
-                colors = np.array(colors, dtype=np.float32)
-                colors = colors.reshape(len(colors) // 4, 4)
-
-            if blender_primitive["attributes"][color_id]["norm"] is True:
-                comp_type = gltf2_io_constants.ComponentType.UnsignedShort
-
-                # Convert to normalized ushorts
-                colors *= 65535
-                colors += 0.5  # bias for rounding
-                colors = colors.astype(np.uint16)
-
-            else:
-                comp_type = gltf2_io_constants.ComponentType.Float
-
-            attributes[color_id] = gltf2_io.Accessor(
-                buffer_view=gltf2_io_binary_data.BinaryData(colors.tobytes(), gltf2_io_constants.BufferViewTarget.ARRAY_BUFFER),
-                byte_offset=None,
-                component_type=comp_type,
-                count=len(colors),
-                extensions=None,
-                extras=None,
-                max=None,
-                min=None,
-                name=None,
-                normalized=blender_primitive["attributes"][color_id]["norm"],
-                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 = {}
 
@@ -208,8 +107,10 @@ def __gather_skins(blender_primitive, export_settings):
         component_type = gltf2_io_constants.ComponentType.UnsignedShort
         if max(internal_joint) < 256:
             component_type = gltf2_io_constants.ComponentType.UnsignedByte
+        joints = np.array(internal_joint, dtype= gltf2_io_constants.ComponentType.to_numpy_dtype(component_type))
+        joints = joints.reshape(-1, 4)
         joint = array_to_accessor(
-            internal_joint,
+            joints,
             component_type,
             data_type=gltf2_io_constants.DataType.Vec4,
         )
@@ -236,3 +137,48 @@ def __gather_skins(blender_primitive, export_settings):
         attributes[weight_id] = weight
 
     return attributes
+
+
+def __gather_attribute(blender_primitive, attribute, export_settings):
+    data = blender_primitive["attributes"][attribute]
+
+
+    include_max_and_mins = {
+        "POSITION": True
+    }
+
+    if (attribute.startswith("_COLOR") or attribute.startswith("COLOR_")) and blender_primitive["attributes"][attribute]['component_type'] == gltf2_io_constants.ComponentType.UnsignedShort:
+        # Byte Color vertex color, need to normalize
+
+        data['data'] *= 65535
+        data['data'] += 0.5  # bias for rounding
+        data['data'] = data['data'].astype(np.uint16)
+
+        return { attribute : gltf2_io.Accessor(
+                buffer_view=gltf2_io_binary_data.BinaryData(data['data'].tobytes(), gltf2_io_constants.BufferViewTarget.ARRAY_BUFFER),
+                byte_offset=None,
+                component_type=data['component_type'],
+                count=len(data['data']),
+                extensions=None,
+                extras=None,
+                max=None,
+                min=None,
+                name=None,
+                normalized=True,
+                sparse=None,
+                type=data['data_type'],
+            )
+        }
+
+    elif attribute.startswith("JOINTS_") or attribute.startswith("WEIGHTS_"):
+        return __gather_skins(blender_primitive, export_settings)
+
+    else:
+        return {
+            attribute: array_to_accessor(
+                data['data'],
+                component_type=data['component_type'],
+                data_type=data['data_type'],
+                include_max_and_min=include_max_and_mins.get(attribute, False)
+            )
+        }
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py
index 576a1418c..d7784c3e5 100755
--- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py
@@ -8,7 +8,7 @@ import numpy as np
 from .gltf2_blender_export_keys import NORMALS, MORPH_NORMAL, TANGENTS, MORPH_TANGENT, MORPH
 
 from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached, cached_by_key
-from io_scene_gltf2.blender.exp import gltf2_blender_extract
+from io_scene_gltf2.blender.exp import gltf2_blender_gather_primitives_extract
 from io_scene_gltf2.blender.exp import gltf2_blender_gather_accessors
 from io_scene_gltf2.blender.exp import gltf2_blender_gather_primitive_attributes
 from io_scene_gltf2.blender.exp import gltf2_blender_gather_materials
@@ -112,7 +112,7 @@ def __gather_cache_primitives(
     """
     primitives = []
 
-    blender_primitives = gltf2_blender_extract.extract_primitives(
+    blender_primitives = gltf2_blender_gather_primitives_extract.extract_primitives(
         blender_mesh, uuid_for_skined_data, vertex_groups, modifiers, export_settings)
 
     for internal_primitive in blender_primitives:
@@ -184,7 +184,7 @@ def __gather_targets(blender_primitive, blender_mesh, modifiers, export_settings
 
                 if blender_primitive["attributes"].get(target_position_id) is not None:
                     target = {}
-                    internal_target_position = blender_primitive["attributes"][target_position_id]
+                    internal_target_position = blender_primitive["attributes"][target_position_id]["data"]
                     target["POSITION"] = gltf2_blender_gather_primitive_attributes.array_to_accessor(
                         internal_target_position,
                         component_type=gltf2_io_constants.ComponentType.Float,
@@ -196,7 +196,7 @@ def __gather_targets(blender_primitive, blender_mesh, modifiers, export_settings
                             and export_settings[MORPH_NORMAL] \
                             and blender_primitive["attributes"].get(target_normal_id) is not None:
 
-                        internal_target_normal = blender_primitive["attributes"][target_normal_id]
+                        internal_target_normal = blender_primitive["attributes"][target_normal_id]["data"]
                         target['NORMAL'] = gltf2_blender_gather_primitive_attributes.array_to_accessor(
                             internal_target_normal,
                             component_type=gltf2_io_constants.ComponentType.Float,
@@ -206,7 +206,7 @@ def __gather_targets(blender_primitive, blender_mesh, modifiers, export_settings
                     if export_settings[TANGENTS] \
                             and export_settings[MORPH_TANGENT] \
                             and blender_primitive["attributes"].get(target_tangent_id) is not None:
-                        internal_target_tangent = blender_primitive["attributes"][target_tangent_id]
+                        internal_target_tangent = blender_primitive["attributes"][target_tangent_id]["data"]
                         target['TANGENT'] = gltf2_blender_gather_primitive_attributes.array_to_accessor(
                             internal_target_tangent,
                             component_type=gltf2_io_constants.ComponentType.Float,
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives_extract.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives_extract.py
new file mode 100644
index 000000000..74682ce70
--- /dev/null
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives_extract.py
@@ -0,0 +1,863 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright 2018-2021 The glTF-Blender-IO authors.
+
+import numpy as np
+from mathutils import Vector
+
+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
+from io_scene_gltf2.io.com import gltf2_io_constants
+from io_scene_gltf2.blender.com import gltf2_blender_conversion
+
+
+def extract_primitives(blender_mesh, uuid_for_skined_data, blender_vertex_groups, modifiers, export_settings):
+    """Extract primitives from a mesh."""
+    print_console('INFO', 'Extracting primitive: ' + blender_mesh.name)
+
+    primitive_creator = PrimitiveCreator(blender_mesh, uuid_for_skined_data, blender_vertex_groups, modifiers, export_settings)
+    primitive_creator.prepare_data()
+    primitive_creator.define_attributes()
+    primitive_creator.create_dots_data_structure()
+    primitive_creator.populate_dots_data()
+    primitive_creator.primitive_split()
+    return primitive_creator.primitive_creation()
+
+class PrimitiveCreator:
+    def __init__(self, blender_mesh, uuid_for_skined_data, blender_vertex_groups, modifiers, export_settings):
+        self.blender_mesh = blender_mesh
+        self.uuid_for_skined_data = uuid_for_skined_data
+        self.blender_vertex_groups = blender_vertex_groups
+        self.modifiers = modifiers
+        self.export_settings = export_settings
+
+    @classmethod
+    def apply_mat_to_all(cls, matrix, vectors):
+        """Given matrix m and vectors [v1,v2,...], computes [m@v1,m@v2,...]"""
+        # Linear part
+        m = matrix.to_3x3() if len(matrix) == 4 else matrix
+        res = np.matmul(vectors, np.array(m.transposed()))
+        # Translation part
+        if len(matrix) == 4:
+            res += np.array(matrix.translation)
+        return res
+
+    @classmethod
+    def normalize_vecs(cls, vectors):
+        norms = np.linalg.norm(vectors, axis=1, keepdims=True)
+        np.divide(vectors, norms, out=vectors, where=norms != 0)
+
+    @classmethod
+    def zup2yup(cls, array):
+        # x,y,z -> x,z,-y
+        array[:, [1,2]] = array[:, [2,1]]  # x,z,y
+        array[:, 2] *= -1  # x,z,-y
+
+    def prepare_data(self):
+        self.blender_object = None
+        if self.uuid_for_skined_data:
+            self.blender_object = self.export_settings['vtree'].nodes[self.uuid_for_skined_data].blender_object
+
+        self.use_normals = self.export_settings[gltf2_blender_export_keys.NORMALS]
+        if self.use_normals:
+            self.blender_mesh.calc_normals_split()
+
+        self.use_tangents = False
+        if self.use_normals and self.export_settings[gltf2_blender_export_keys.TANGENTS]:
+            if self.blender_mesh.uv_layers.active and len(self.blender_mesh.uv_layers) > 0:
+                try:
+                    self.blender_mesh.calc_tangents()
+                    self.use_tangents = True
+                except Exception:
+                    print_console('WARNING', 'Could not calculate tangents. Please try to triangulate the mesh first.')
+
+        self.tex_coord_max = 0
+        if self.export_settings[gltf2_blender_export_keys.TEX_COORDS]:
+            if self.blender_mesh.uv_layers.active:
+                self.tex_coord_max = len(self.blender_mesh.uv_layers)
+
+        self.use_morph_normals = self.use_normals and self.export_settings[gltf2_blender_export_keys.MORPH_NORMAL]
+        self.use_morph_tangents = self.use_morph_normals and self.use_tangents and self.export_settings[gltf2_blender_export_keys.MORPH_TANGENT]
+
+        self.use_materials = self.export_settings[gltf2_blender_export_keys.MATERIALS]
+
+        self.blender_attributes = []
+
+        # Check if we have to export skin
+        self.armature = None
+        self.skin = None
+        if self.blender_vertex_groups and self.export_settings[gltf2_blender_export_keys.SKINS]:
+            if self.modifiers is not None:
+                modifiers_dict = {m.type: m for m in self.modifiers}
+                if "ARMATURE" in modifiers_dict:
+                    modifier = modifiers_dict["ARMATURE"]
+                    self.armature = modifier.object
+
+            # Skin must be ignored if the object is parented to a bone of the armature
+            # (This creates an infinite recursive error)
+            # So ignoring skin in that case
+            is_child_of_arma = (
+                self.armature and
+                self.blender_object and
+                self.blender_object.parent_type == "BONE" and
+                self.blender_object.parent.name == self.armature.name
+            )
+            if is_child_of_arma:
+                self.armature = None
+
+            if self.armature:
+                self.skin = gltf2_blender_gather_skins.gather_skin(self.export_settings['vtree'].nodes[self.uuid_for_skined_data].armature, self.export_settings)
+                if not self.skin:
+                    self.armature = None
+
+        self.key_blocks = []
+        if self.export_settings[gltf2_blender_export_keys.APPLY] is False and self.blender_mesh.shape_keys and self.export_settings[gltf2_blender_export_keys.MORPH]:
+            self.key_blocks = [
+                key_block
+                for key_block in self.blender_mesh.shape_keys.key_blocks
+                if not (key_block == key_block.relative_key or key_block.mute)
+            ]
+
+        # Fetch vert positions and bone data (joint,weights)
+
+        self.locs = None
+        self.morph_locs = None
+        self.__get_positions()
+
+        if self.skin:
+            self.__get_bone_data()
+            if self.need_neutral_bone is True:
+                # Need to create a fake joint at root of armature
+                # In order to assign not assigned vertices to it
+                # But for now, this is not yet possible, we need to wait the armature node is created
+                # Just store this, to be used later
+                armature_uuid = self.export_settings['vtree'].nodes[self.uuid_for_skined_data].armature
+                self.export_settings['vtree'].nodes[armature_uuid].need_neutral_bone = True
+
+    def define_attributes(self):
+        # Manage attributes + COLOR_0
+        for blender_attribute_index, blender_attribute in enumerate(self.blender_mesh.attributes):
+            attr = {}
+            attr['blender_attribute_index'] = blender_attribute_index
+            attr['blender_name'] = blender_attribute.name
+            attr['blender_domain'] = blender_attribute.domain
+            attr['blender_data_type'] = blender_attribute.data_type
+
+            # For now, we don't export edge data, because I need to find how to 
+            # get from edge data to dots data
+            if attr['blender_domain'] == "EDGE":
+                continue
+
+            # Some type are not exportable (example : String)
+            if gltf2_blender_conversion.get_component_type(blender_attribute.data_type) is None or \
+                gltf2_blender_conversion.get_data_type(blender_attribute.data_type) is None:
+
+                continue
+
+            if self.blender_mesh.color_attributes.find(blender_attribute.name) == self.blender_mesh.color_attributes.render_color_index \
+                and self.blender_mesh.color_attributes.render_color_index != -1:
+
+                if self.export_settings[gltf2_blender_export_keys.COLORS] is False:
+                    continue
+                attr['gltf_attribute_name'] = 'COLOR_0'
+                attr['get'] = self.get_function()
+
+            else:
+                attr['gltf_attribute_name'] = '_' + blender_attribute.name.upper()
+                attr['get'] = self.get_function()
+                if self.export_settings['gltf_attributes'] is False:
+                    continue
+
+            self.blender_attributes.append(attr)
+
+        # Manage POSITION
+        attr = {}
+        attr['blender_data_type'] = 'FLOAT_VECTOR'
+        attr['blender_domain'] = 'POINT'
+        attr['gltf_attribute_name'] = 'POSITION'
+        attr['set'] = self.set_function()
+        attr['skip_getting_to_dots'] = True
+        self.blender_attributes.append(attr)
+
+        # Manage uvs TEX_COORD_x
+        for tex_coord_i in range(self.tex_coord_max):
+            attr = {}
+            attr['blender_data_type'] = 'FLOAT2'
+            attr['blender_domain'] = 'CORNER'
+            attr['gltf_attribute_name'] = 'TEXCOORD_' + str(tex_coord_i)
+            attr['get'] = self.get_function()
+            self.blender_attributes.append(attr)
+
+        # Manage NORMALS
+        if self.use_normals:
+            attr = {}
+            attr['blender_data_type'] = 'FLOAT_VECTOR'
+            attr['blender_domain'] = 'CORNER'
+            attr['gltf_attribute_name'] = 'NORMAL'
+            attr['gltf_attribute_name_morph'] = 'MORPH_NORMAL_'
+            attr['get'] = self.get_function()
+            self.blender_attributes.append(attr)
+
+        # Manage TANGENT
+        if self.use_tangents:
+            attr = {}
+            attr['blender_data_type'] = 'FLOAT_VECTOR_4'
+            attr['blender_domain'] = 'CORNER'
+            attr['gltf_attribute_name'] = 'TANGENT'
+            attr['get'] = self.get_function()
+            self.blender_attributes.append(attr)
+
+        # Manage MORPH_POSITION_x
+        for morph_i, vs in enumerate(self.morph_locs):
+            attr = {}
+            attr['blender_attribute_index'] = morph_i
+            attr['blender_data_type'] = 'FLOAT_VECTOR'
+            attr['blender_domain'] = 'POINT'
+            attr['gltf_attribute_name'] = 'MORPH_POSITION_' + str(morph_i)
+            attr['skip_getting_to_dots'] = True
+            attr['set'] = self.set_function()
+            self.blender_attributes.append(attr)
+
+            # Manage MORPH_NORMAL_x
+            if self.use_morph_normals:
+                attr = {}
+                attr['blender_attribute_index'] = morph_i
+                attr['blender_data_type'] = 'FLOAT_VECTOR'
+                attr['blender_domain'] = 'CORNER'
+                attr['gltf_attribute_name'] = 'MORPH_NORMAL_' + str(morph_i)
+                # No get function is set here, because data are set from NORMALS
+                self.blender_attributes.append(attr)
+
+                # Manage MORPH_TANGENT_x
+                # This is a particular case, where we need to have the following data already calculated
+                # - NORMAL
+                # - MORPH_NORMAL
+                # - TANGENT
+                # So, the following needs to be AFTER the 3 others.
+                if self.use_morph_tangents:
+                    attr = {}
+                    attr['blender_attribute_index'] = morph_i
+                    attr['blender_data_type'] = 'FLOAT_VECTOR'
+                    attr['blender_domain'] = 'CORNER'
+                    attr['gltf_attribute_name'] = 'MORPH_TANGENT_' + str(morph_i)
+                    attr['gltf_attribute_name_normal'] = "NORMAL"
+                    attr['gltf_attribute_name_morph_normal'] = "MORPH_NORMAL_" + str(morph_i)
+                    attr['gltf_attribute_name_tangent'] = "TANGENT"
+                    attr['skip_getting_to_dots'] = True
+                    attr['set'] = self.set_function()
+                    self.blender_attributes.append(attr)
+
+        for attr in self.blender_attributes:
+            attr['len'] = gltf2_blender_conversion.get_data_length(attr['blender_data_type'])
+            attr['type'] = gltf2_blender_conversion.get_numpy_type(attr['blender_data_type'])
+
+    def create_dots_data_structure(self):
+        # Now that we get all attributes that are going to be exported, create numpy array that will store them
+        dot_fields = [('vertex_index', np.uint32)]
+        if self.export_settings['gltf_loose_edges']:
+            dot_fields_edges = [('vertex_index', np.uint32)]
+        if self.export_settings['gltf_loose_points']:
+            dot_fields_points = [('vertex_index', np.uint32)]
+        for attr in self.blender_attributes:
+            if 'skip_getting_to_dots' in attr:
+                continue
+            for i in range(attr['len']):
+                dot_fields.append((attr['gltf_attribute_name'] + str(i), attr['type']))
+                if attr['blender_domain'] != 'POINT':
+                    continue
+                if self.export_settings['gltf_loose_edges']:
+                    dot_fields_edges.append((attr['gltf_attribute_name'] + str(i), attr['type']))
+                if self.export_settings['gltf_loose_points']:
+                    dot_fields_points.append((attr['gltf_attribute_name'] + str(i), attr['type']))
+
+        # In Blender there is both per-vert data, like position, and also per-loop
+        # (loop=corner-of-poly) data, like normals or UVs. glTF only has per-vert
+        # data, so we need to split Blender verts up into potentially-multiple glTF
+        # verts.
+        #
+        # First, we'll collect a "dot" for every loop: a struct that stores all the
+        # attributes at that loop, namely the vertex index (which determines all
+        # per-vert data), and all the per-loop data like UVs, etc.
+        #
+        # Each unique dot will become one unique glTF vert.
+
+        self.dots = np.empty(len(self.blender_mesh.loops), dtype=np.dtype(dot_fields))
+
+        # Find loose edges
+        if self.export_settings['gltf_loose_edges']:
+            loose_edges = [e for e in self.blender_mesh.edges if e.is_loose]
+            self.blender_idxs_edges = [vi for e in loose_edges for vi in e.vertices]
+            self.blender_idxs_edges = np.array(self.blender_idxs_edges, dtype=np.uint32)
+
+            self.dots_edges = np.empty(len(self.blender_idxs_edges), dtype=np.dtype(dot_fields_edges))
+            self.dots_edges['vertex_index'] = self.blender_idxs_edges
+
+        # Find loose points
+        if self.export_settings['gltf_loose_points']:
+            verts_in_edge = set(vi for e in self.blender_mesh.edges for vi in e.vertices)
+            self.blender_idxs_points = [
+                vi for vi, _ in enumerate(self.blender_mesh.vertices)
+                if vi not in verts_in_edge
+            ]
+            self.blender_idxs_points = np.array(self.blender_idxs_points, dtype=np.uint32)
+
+            self.dots_points = np.empty(len(self.blender_idxs_points), dtype=np.dtype(dot_fields_points))
+            self.dots_points['vertex_index'] = self.blender_idxs_points
+
+
+    def populate_dots_data(self):
+        vidxs = np.empty(len(self.blender_mesh.loops))
+        self.blender_mesh.loops.foreach_get('vertex_index', vidxs)
+        self.dots['vertex_index'] = vidxs
+        del vidxs
+
+        for attr in self.blender_attributes:
+            if 'skip_getting_to_dots' in attr:
+                continue
+            if 'get' not in attr:
+                continue
+            attr['get'](attr)
+
+    def primitive_split(self):
+        # Calculate triangles and sort them into primitives.
+
+        self.blender_mesh.calc_loop_triangles()
+        loop_indices = np.empty(len(self.blender_mesh.loop_triangles) * 3, dtype=np.uint32)
+        self.blender_mesh.loop_triangles.foreach_get('loops', loop_indices)
+
+        self.prim_indices = {}  # maps material index to TRIANGLES-style indices into dots
+
+        if self.use_materials == "NONE": # Only for None. For placeholder and export, keep primitives
+            # Put all vertices into one primitive
+            self.prim_indices[-1] = loop_indices
+
+        else:
+            # Bucket by material index.
+
+            tri_material_idxs = np.empty(len(self.blender_mesh.loop_triangles), dtype=np.uint32)
+            self.blender_mesh.loop_triangles.foreach_get('material_index', tri_material_idxs)
+            loop_material_idxs = np.repeat(tri_material_idxs, 3)  # material index for every loop
+            unique_material_idxs = np.unique(tri_material_idxs)
+            del tri_material_idxs
+
+            for material_idx in unique_material_idxs:
+                self.prim_indices[material_idx] = loop_indices[loop_material_idxs == material_idx]
+
+    def primitive_creation(self):
+        primitives = []
+
+        for material_idx, dot_indices in self.prim_indices.items():
+            # Extract just dots used by this primitive, deduplicate them, and
+            # calculate indices into this deduplicated list.
+            self.prim_dots = self.dots[dot_indices]
+            self.prim_dots, indices = np.unique(self.prim_dots, return_inverse=True)
+
+            if len(self.prim_dots) == 0:
+                continue
+
+            # Now just move all the data for prim_dots into attribute arrays
+
+            self.attributes = {}
+
+            self.blender_idxs = self.prim_dots['vertex_index']
+
+            for attr in self.blender_attributes:
+                if 'set' in attr:
+                    attr['set'](attr)
+                else: # Regular case
+                    self.__set_regular_attribute(attr)
+                
+            if self.skin:
+                joints = [[] for _ in range(self.num_joint_sets)]
+                weights = [[] for _ in range(self.num_joint_sets)]
+
+                for vi in self.blender_idxs:
+                    bones = self.vert_bones[vi]
+                    for j in range(0, 4 * self.num_joint_sets):
+                        if j < len(bones):
+                            joint, weight = bones[j]
+                        else:
+                            joint, weight = 0, 0.0
+                        joints[j//4].append(joint)
+                        weights[j//4].append(weight)
+
+                for i, (js, ws) in enumerate(zip(joints, weights)):
+                    self.attributes['JOINTS_%d' % i] = js
+                    self.attributes['WEIGHTS_%d' % i] = ws
+
+            primitives.append({
+                'attributes': self.attributes,
+                'indices': indices,
+                'material': material_idx
+            })
+
+        if self.export_settings['gltf_loose_edges']:
+
+            if self.blender_idxs_edges.shape[0] > 0:
+                # Export one glTF vert per unique Blender vert in a loose edge
+                self.blender_idxs = self.blender_idxs_edges
+                dots_edges, indices = np.unique(self.dots_edges, return_inverse=True)
+                self.blender_idxs = np.unique(self.blender_idxs_edges)
+
+                self.attributes = {}
+
+                for attr in self.blender_attributes:
+                    if attr['blender_domain'] != 'POINT':
+                        continue
+                    if 'set' in attr:
+                        attr['set'](attr)
+                    else:
+                        res = np.empty((len(dots_edges), attr['len']), dtype=attr['type'])
+                        for i in range(attr['len']):
+                            res[:, i] = dots_edges[attr['gltf_attribute_name'] + str(i)]
+                        self.attributes[attr['gltf_attribute_name']] = {}
+                        self.attributes[attr['gltf_attribute_name']]["data"] = res
+                        self.attributes[attr['gltf_attribute_name']]["component_type"] = gltf2_blender_conversion.get_component_type(attr['blender_data_type'])
+                        self.attributes[attr['gltf_attribute_name']]["data_type"] = gltf2_blender_conversion.get_data_type(attr['blender_data_type'])
+
+
+                if self.skin:
+                    joints = [[] for _ in range(self.num_joint_sets)]
+                    weights = [[] for _ in range(self.num_joint_sets)]
+
+                    for vi in self.blender_idxs:
+                        bones = self.vert_bones[vi]
+                        for j in range(0, 4 * self.num_joint_sets):
+                            if j < len(bones):
+                                joint, weight = bones[j]
+                            else:
+                                joint, weight = 0, 0.0
+                            joints[j//4].append(joint)
+                            weights[j//4].append(weight)
+
+                    for i, (js, ws) in enumerate(zip(joints, weights)):
+                        self.attributes['JOINTS_%d' % i] = js
+                        self.attributes['WEIGHTS_%d' % i] = ws
+
+                primitives.append({
+                    'attributes': self.attributes,
+                    'indices': indices,
+                    'mode': 1,  # LINES
+                    'material': 0
+                })
+
+        if self.export_settings['gltf_loose_points']:
+
+            if self.blender_idxs_points.shape[0] > 0:
+                self.blender_idxs = self.blender_idxs_points
+
+                self.attributes = {}
+
+                for attr in self.blender_attributes:
+                    if attr['blender_domain'] != 'POINT':
+                        continue
+                    if 'set' in attr:
+                        attr['set'](attr)
+                    else:
+                        res = np.empty((len(self.blender_idxs), attr['len']), dtype=attr['type'])
+                        for i in range(attr['len']):
+                            res[:, i] = self.dots_points[attr['gltf_attribute_name'] + str(i)]
+                        self.attributes[attr['gltf_attribute_name']] = {}
+                        self.attributes[attr['gltf_attribute_name']]["data"] = res
+                        self.attributes[attr['gltf_attribute_name']]["component_type"] = gltf2_blender_conversion.get_component_type(attr['blender_data_type'])
+                        self.attributes[attr['gltf_attribute_name']]["data_type"] = gltf2_blender_conversion.get_data_type(attr['blender_data_type'])
+
+
+                if self.skin:
+                    joints = [[] for _ in range(self.num_joint_sets)]
+                    weights = [[] for _ in range(self.num_joint_sets)]
+
+                    for vi in self.blender_idxs:
+                        bones = self.vert_bones[vi]
+                        for j in range(0, 4 * self.num_joint_sets):
+                            if j < len(bones):
+                                joint, weight = bones[j]
+                            else:
+                                joint, weight = 0, 0.0
+                            joints[j//4].append(joint)
+                            weights[j//4].append(weight)
+
+                    for i, (js, ws) in enumerate(zip(joints, weights)):
+                        self.attributes['JOINTS_%d' % i] = js
+                        self.attributes['WEIGHTS_%d' % i] = ws
+
+                primitives.append({
+                    'attributes': self.attributes,
+                    'mode': 0,  # POINTS
+                    'material': 0
+                })
+
+        print_console('INFO', 'Primitives created: %d' % len(primitives))
+
+        return primitives
+
+################################## Get ##################################################
+
+    def __get_positions(self):
+        self.locs = np.empty(len(self.blender_mesh.vertices) * 3, dtype=np.float32)
+        source = self.key_blocks[0].relative_key.data if self.key_blocks else self.blender_mesh.vertices
+        source.foreach_get('co', self.locs)
+        self.locs = self.locs.reshape(len(self.blender_mesh.vertices), 3)
+
+        self.morph_locs = []
+        for key_block in self.key_blocks:
+            vs = np.empty(len(self.blender_mesh.vertices) * 3, dtype=np.float32)
+            key_block.data.foreach_get('co', vs)
+            vs = vs.reshape(len(self.blender_mesh.vertices), 3)
+            self.morph_locs.append(vs)
+
+        # Transform for skinning
+        if self.armature and self.blender_object:
+            # apply_matrix = armature.matrix_world.inverted_safe() @ blender_object.matrix_world
+            # loc_transform = armature.matrix_world @ apply_matrix
+
+            loc_transform = self.blender_object.matrix_world
+            self.locs[:] = PrimitiveCreator.apply_mat_to_all(loc_transform, self.locs)
+            for vs in self.morph_locs:
+                vs[:] = PrimitiveCreator.apply_mat_to_all(loc_transform, vs)
+
+        # glTF stores deltas in morph targets
+        for vs in self.morph_locs:
+            vs -= self.locs
+
+        if self.export_settings[gltf2_blender_export_keys.YUP]:
+            PrimitiveCreator.zup2yup(self.locs)
+            for vs in self.morph_locs:
+                PrimitiveCreator.zup2yup(vs)
+
+    def get_function(self):
+
+        def getting_function(attr):
+            if attr['gltf_attribute_name'] == "COLOR_0":
+                self.__get_color_attribute(attr)
+            elif attr['gltf_attribute_name'].startswith("_"):
+                self.__get_layer_attribute(attr)
+            elif attr['gltf_attribute_name'].startswith("TEXCOORD_"):
+                self.__get_uvs_attribute(int(attr['gltf_attribute_name'].split("_")[-1]), attr)
+            elif attr['gltf_attribute_name'] == "NORMAL":
+                self.__get_normal_attribute(attr)
+            elif attr['gltf_attribute_name'] == "TANGENT":
+                self.__get_tangent_attribute(attr)
+            
+        return getting_function
+
+
+    def __get_color_attribute(self, attr):
+        blender_color_idx = self.blender_mesh.color_attributes.render_color_index
+
+        if attr['blender_domain'] == "POINT":
+            colors = np.empty(len(self.blender_mesh.vertices) * 4, dtype=np.float32)
+        elif attr['blender_domain'] == "CORNER":
+            colors = np.empty(len(self.blender_mesh.loops) * 4, dtype=np.float32)
+        self.blender_mesh.color_attributes[blender_color_idx].data.foreach_get('color', colors)
+        if attr['blender_domain'] == "POINT":
+            colors = colors.reshape(-1, 4)
+            colors = colors[self.dots['vertex_index']]
+        elif attr['blender_domain'] == "CORNER":
+            colors = colors.reshape(-1, 4)
+        # colors are already linear, no need to switch color space
+        self.dots[attr['gltf_attribute_name'] + '0'] = colors[:, 0]
+        self.dots[attr['gltf_attribute_name'] + '1'] = colors[:, 1]
+        self.dots[attr['gltf_attribute_name'] + '2'] = colors[:, 2]
+        self.dots[attr['gltf_attribute_name'] + '3'] = colors[:, 3]
+        del colors
+
+
+    def __get_layer_attribute(self, attr):
+        if attr['blender_domain'] in ['CORNER']:
+            data = np.empty(len(self.blender_mesh.loops) * attr['len'], dtype=attr['type'])
+        elif attr['blender_domain'] in ['POINT']:
+            data = np.empty(len(self.blender_mesh.vertices) * attr['len'], dtype=attr['type'])
+        elif attr['blender_domain'] in ['EDGE']:
+            data = np.empty(len(self.blender_mesh.edges) * attr['len'], dtype=attr['type'])
+        elif attr['blender_domain'] in ['FACE']:
+            data = np.empty(len(self.blender_mesh.polygons) * attr['len'], dtype=attr['type'])
+        else:
+            print_console("ERROR", "domain not known")
+
+        if attr['blender_data_type'] == "BYTE_COLOR":
+            self.blender_mesh.attributes[attr['blender_attribute_index']].data.foreach_get('color', data)
+            data = data.reshape(-1, attr['len'])
+        elif attr['blender_data_type'] == "INT8":
+            self.blender_mesh.attributes[attr['blender_attribute_index']].data.foreach_get('value', data)
+            data = data.reshape(-1, attr['len'])
+        elif attr['blender_data_type'] == "FLOAT2":
+            self.blender_mesh.attributes[attr['blender_attribute_index']].data.foreach_get('vector', data)
+            data = data.reshape(-1, attr['len'])
+        elif attr['blender_data_type'] == "BOOLEAN":
+            self.blender_mesh.attributes[attr['blender_attribute_index']].data.foreach_get('value', data)
+            data = data.reshape(-1, attr['len'])
+        elif attr['blender_data_type'] == "STRING":
+            self.blender_mesh.attributes[attr['blender_attribute_index']].data.foreach_get('value', data)
+            data = data.reshape(-1, attr['len'])
+        elif attr['blender_data_type'] == "FLOAT_COLOR":
+            self.blender_mesh.attributes[attr['blender_attribute_index']].data.foreach_get('color', data)
+            data = data.reshape(-1, attr['len'])
+        elif attr['blender_data_type'] == "FLOAT_VECTOR":
+            self.blender_mesh.attributes[attr['blender_attribute_index']].data.foreach_get('vector', data)
+            data = data.reshape(-1, attr['len'])
+        elif attr['blender_data_type'] == "FLOAT_VECTOR_4": # Specific case for tangent
+            pass
+        elif attr['blender_data_type'] == "INT":
+            self.blender_mesh.attributes[attr['blender_attribute_index']].data.foreach_get('value', data)
+            data = data.reshape(-1, attr['len'])
+        elif attr['blender_data_type'] == "FLOAT":
+            self.blender_mesh.attributes[attr['blender_attribute_index']].data.foreach_get('value', data)
+            data = data.reshape(-1, attr['len'])
+        else:
+            print_console('ERROR',"blender type not found " +  attr['blender_data_type'])
+
+        if attr['blender_domain'] in ['CORNER']:
+            for i in range(attr['len']):
+                self.dots[attr['gltf_attribute_name'] + str(i)] = data[:, i]
+        elif attr['blender_domain'] in ['POINT']:
+            if attr['len'] > 1:
+                data = data.reshape(-1, attr['len'])
+            data_dots = data[self.dots['vertex_index']]
+            if self.export_settings['gltf_loose_edges']:
+                data_dots_edges = data[self.dots_edges['vertex_index']]
+            if self.export_settings['gltf_loose_points']:
+                data_dots_points = data[self.dots_points['vertex_index']]
+            for i in range(attr['len']):
+                self.dots[attr['gltf_attribute_name'] + str(i)] = data_dots[:, i]
+                if self.export_settings['gltf_loose_edges']:
+                    self.dots_edges[attr['gltf_attribute_name'] + str(i)] = data_dots_edges[:, i]
+                if self.export_settings['gltf_loose_points']:
+                    self.dots_points[attr['gltf_attribute_name'] + str(i)] = data_dots_points[:, i]
+        elif attr['blender_domain'] in ['EDGE']:
+            # No edge attribute exports
+            pass
+        elif attr['blender_domain'] in ['FACE']:
+            if attr['len'] > 1:
+                data = data.reshape(-1, attr['len'])
+            data = data.repeat(4, axis=0)
+            for i in range(attr['len']):
+                self.dots[attr['gltf_attribute_name'] + str(i)] = data[:, i]
+
+        else:
+            print_console("ERROR", "domain not known")
+
+    def __get_uvs_attribute(self, blender_uv_idx, attr):
+        layer = self.blender_mesh.uv_layers[blender_uv_idx]
+        uvs = np.empty(len(self.blender_mesh.loops) * 2, dtype=np.float32)
+        layer.data.foreach_get('uv', uvs)
+        uvs = uvs.reshape(len(self.blender_mesh.loops), 2)
+
+        # Blender UV space -> glTF UV space
+        # u,v -> u,1-v
+        uvs[:, 1] *= -1
+        uvs[:, 1] += 1
+
+        self.dots[attr['gltf_attribute_name'] + '0'] = uvs[:, 0]
+        self.dots[attr['gltf_attribute_name'] + '1'] = uvs[:, 1]
+        del uvs
+
+    def __get_normals(self):
+        """Get normal for each loop."""
+        key_blocks = self.key_blocks if self.use_morph_normals else []
+        if key_blocks:
+            self.normals = key_blocks[0].relative_key.normals_split_get()
+            self.normals = np.array(self.normals, dtype=np.float32)
+        else:
+            self.normals = np.empty(len(self.blender_mesh.loops) * 3, dtype=np.float32)
+            self.blender_mesh.calc_normals_split()
+            self.blender_mesh.loops.foreach_get('normal', self.normals)
+
+        self.normals = self.normals.reshape(len(self.blender_mesh.loops), 3)
+
+        self.morph_normals = []
+        for key_block in key_blocks:
+            ns = np.array(key_block.normals_split_get(), dtype=np.float32)
+            ns = ns.reshape(len(self.blender_mesh.loops), 3)
+            self.morph_normals.append(ns)
+
+        # Transform for skinning
+        if self.armature and self.blender_object:
+            apply_matrix = (self.armature.matrix_world.inverted_safe() @ self.blender_object.matrix_world)
+            apply_matrix = apply_matrix.to_3x3().inverted_safe().transposed()
+            normal_transform = self.armature.matrix_world.to_3x3() @ apply_matrix
+
+            self.normals[:] = PrimitiveCreator.apply_mat_to_all(normal_transform, self.normals)
+            PrimitiveCreator.normalize_vecs(self.normals)
+            for ns in self.morph_normals:
+                ns[:] = PrimitiveCreator.apply_mat_to_all(normal_transform, ns)
+                PrimitiveCreator.normalize_vecs(ns)
+
+        for ns in [self.normals, *self.morph_normals]:
+            # Replace zero normals with the unit UP vector.
+            # Seems to happen sometimes with degenerate tris?
+            is_zero = ~ns.any(axis=1)
+            ns[is_zero, 2] = 1
+
+        # glTF stores deltas in morph targets
+        for ns in self.morph_normals:
+            ns -= self.normals
+
+        if self.export_settings[gltf2_blender_export_keys.YUP]:
+            PrimitiveCreator.zup2yup(self.normals)
+            for ns in self.morph_normals:
+                PrimitiveCreator.zup2yup(ns)
+
+    def __get_normal_attribute(self, attr):
+        self.__get_normals()
+        self.dots[attr['gltf_attribute_name'] + "0"] = self.normals[:, 0]
+        self.dots[attr['gltf_attribute_name'] + "1"] = self.normals[:, 1]
+        self.dots[attr['gltf_attribute_name'] + "2"] = self.normals[:, 2]
+
+        if self.use_morph_normals:
+            for morph_i, ns in enumerate(self.morph_normals):
+                self.dots[attr['gltf_attribute_name_morph'] + str(morph_i) + "0"] = ns[:, 0]
+                self.dots[attr['gltf_attribute_name_morph'] + str(morph_i) + "1"] = ns[:, 1]
+                self.dots[attr['gltf_attribute_name_morph'] + str(morph_i) + "2"] = ns[:, 2]
+            del self.normals
+            del self.morph_normals
+
+    def __get_tangent_attribute(self, attr):
+        self.__get_tangents()
+        self.dots[attr['gltf_attribute_name'] + "0"] = self.tangents[:, 0]
+        self.dots[attr['gltf_attribute_name'] + "1"] = self.tangents[:, 1]
+        self.dots[attr['gltf_attribute_name'] + "2"] = self.tangents[:, 2]
+        del self.tangents
+        self.__get_bitangent_signs()
+        self.dots[attr['gltf_attribute_name'] + "3"] = self.signs
+        del self.signs
+
+    def __get_tangents(self):
+        """Get an array of the tangent for each loop."""
+        self.tangents = np.empty(len(self.blender_mesh.loops) * 3, dtype=np.float32)
+        self.blender_mesh.loops.foreach_get('tangent', self.tangents)
+        self.tangents = self.tangents.reshape(len(self.blender_mesh.loops), 3)
+
+        # Transform for skinning
+        if self.armature and self.blender_object:
+            apply_matrix = self.armature.matrix_world.inverted_safe() @ self.blender_object.matrix_world
+            tangent_transform = apply_matrix.to_quaternion().to_matrix()
+            self.tangents = PrimitiveCreator.apply_mat_to_all(tangent_transform, self.tangents)
+            PrimitiveCreator.normalize_vecs(self.tangents)
+
+        if self.export_settings[gltf2_blender_export_keys.YUP]:
+            PrimitiveCreator.zup2yup(self.tangents)
+
+
+    def __get_bitangent_signs(self):
+        self.signs = np.empty(len(self.blender_mesh.loops), dtype=np.float32)
+        self.blender_mesh.loops.foreach_get('bitangent_sign', signs)
+
+        # Transform for skinning
+        if self.armature and self.blender_object:
+            # Bitangent signs should flip when handedness changes
+            # TODO: confirm
+            apply_matrix = self.armature.matrix_world.inverted_safe() @ self.blender_object.matrix_world
+            tangent_transform = apply_matrix.to_quaternion().to_matrix()
+            flipped = tangent_transform.determinant() < 0
+            if flipped:
+                signs *= -1
+
+        # No change for Zup -> Yup
+
+
+    def __get_bone_data(self):
+
+        self.need_neutral_bone = False
+        min_influence = 0.0001
+
+        joint_name_to_index = {joint.name: index for index, joint in enumerate(self.skin.joints)}
+        group_to_joint = [joint_name_to_index.get(g.name) for g in self.blender_vertex_groups]
+
+        # List of (joint, weight) pairs for each vert
+        self.vert_bones = []
+        max_num_influences = 0
+
+        for vertex in self.blender_mesh.vertices:
+            bones = []
+            if vertex.groups:
+                for group_element in vertex.groups:
+                    weight = group_element.weight
+                    if weight <= min_influence:
+                        continue
+                    try:
+                        joint = group_to_joint[group_element.group]
+                    except Exception:
+                        continue
+                    if joint is None:
+                        continue
+                    bones.append((joint, weight))
+            bones.sort(key=lambda x: x[1], reverse=True)
+            if not bones:
+                # Is not assign to any bone
+                bones = ((len(self.skin.joints), 1.0),)  # Assign to a joint that will be created later
+                self.need_neutral_bone = True
+            self.vert_bones.append(bones)
+            if len(bones) > max_num_influences:
+                max_num_influences = len(bones)
+
+        # How many joint sets do we need? 1 set = 4 influences
+        self.num_joint_sets = (max_num_influences + 3) // 4
+
+##################################### Set ###################################
+    def set_function(self):
+
+        def setting_function(attr):
+            if attr['gltf_attribute_name'] == "POSITION":
+                self.__set_positions_attribute(attr)
+            elif attr['gltf_attribute_name'].startswith("MORPH_POSITION_"):
+                self.__set_morph_locs_attribute(attr)
+            elif attr['gltf_attribute_name'].startswith("MORPH_TANGENT_"):
+                self.__set_morph_tangent_attribute(attr)
+
+        return setting_function
+
+    def __set_positions_attribute(self, attr):
+        self.attributes[attr['gltf_attribute_name']] = {}
+        self.attributes[attr['gltf_attribute_name']]["data"] = self.locs[self.blender_idxs]
+        self.attributes[attr['gltf_attribute_name']]["data_type"] = gltf2_io_constants.DataType.Vec3
+        self.attributes[attr['gltf_attribute_name']]["component_type"] = gltf2_io_constants.ComponentType.Float
+
+
+    def __set_morph_locs_attribute(self, attr):
+        self.attributes[attr['gltf_attribute_name']] = {}
+        self.attributes[attr['gltf_attribute_name']]["data"] = self.morph_locs[attr['blender_attribute_index']][self.blender_idxs]
+
+    def __set_morph_tangent_attribute(self, attr):
+        # Morph tangent are after these 3 others, so, they are already calculated
+        self.normals = self.attributes[attr['gltf_attribute_name_normal']]["data"]
+        self.morph_normals = self.attributes[attr['gltf_attribute_name_morph_normal']]["data"]
+        self.tangent = self.attributes[attr['gltf_attribute_name_tangent']]["data"]
+
+        self.__calc_morph_tangents()
+        self.attributes[attr['gltf_attribute_name']] = {}
+        self.attributes[attr['gltf_attribute_name']]["data"] = self.morph_tangents
+
+    def __calc_morph_tangents(self):
+        # TODO: check if this works
+        self.morph_tangent_deltas = np.empty((len(self.normals), 3), dtype=np.float32)
+
+        for i in range(len(self.normals)):
+            n = Vector(self.normals[i])
+            morph_n = n + Vector(self.morph_normal_deltas[i])  # convert back to non-delta
+            t = Vector(self.tangents[i, :3])
+
+            rotation = morph_n.rotation_difference(n)
+
+            t_morph = Vector(t)
+            t_morph.rotate(rotation)
+            self.morph_tangent_deltas[i] = t_morph - t  # back to delta
+
+    def __set_regular_attribute(self, attr):
+            res = np.empty((len(self.prim_dots), attr['len']), dtype=attr['type'])
+            for i in range(attr['len']):
+                res[:, i] = self.prim_dots[attr['gltf_attribute_name'] + str(i)]
+            self.attributes[attr['gltf_attribute_name']] = {}
+            self.attributes[attr['gltf_attribute_name']]["data"] = res
+            if 'gltf_attribute_name' == "NORMAL":
+                self.attributes[attr['gltf_attribute_name']]["component_type"] = gltf2_io_constants.ComponentType.Float
+                self.attributes[attr['gltf_attribute_name']]["data_type"] = gltf2_io_constants.DataType.Vec3
+            elif 'gltf_attribute_name' == "TANGENT":
+                self.attributes[attr['gltf_attribute_name']]["component_type"] = gltf2_io_constants.ComponentType.Float
+                self.attributes[attr['gltf_attribute_name']]["data_type"] = gltf2_io_constants.DataType.Vec4
+            elif attr['gltf_attribute_name'].startswith('TEXCOORD_'):
+                self.attributes[attr['gltf_attribute_name']]["component_type"] = gltf2_io_constants.ComponentType.Float
+                self.attributes[attr['gltf_attribute_name']]["data_type"] = gltf2_io_constants.DataType.Vec2
+            else:
+                self.attributes[attr['gltf_attribute_name']]["component_type"] = gltf2_blender_conversion.get_component_type(attr['blender_data_type'])
+                self.attributes[attr['gltf_attribute_name']]["data_type"] = gltf2_blender_conversion.get_data_type(attr['blender_data_type'])
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_tree.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_tree.py
index cfeb70e24..dc25b417c 100644
--- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_tree.py
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_tree.py
@@ -11,7 +11,6 @@ from mathutils import Quaternion, Matrix
 from io_scene_gltf2.io.com import gltf2_io
 from io_scene_gltf2.io.imp.gltf2_io_binary import BinaryData
 from io_scene_gltf2.io.com import gltf2_io_constants
-from .gltf2_blender_gather_primitive_attributes import array_to_accessor
 from io_scene_gltf2.io.exp import gltf2_io_binary_data
 from io_scene_gltf2.blender.exp import gltf2_blender_gather_accessors
 
-- 
GitLab