# 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 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) 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_skins.gather_skin(export_settings['vtree'].nodes[uuid_for_skined_data].armature, 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 = [] if 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 range(color_max): dot_fields += [ ('color%dr' % col_i, np.float32), ('color%dg' % col_i, np.float32), ('color%db' % col_i, np.float32), ('color%da' % col_i, np.float32), ] if use_morph_normals: for morph_i, _ in enumerate(key_blocks): dot_fields += [ ('morph%dnx' % morph_i, np.float32), ('morph%dny' % morph_i, np.float32), ('morph%dnz' % morph_i, np.float32), ] dots = np.empty(len(blender_mesh.loops), dtype=np.dtype(dot_fields)) vidxs = np.empty(len(blender_mesh.loops)) blender_mesh.loops.foreach_get('vertex_index', vidxs) dots['vertex_index'] = vidxs del vidxs if use_normals: kbs = key_blocks if use_morph_normals else [] normals, morph_normals = __get_normals( blender_mesh, kbs, armature, blender_object, export_settings ) dots['nx'] = normals[:, 0] dots['ny'] = normals[:, 1] dots['nz'] = normals[:, 2] del normals for morph_i, ns in enumerate(morph_normals): dots['morph%dnx' % morph_i] = ns[:, 0] dots['morph%dny' % morph_i] = ns[:, 1] dots['morph%dnz' % morph_i] = ns[:, 2] del morph_normals if use_tangents: tangents = __get_tangents(blender_mesh, armature, blender_object, export_settings) dots['tx'] = tangents[:, 0] dots['ty'] = tangents[:, 1] dots['tz'] = tangents[:, 2] del tangents signs = __get_bitangent_signs(blender_mesh, armature, blender_object, export_settings) dots['tw'] = signs del signs for uv_i in range(tex_coord_max): uvs = __get_uvs(blender_mesh, uv_i) dots['uv%dx' % uv_i] = uvs[:, 0] dots['uv%dy' % uv_i] = uvs[:, 1] del uvs for col_i in range(color_max): colors = __get_colors(blender_mesh, col_i) dots['color%dr' % col_i] = colors[:, 0] dots['color%dg' % col_i] = colors[:, 1] dots['color%db' % col_i] = colors[:, 2] dots['color%da' % col_i] = colors[:, 3] del colors # Calculate triangles and sort them into primitives. blender_mesh.calc_loop_triangles() loop_indices = np.empty(len(blender_mesh.loop_triangles) * 3, dtype=np.uint32) blender_mesh.loop_triangles.foreach_get('loops', loop_indices) 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 range(color_max): colors = np.empty((len(prim_dots), 4), dtype=np.float32) colors[:, 0] = prim_dots['color%dr' % color_i] colors[:, 1] = prim_dots['color%dg' % color_i] colors[:, 2] = prim_dots['color%db' % color_i] colors[:, 3] = prim_dots['color%da' % color_i] attributes['COLOR_%d' % color_i] = colors if skin: joints = [[] for _ in range(num_joint_sets)] weights = [[] for _ in range(num_joint_sets)] for vi in blender_idxs: bones = vert_bones[vi] for j in range(0, 4 * num_joint_sets): if j < len(bones): joint, weight = bones[j] else: 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): colors = np.empty(len(blender_mesh.loops) * 4, dtype=np.float32) layer = blender_mesh.vertex_colors[color_i] blender_mesh.color_attributes[layer.name].data.foreach_get('color', colors) colors = colors.reshape(len(blender_mesh.loops), 4) # colors are already linear, no need to switch color space return colors 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)