From 566a68e2083a2a5d7ad66157dc9b79e410bd038c Mon Sep 17 00:00:00 2001
From: Julien Duroure <julien.duroure@gmail.com>
Date: Sat, 5 Sep 2020 15:19:55 +0200
Subject: [PATCH] glTF exporter: perf: use numpy to speedup primitive extract

---
 io_scene_gltf2/__init__.py                    |   2 +-
 .../blender/exp/gltf2_blender_extract.py      | 710 ++++++++++--------
 .../blender/exp/gltf2_blender_gather_nodes.py |  43 +-
 ...tf2_blender_gather_primitive_attributes.py |   8 +-
 .../exp/gltf2_blender_gather_primitives.py    |  15 +-
 5 files changed, 448 insertions(+), 330 deletions(-)

diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py
index d3a247dea..daa6c7e42 100755
--- a/io_scene_gltf2/__init__.py
+++ b/io_scene_gltf2/__init__.py
@@ -15,7 +15,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": (1, 4, 8),
+    "version": (1, 4, 9),
     'blender': (2, 90, 0),
     'location': 'File > Import-Export',
     'description': 'Import-Export as glTF 2.0',
diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_extract.py b/io_scene_gltf2/blender/exp/gltf2_blender_extract.py
index eef05044b..ca38aa725 100755
--- a/io_scene_gltf2/blender/exp/gltf2_blender_extract.py
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_extract.py
@@ -12,128 +12,18 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-#
-# Imports
-#
-
+import numpy as np
 from mathutils import Vector, Quaternion, Matrix
 
 from . import gltf2_blender_export_keys
 from ...io.com.gltf2_io_debug import print_console
-from ...io.com.gltf2_io_color_management import color_srgb_to_scene_linear
 from io_scene_gltf2.blender.exp import gltf2_blender_gather_skins
 
 
