Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJulien Duroure <julien.duroure@gmail.com>2022-09-25 18:08:07 +0300
committerJulien Duroure <julien.duroure@gmail.com>2022-09-25 18:08:07 +0300
commit51e15a9db4ce463e01c291f6ec9152efe629bab5 (patch)
tree8112796fc281e471a9be0af2858c7f97f01496d7
parente77b55e45a2a444b461c2133ce602bbff4b0d65a (diff)
glTF exporter: Big gltf primitive extraction refactoring + Blender attributes export
-rwxr-xr-xio_scene_gltf2/__init__.py10
-rwxr-xr-xio_scene_gltf2/blender/com/gltf2_blender_conversion.py54
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_extract.py619
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py186
-rwxr-xr-xio_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py10
-rw-r--r--io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives_extract.py863
-rw-r--r--io_scene_gltf2/blender/exp/gltf2_blender_gather_tree.py1
7 files changed, 997 insertions, 746 deletions
diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py
index 4d8acd68..610c061a 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 ecb91c8f..85ab654a 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 bdea2c6f..00000000
--- 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 1af588b9..ce2f9a59 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 576a1418..d7784c3e 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 00000000..74682ce7
--- /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 cfeb70e2..dc25b417 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