diff options
Diffstat (limited to 'io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives_extract.py')
-rw-r--r-- | io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives_extract.py | 863 |
1 files changed, 863 insertions, 0 deletions
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']) |