-#
-# Classes
-#
-
-class Prim:
-    def __init__(self):
-        self.verts = {}
-        self.indices = []
-
-class ShapeKey:
-    def __init__(self, shape_key, split_normals):
-        self.shape_key = shape_key
-        self.split_normals = split_normals
-
-
-#
-# Functions
-#
-
-def convert_swizzle_normal(loc, armature, blender_object, export_settings):
-    """Convert a normal data from Blender coordinate system to glTF coordinate system."""
-    if (not armature) or (not blender_object):
-        # Classic case. Mesh is not skined, no need to apply armature transfoms on vertices / normals / tangents
-        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]))
-    else:
-        # Mesh is skined, we have to apply armature transforms on data
-        apply_matrix = (armature.matrix_world.inverted() @ blender_object.matrix_world).to_3x3().inverted()
-        apply_matrix.transpose()
-        new_loc = ((armature.matrix_world.to_3x3() @ apply_matrix).to_4x4() @ Matrix.Translation(Vector((loc[0], loc[1], loc[2])))).to_translation()
-        new_loc.normalize()
-
-        if export_settings[gltf2_blender_export_keys.YUP]:
-            return Vector((new_loc[0], new_loc[2], -new_loc[1]))
-        else:
-            return Vector((new_loc[0], new_loc[1], new_loc[2]))
-
-def convert_swizzle_location(loc, armature, blender_object, export_settings):
-    """Convert a location from Blender coordinate system to glTF coordinate system."""
-    if (not armature) or (not blender_object):
-        # Classic case. Mesh is not skined, no need to apply armature transfoms on vertices / normals / tangents
-        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]))
-    else:
-        # Mesh is skined, we have to apply armature transforms on data
-        apply_matrix = armature.matrix_world.inverted() @ blender_object.matrix_world
-        new_loc = (armature.matrix_world @ apply_matrix @ Matrix.Translation(Vector((loc[0], loc[1], loc[2])))).to_translation()
-
-        if export_settings[gltf2_blender_export_keys.YUP]:
-            return Vector((new_loc[0], new_loc[2], -new_loc[1]))
-        else:
-            return Vector((new_loc[0], new_loc[1], new_loc[2]))
-
-
-def convert_swizzle_tangent(tan, armature, blender_object, 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 (not armature) or (not blender_object):
-        # Classic case. Mesh is not skined, no need to apply armature transfoms on vertices / normals / tangents
-        if export_settings[gltf2_blender_export_keys.YUP]:
-            return Vector((tan[0], tan[2], -tan[1]))
-        else:
-            return Vector((tan[0], tan[1], tan[2]))
-    else:
-        # Mesh is skined, we have to apply armature transforms on data
-        apply_matrix = armature.matrix_world.inverted() @ blender_object.matrix_world
-        new_tan = apply_matrix.to_quaternion() @ Vector((tan[0], tan[1], tan[2]))
-        if export_settings[gltf2_blender_export_keys.YUP]:
-            return Vector((new_tan[0], new_tan[2], -new_tan[1]))
-        else:
-            return Vector((new_tan[0], new_tan[1], new_tan[2]))
-
-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 extract_primitives(glTF, blender_mesh, library, blender_object, blender_vertex_groups, modifiers, export_settings):
-    """
-    Extract primitives from a mesh. Polygons are triangulated and sorted by material.
-    Vertices in multiple faces get split up as necessary.
-    """
+    """Extract primitives from a mesh."""
     print_console('INFO', 'Extracting primitive: ' + blender_mesh.name)
 
-    #
-    # First, decide what attributes to gather (eg. how many COLOR_n, etc.)
-    # Also calculate normals/tangents now if necessary.
-    #
-
     use_normals = export_settings[gltf2_blender_export_keys.NORMALS]
     if use_normals:
         blender_mesh.calc_normals_split()
@@ -156,8 +46,8 @@ def extract_primitives(glTF, blender_mesh, library, blender_object, blender_vert
     if export_settings[gltf2_blender_export_keys.COLORS]:
         color_max = len(blender_mesh.vertex_colors)
 
-    bone_max = 0  # number of JOINTS_n sets needed (1 set = 4 influences)
     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}
@@ -181,191 +71,201 @@ def extract_primitives(glTF, blender_mesh, library, blender_object, blender_vert
             skin = gltf2_blender_gather_skins.gather_skin(armature, export_settings)
             if not skin:
                 armature = None
-            else:
-                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]
-
-                # Find out max number of bone influences
-                for blender_polygon in blender_mesh.polygons:
-                    for loop_index in blender_polygon.loop_indices:
-                        vertex_index = blender_mesh.loops[loop_index].vertex_index
-                        groups_count = len(blender_mesh.vertices[vertex_index].groups)
-                        bones_count = (groups_count + 3) // 4
-                        bone_max = max(bone_max, bones_count)
 
     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]
 
-    shape_keys = []
+    key_blocks = []
     if blender_mesh.shape_keys and export_settings[gltf2_blender_export_keys.MORPH]:
-        for blender_shape_key in blender_mesh.shape_keys.key_blocks:
-            if blender_shape_key == blender_shape_key.relative_key or blender_shape_key.mute:
-                continue
+        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)
+        ]
 
-            split_normals = None
-            if use_morph_normals:
-                split_normals = blender_shape_key.normals_split_get()
-
-            shape_keys.append(ShapeKey(
-                blender_shape_key,
-                split_normals,
-            ))
+    use_materials = export_settings[gltf2_blender_export_keys.MATERIALS]
 
+    # Fetch vert positions and bone data (joint,weights)
 
-    use_materials = export_settings[gltf2_blender_export_keys.MATERIALS]
+    locs, morph_locs = __get_positions(blender_mesh, key_blocks, armature, blender_object, export_settings)
+    if skin:
+        vert_bones, num_joint_sets = __get_bone_data(blender_mesh, skin, blender_vertex_groups)
 
+    # 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.
     #
-    # Gather the verts and indices for each primitive.
+    # 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.
 
-    prims = {}
+    # 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 range(color_max):
+        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
+
+    for col_i in range(color_max):
+        colors = __get_colors(blender_mesh, col_i)
+        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)
 
-    for loop_tri in blender_mesh.loop_triangles:
-        blender_polygon = blender_mesh.polygons[loop_tri.polygon_index]
-
-        material_idx = -1
-        if use_materials:
-            material_idx = blender_polygon.material_index
-
-        prim = prims.get(material_idx)
-        if not prim:
-            prim = Prim()
-            prims[material_idx] = prim
-
-        for loop_index in loop_tri.loops:
-            vertex_index = blender_mesh.loops[loop_index].vertex_index
-            vertex = blender_mesh.vertices[vertex_index]
-
-            # vert will be a tuple of all the vertex attributes.
-            # Used as cache key in prim.verts.
-            vert = (vertex_index,)
-
-            v = vertex.co
-            vert += ((v[0], v[1], v[2]),)
-
-            if use_normals:
-                n = blender_mesh.loops[loop_index].normal
-                vert += ((n[0], n[1], n[2]),)
-                if use_tangents:
-                    t = blender_mesh.loops[loop_index].tangent
-                    b = blender_mesh.loops[loop_index].bitangent
-                    vert += ((t[0], t[1], t[2]),)
-                    vert += ((b[0], b[1], b[2]),)
-                    # TODO: store just bitangent_sign in vert, not whole bitangent?
-
-            for tex_coord_index in range(0, tex_coord_max):
-                uv = blender_mesh.uv_layers[tex_coord_index].data[loop_index].uv
-                uv = (uv.x, 1.0 - uv.y)
-                vert += (uv,)
-
-            for color_index in range(0, color_max):
-                color = blender_mesh.vertex_colors[color_index].data[loop_index].color
-                col = (
-                    color_srgb_to_scene_linear(color[0]),
-                    color_srgb_to_scene_linear(color[1]),
-                    color_srgb_to_scene_linear(color[2]),
-                    color[3],
-                )
-                vert += (col,)
-
-            if bone_max:
-                bones = []
-                if vertex.groups:
-                    for group_element in vertex.groups:
-                        weight = group_element.weight
-                        if weight <= 0.0:
-                            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)
-                bones = tuple(bones)
-                if not bones: bones = ((0, 1.0),)  # HACK for verts with zero weight (#308)
-                vert += (bones,)
-
-            for shape_key in shape_keys:
-                v_morph = shape_key.shape_key.data[vertex_index].co
-                v_morph = v_morph - v  # store delta
-                vert += ((v_morph[0], v_morph[1], v_morph[2]),)
-
-                if use_morph_normals:
-                    normals = shape_key.split_normals
-                    n_morph = Vector(normals[loop_index * 3 : loop_index * 3 + 3])
-                    n_morph = n_morph - n  # store delta
-                    vert += ((n_morph[0], n_morph[1], n_morph[2]),)
-
-            vert_idx = prim.verts.setdefault(vert, len(prim.verts))
-            prim.indices.append(vert_idx)
+    prim_indices = {}  # maps material index to TRIANGLES-style indices into dots
 
-    #
-    # Put the verts into attribute arrays.
-    #
+    if not use_materials:
+        # 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]
 
-    result_primitives = []
+    # Create all the primitives.
 
-    for material_idx, prim in prims.items():
-        if not prim.indices:
+    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
 
-        vs = []
-        ns = []
-        ts = []
-        uvs = [[] for _ in range(tex_coord_max)]
-        cols = [[] for _ in range(color_max)]
-        joints = [[] for _ in range(bone_max)]
-        weights = [[] for _ in range(bone_max)]
-        vs_morph = [[] for _ in shape_keys]
-        ns_morph = [[] for _ in shape_keys]
-        ts_morph = [[] for _ in shape_keys]
-
-        for vert in prim.verts.keys():
-            i = 0
-
-            i += 1  # skip over Blender mesh index
-
-            v = vert[i]
-            i += 1
-            v = convert_swizzle_location(v, armature, blender_object, export_settings)
-            vs.extend(v)
-
-            if use_normals:
-                n = vert[i]
-                i += 1
-                n = convert_swizzle_normal(n, armature, blender_object, export_settings)
-                ns.extend(n)
-
-                if use_tangents:
-                    t = vert[i]
-                    i += 1
-                    t = convert_swizzle_tangent(t, armature, blender_object, export_settings)
-                    ts.extend(t)
-
-                    b = vert[i]
-                    i += 1
-                    b = convert_swizzle_tangent(b, armature, blender_object, export_settings)
-                    b_sign = -1.0 if (Vector(n).cross(Vector(t))).dot(Vector(b)) < 0.0 else 1.0
-                    ts.append(b_sign)
-
-            for tex_coord_index in range(0, tex_coord_max):
-                uv = vert[i]
-                i += 1
-                uvs[tex_coord_index].extend(uv)
-
-            for color_index in range(0, color_max):
-                col = vert[i]
-                i += 1
-                cols[color_index].extend(col)
-
-            if bone_max:
-                bones = vert[i]
-                i += 1
-                for j in range(0, 4 * bone_max):
+        # 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 range(color_max):
+            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] = colors
+
+        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:
@@ -373,42 +273,230 @@ def extract_primitives(glTF, blender_mesh, library, blender_object, blender_vert
                     joints[j//4].append(joint)
                     weights[j//4].append(weight)
 
-            for shape_key_index in range(0, len(shape_keys)):
-                v_morph = vert[i]
-                i += 1
-                v_morph = convert_swizzle_location(v_morph, armature, blender_object, export_settings)
-                vs_morph[shape_key_index].extend(v_morph)
-
-                if use_morph_normals:
-                    n_morph = vert[i]
-                    i += 1
-                    n_morph = convert_swizzle_normal(n_morph, armature, blender_object, export_settings)
-                    ns_morph[shape_key_index].extend(n_morph)
-
-                if use_morph_tangents:
-                    rotation = n_morph.rotation_difference(n)
-                    t_morph = Vector(t)
-                    t_morph.rotate(rotation)
-                    ts_morph[shape_key_index].extend(t_morph)
+            for i, (js, ws) in enumerate(zip(joints, weights)):
+                attributes['JOINTS_%d' % i] = js
+                attributes['WEIGHTS_%d' % i] = ws
 
-        attributes = {}
-        attributes['POSITION'] = vs
-        if ns: attributes['NORMAL'] = ns
-        if ts: attributes['TANGENT'] = ts
-        for i, uv in enumerate(uvs): attributes['TEXCOORD_%d' % i] = uv
-        for i, col in enumerate(cols): attributes['COLOR_%d' % i] = col
-        for i, js in enumerate(joints): attributes['JOINTS_%d' % i] = js
-        for i, ws in enumerate(weights): attributes['WEIGHTS_%d' % i] = ws
-        for i, vm in enumerate(vs_morph): attributes['MORPH_POSITION_%d' % i] = vm
-        for i, nm in enumerate(ns_morph): attributes['MORPH_NORMAL_%d' % i] = nm
-        for i, tm in enumerate(ts_morph): attributes['MORPH_TANGENT_%d' % i] = tm
-
-        result_primitives.append({
+        primitives.append({
             'attributes': attributes,
-            'indices': prim.indices,
+            'indices': indices,
             'material': material_idx,
         })
 
-    print_console('INFO', 'Primitives created: %d' % len(result_primitives))
+    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)
+    blender_mesh.vertices.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() @ 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."""
+    blender_mesh.calc_normals_split()
+
+    normals = np.empty(len(blender_mesh.loops) * 3, dtype=np.float32)
+    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() @ blender_object.matrix_world)
+        apply_matrix = apply_matrix.to_3x3().inverted().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)
+
+    # 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() @ 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() @ 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):
+    layer = blender_mesh.vertex_colors[color_i]
+    colors = np.empty(len(blender_mesh.loops) * 4, dtype=np.float32)
+    layer.data.foreach_get('color', colors)
+    colors = colors.reshape(len(blender_mesh.loops), 4)
+
+    # sRGB -> Linear
+    rgb = colors[:, :-1]
+    not_small = rgb >= 0.04045
+    small_result = np.where(rgb < 0.0, 0.0, rgb * (1.0 / 12.92))
+    large_result = np.power((rgb + 0.055) * (1.0 / 1.055), 2.4, where=not_small)
+    rgb[:] = np.where(not_small, large_result, small_result)
+
+    return colors
+
+
+def __get_bone_data(blender_mesh, skin, blender_vertex_groups):
+    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 <= 0.0:
+                    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: bones = ((0, 1.0),)  # HACK for verts with zero weight (#308)
+        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
+
+
+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
+
 
-    return result_primitives
+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_nodes.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py
index b09e7aa12..1a1abcd02 100755
--- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py
@@ -14,7 +14,7 @@
 
 import math
 import bpy
-from mathutils import Matrix, Quaternion
+from mathutils import Matrix, Quaternion, Vector
 
 from . import gltf2_blender_export_keys
 from io_scene_gltf2.blender.com import gltf2_blender_math
@@ -23,7 +23,6 @@ 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.blender.exp import gltf2_blender_gather_lights
 from ..com.gltf2_blender_extras import generate_extras
 from io_scene_gltf2.io.com import gltf2_io
@@ -418,13 +417,13 @@ def __gather_trans_rot_scale(blender_object, export_settings):
     # make sure the rotation is normalized
     rot.normalize()
 
-    trans = gltf2_blender_extract.convert_swizzle_location(trans, None, None, export_settings)
-    rot = gltf2_blender_extract.convert_swizzle_rotation(rot, export_settings)
-    sca = gltf2_blender_extract.convert_swizzle_scale(sca, export_settings)
+    trans = __convert_swizzle_location(trans, export_settings)
+    rot = __convert_swizzle_rotation(rot, export_settings)
+    sca = __convert_swizzle_scale(sca, export_settings)
 
     if blender_object.instance_type == 'COLLECTION' and blender_object.instance_collection:
-        trans -= gltf2_blender_extract.convert_swizzle_location(
-            blender_object.instance_collection.instance_offset, None, None, export_settings)
+        trans -= __convert_swizzle_location(
+            blender_object.instance_collection.instance_offset, export_settings)
     translation, rotation, scale = (None, None, None)
     trans[0], trans[1], trans[2] = gltf2_blender_math.round_if_near(trans[0], 0.0), gltf2_blender_math.round_if_near(trans[1], 0.0), \
                                    gltf2_blender_math.round_if_near(trans[2], 0.0)
@@ -476,7 +475,7 @@ def __gather_weights(blender_object, export_settings):
 
 
 def __get_correction_node(blender_object, export_settings):
-    correction_quaternion = gltf2_blender_extract.convert_swizzle_rotation(
+    correction_quaternion = __convert_swizzle_rotation(
         Quaternion((1.0, 0.0, 0.0), math.radians(-90.0)), export_settings)
     correction_quaternion = [correction_quaternion[1], correction_quaternion[2],
                              correction_quaternion[3], correction_quaternion[0]]
@@ -494,3 +493,31 @@ def __get_correction_node(blender_object, export_settings):
         translation=None,
         weights=None
     )
+
+
+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_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]))
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 61adea896..e6a5881fc 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
@@ -85,9 +85,9 @@ def __gather_position(blender_primitive, export_settings):
 def __gather_normal(blender_primitive, export_settings):
     if not export_settings[gltf2_blender_export_keys.NORMALS]:
         return {}
-    normal = blender_primitive["attributes"].get('NORMAL')
-    if not normal:
+    if 'NORMAL' not in blender_primitive["attributes"]:
         return {}
+    normal = blender_primitive["attributes"]['NORMAL']
     return {
         "NORMAL": array_to_accessor(
             normal,
@@ -100,9 +100,9 @@ def __gather_normal(blender_primitive, export_settings):
 def __gather_tangent(blender_primitive, export_settings):
     if not export_settings[gltf2_blender_export_keys.TANGENTS]:
         return {}
-    tangent = blender_primitive["attributes"].get('TANGENT')
-    if not tangent:
+    if 'TANGENT' not in blender_primitive["attributes"]:
         return {}
+    tangent = blender_primitive["attributes"]['TANGENT']
     return {
         "TANGENT": array_to_accessor(
             tangent,
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 1a2ae00d4..4fe498e10 100755
--- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py
+++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py
@@ -14,6 +14,7 @@
 
 import bpy
 from typing import List, Optional, Tuple
+import numpy as np
 
 from .gltf2_blender_export_keys import NORMALS, MORPH_NORMAL, TANGENTS, MORPH_TANGENT, MORPH
 
@@ -114,21 +115,23 @@ def __gather_indices(blender_primitive, blender_mesh, modifiers, export_settings
     # https://github.com/KhronosGroup/glTF/pull/1476/files
     # Also, UINT8 mode is not supported:
     # https://github.com/KhronosGroup/glTF/issues/1471
-    max_index = max(indices)
+    max_index = indices.max()
     if max_index < 65535:
         component_type = gltf2_io_constants.ComponentType.UnsignedShort
+        indices = indices.astype(np.uint16, copy=False)
     elif max_index < 4294967295:
         component_type = gltf2_io_constants.ComponentType.UnsignedInt
+        indices = indices.astype(np.uint32, copy=False)
     else:
         print_console('ERROR', 'A mesh contains too many vertices (' + str(max_index) + ') and needs to be split before export.')
         return None
 
     element_type = gltf2_io_constants.DataType.Scalar
-    binary_data = gltf2_io_binary_data.BinaryData.from_list(indices, component_type)
+    binary_data = gltf2_io_binary_data.BinaryData(indices.tobytes())
     return gltf2_blender_gather_accessors.gather_accessor(
         binary_data,
         component_type,
-        len(indices) // gltf2_io_constants.DataType.num_elements(element_type),
+        len(indices),
         None,
         None,
         element_type,
@@ -156,7 +159,7 @@ def __gather_targets(blender_primitive, blender_mesh, modifiers, export_settings
                 target_normal_id = 'MORPH_NORMAL_' + str(morph_index)
                 target_tangent_id = 'MORPH_TANGENT_' + str(morph_index)
 
-                if blender_primitive["attributes"].get(target_position_id):
+                if blender_primitive["attributes"].get(target_position_id) is not None:
                     target = {}
                     internal_target_position = blender_primitive["attributes"][target_position_id]
                     target["POSITION"] = gltf2_blender_gather_primitive_attributes.array_to_accessor(
@@ -168,7 +171,7 @@ def __gather_targets(blender_primitive, blender_mesh, modifiers, export_settings
 
                     if export_settings[NORMALS] \
                             and export_settings[MORPH_NORMAL] \
-                            and blender_primitive["attributes"].get(target_normal_id):
+                            and blender_primitive["attributes"].get(target_normal_id) is not None:
 
                         internal_target_normal = blender_primitive["attributes"][target_normal_id]
                         target['NORMAL'] = gltf2_blender_gather_primitive_attributes.array_to_accessor(
@@ -179,7 +182,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):
+                            and blender_primitive["attributes"].get(target_tangent_id) is not None:
                         internal_target_tangent = blender_primitive["attributes"][target_tangent_id]
                         target['TANGENT'] = gltf2_blender_gather_primitive_attributes.array_to_accessor(
                             internal_target_tangent,
-- 
GitLab