diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index d3a247dea98932b9cd1735e97bd3073eca49d3ac..daa6c7e42171d5e88f3eb083e216afbc250c1a26 100755 --- a/io_scene_gltf2/__init__.py +++ b/io_scene_gltf2/__init__.py @@ -15,7 +15,7 @@ bl_info = { 'name': 'glTF 2.0 format', 'author': 'Julien Duroure, Scurest, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin SchmithĂĽsen, Jim Eckerlein, and many external contributors', - "version": (1, 4, 8), + "version": (1, 4, 9), 'blender': (2, 90, 0), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_extract.py b/io_scene_gltf2/blender/exp/gltf2_blender_extract.py index eef05044be8dd0360acc464368b626e3cf795194..ca38aa7250fe6c6776c21a1373ac4157872ac6c3 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_extract.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_extract.py @@ -12,128 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -# -# Imports -# - +import numpy as np from mathutils import Vector, Quaternion, Matrix from . import gltf2_blender_export_keys from ...io.com.gltf2_io_debug import print_console -from ...io.com.gltf2_io_color_management import color_srgb_to_scene_linear from io_scene_gltf2.blender.exp import gltf2_blender_gather_skins -# -# Classes -# - -class Prim: - def __init__(self): - self.verts = {} - self.indices = [] - -class ShapeKey: - def __init__(self, shape_key, split_normals): - self.shape_key = shape_key - self.split_normals = split_normals - - -# -# Functions -# - -def convert_swizzle_normal(loc, armature, blender_object, export_settings): - """Convert a normal data from Blender coordinate system to glTF coordinate system.""" - if (not armature) or (not blender_object): - # Classic case. Mesh is not skined, no need to apply armature transfoms on vertices / normals / tangents - if export_settings[gltf2_blender_export_keys.YUP]: - return Vector((loc[0], loc[2], -loc[1])) - else: - return Vector((loc[0], loc[1], loc[2])) - else: - # Mesh is skined, we have to apply armature transforms on data - apply_matrix = (armature.matrix_world.inverted() @ blender_object.matrix_world).to_3x3().inverted() - apply_matrix.transpose() - new_loc = ((armature.matrix_world.to_3x3() @ apply_matrix).to_4x4() @ Matrix.Translation(Vector((loc[0], loc[1], loc[2])))).to_translation() - new_loc.normalize() - - if export_settings[gltf2_blender_export_keys.YUP]: - return Vector((new_loc[0], new_loc[2], -new_loc[1])) - else: - return Vector((new_loc[0], new_loc[1], new_loc[2])) - -def convert_swizzle_location(loc, armature, blender_object, export_settings): - """Convert a location from Blender coordinate system to glTF coordinate system.""" - if (not armature) or (not blender_object): - # Classic case. Mesh is not skined, no need to apply armature transfoms on vertices / normals / tangents - if export_settings[gltf2_blender_export_keys.YUP]: - return Vector((loc[0], loc[2], -loc[1])) - else: - return Vector((loc[0], loc[1], loc[2])) - else: - # Mesh is skined, we have to apply armature transforms on data - apply_matrix = armature.matrix_world.inverted() @ blender_object.matrix_world - new_loc = (armature.matrix_world @ apply_matrix @ Matrix.Translation(Vector((loc[0], loc[1], loc[2])))).to_translation() - - if export_settings[gltf2_blender_export_keys.YUP]: - return Vector((new_loc[0], new_loc[2], -new_loc[1])) - else: - return Vector((new_loc[0], new_loc[1], new_loc[2])) - - -def convert_swizzle_tangent(tan, armature, blender_object, export_settings): - """Convert a tangent from Blender coordinate system to glTF coordinate system.""" - if tan[0] == 0.0 and tan[1] == 0.0 and tan[2] == 0.0: - print_console('WARNING', 'Tangent has zero length.') - - if (not armature) or (not blender_object): - # Classic case. Mesh is not skined, no need to apply armature transfoms on vertices / normals / tangents - if export_settings[gltf2_blender_export_keys.YUP]: - return Vector((tan[0], tan[2], -tan[1])) - else: - return Vector((tan[0], tan[1], tan[2])) - else: - # Mesh is skined, we have to apply armature transforms on data - apply_matrix = armature.matrix_world.inverted() @ blender_object.matrix_world - new_tan = apply_matrix.to_quaternion() @ Vector((tan[0], tan[1], tan[2])) - if export_settings[gltf2_blender_export_keys.YUP]: - return Vector((new_tan[0], new_tan[2], -new_tan[1])) - else: - return Vector((new_tan[0], new_tan[1], new_tan[2])) - -def convert_swizzle_rotation(rot, export_settings): - """ - Convert a quaternion rotation from Blender coordinate system to glTF coordinate system. - - 'w' is still at first position. - """ - if export_settings[gltf2_blender_export_keys.YUP]: - return Quaternion((rot[0], rot[1], rot[3], -rot[2])) - else: - return Quaternion((rot[0], rot[1], rot[2], rot[3])) - - -def convert_swizzle_scale(scale, export_settings): - """Convert a scale from Blender coordinate system to glTF coordinate system.""" - if export_settings[gltf2_blender_export_keys.YUP]: - return Vector((scale[0], scale[2], scale[1])) - else: - return Vector((scale[0], scale[1], scale[2])) - - def extract_primitives(glTF, blender_mesh, library, blender_object, blender_vertex_groups, modifiers, export_settings): - """ - Extract primitives from a mesh. Polygons are triangulated and sorted by material. - Vertices in multiple faces get split up as necessary. - """ + """Extract primitives from a mesh.""" print_console('INFO', 'Extracting primitive: ' + blender_mesh.name) - # - # First, decide what attributes to gather (eg. how many COLOR_n, etc.) - # Also calculate normals/tangents now if necessary. - # - use_normals = export_settings[gltf2_blender_export_keys.NORMALS] if use_normals: blender_mesh.calc_normals_split() @@ -156,8 +46,8 @@ def extract_primitives(glTF, blender_mesh, library, blender_object, blender_vert if export_settings[gltf2_blender_export_keys.COLORS]: color_max = len(blender_mesh.vertex_colors) - bone_max = 0 # number of JOINTS_n sets needed (1 set = 4 influences) armature = None + skin = None if blender_vertex_groups and export_settings[gltf2_blender_export_keys.SKINS]: if modifiers is not None: modifiers_dict = {m.type: m for m in modifiers} @@ -181,191 +71,201 @@ def extract_primitives(glTF, blender_mesh, library, blender_object, blender_vert skin = gltf2_blender_gather_skins.gather_skin(armature, export_settings) if not skin: armature = None - else: - joint_name_to_index = {joint.name: index for index, joint in enumerate(skin.joints)} - group_to_joint = [joint_name_to_index.get(g.name) for g in blender_vertex_groups] - - # Find out max number of bone influences - for blender_polygon in blender_mesh.polygons: - for loop_index in blender_polygon.loop_indices: - vertex_index = blender_mesh.loops[loop_index].vertex_index - groups_count = len(blender_mesh.vertices[vertex_index].groups) - bones_count = (groups_count + 3) // 4 - bone_max = max(bone_max, bones_count) use_morph_normals = use_normals and export_settings[gltf2_blender_export_keys.MORPH_NORMAL] use_morph_tangents = use_morph_normals and use_tangents and export_settings[gltf2_blender_export_keys.MORPH_TANGENT] - shape_keys = [] + key_blocks = [] if blender_mesh.shape_keys and export_settings[gltf2_blender_export_keys.MORPH]: - for blender_shape_key in blender_mesh.shape_keys.key_blocks: - if blender_shape_key == blender_shape_key.relative_key or blender_shape_key.mute: - continue + key_blocks = [ + key_block + for key_block in blender_mesh.shape_keys.key_blocks + if not (key_block == key_block.relative_key or key_block.mute) + ] - split_normals = None - if use_morph_normals: - split_normals = blender_shape_key.normals_split_get() - - shape_keys.append(ShapeKey( - blender_shape_key, - split_normals, - )) + use_materials = export_settings[gltf2_blender_export_keys.MATERIALS] + # Fetch vert positions and bone data (joint,weights) - use_materials = export_settings[gltf2_blender_export_keys.MATERIALS] + locs, morph_locs = __get_positions(blender_mesh, key_blocks, armature, blender_object, export_settings) + if skin: + vert_bones, num_joint_sets = __get_bone_data(blender_mesh, skin, blender_vertex_groups) + # In Blender there is both per-vert data, like position, and also per-loop + # (loop=corner-of-poly) data, like normals or UVs. glTF only has per-vert + # data, so we need to split Blender verts up into potentially-multiple glTF + # verts. # - # Gather the verts and indices for each primitive. + # First, we'll collect a "dot" for every loop: a struct that stores all the + # attributes at that loop, namely the vertex index (which determines all + # per-vert data), and all the per-loop data like UVs, etc. # + # Each unique dot will become one unique glTF vert. - prims = {} + # List all fields the dot struct needs. + dot_fields = [('vertex_index', np.uint32)] + if use_normals: + dot_fields += [('nx', np.float32), ('ny', np.float32), ('nz', np.float32)] + if use_tangents: + dot_fields += [('tx', np.float32), ('ty', np.float32), ('tz', np.float32), ('tw', np.float32)] + for uv_i in range(tex_coord_max): + dot_fields += [('uv%dx' % uv_i, np.float32), ('uv%dy' % uv_i, np.float32)] + for col_i in range(color_max): + dot_fields += [ + ('color%dr' % col_i, np.float32), + ('color%dg' % col_i, np.float32), + ('color%db' % col_i, np.float32), + ('color%da' % col_i, np.float32), + ] + if use_morph_normals: + for morph_i, _ in enumerate(key_blocks): + dot_fields += [ + ('morph%dnx' % morph_i, np.float32), + ('morph%dny' % morph_i, np.float32), + ('morph%dnz' % morph_i, np.float32), + ] + + dots = np.empty(len(blender_mesh.loops), dtype=np.dtype(dot_fields)) + + vidxs = np.empty(len(blender_mesh.loops)) + blender_mesh.loops.foreach_get('vertex_index', vidxs) + dots['vertex_index'] = vidxs + del vidxs + + if use_normals: + kbs = key_blocks if use_morph_normals else [] + normals, morph_normals = __get_normals( + blender_mesh, kbs, armature, blender_object, export_settings + ) + dots['nx'] = normals[:, 0] + dots['ny'] = normals[:, 1] + dots['nz'] = normals[:, 2] + del normals + for morph_i, ns in enumerate(morph_normals): + dots['morph%dnx' % morph_i] = ns[:, 0] + dots['morph%dny' % morph_i] = ns[:, 1] + dots['morph%dnz' % morph_i] = ns[:, 2] + del morph_normals + + if use_tangents: + tangents = __get_tangents(blender_mesh, armature, blender_object, export_settings) + dots['tx'] = tangents[:, 0] + dots['ty'] = tangents[:, 1] + dots['tz'] = tangents[:, 2] + del tangents + signs = __get_bitangent_signs(blender_mesh, armature, blender_object, export_settings) + dots['tw'] = signs + del signs + + for uv_i in range(tex_coord_max): + uvs = __get_uvs(blender_mesh, uv_i) + dots['uv%dx' % uv_i] = uvs[:, 0] + dots['uv%dy' % uv_i] = uvs[:, 1] + del uvs + + for col_i in range(color_max): + colors = __get_colors(blender_mesh, col_i) + dots['color%dr' % col_i] = colors[:, 0] + dots['color%dg' % col_i] = colors[:, 1] + dots['color%db' % col_i] = colors[:, 2] + dots['color%da' % col_i] = colors[:, 3] + del colors + + # Calculate triangles and sort them into primitives. blender_mesh.calc_loop_triangles() + loop_indices = np.empty(len(blender_mesh.loop_triangles) * 3, dtype=np.uint32) + blender_mesh.loop_triangles.foreach_get('loops', loop_indices) - for loop_tri in blender_mesh.loop_triangles: - blender_polygon = blender_mesh.polygons[loop_tri.polygon_index] - - material_idx = -1 - if use_materials: - material_idx = blender_polygon.material_index - - prim = prims.get(material_idx) - if not prim: - prim = Prim() - prims[material_idx] = prim - - for loop_index in loop_tri.loops: - vertex_index = blender_mesh.loops[loop_index].vertex_index - vertex = blender_mesh.vertices[vertex_index] - - # vert will be a tuple of all the vertex attributes. - # Used as cache key in prim.verts. - vert = (vertex_index,) - - v = vertex.co - vert += ((v[0], v[1], v[2]),) - - if use_normals: - n = blender_mesh.loops[loop_index].normal - vert += ((n[0], n[1], n[2]),) - if use_tangents: - t = blender_mesh.loops[loop_index].tangent - b = blender_mesh.loops[loop_index].bitangent - vert += ((t[0], t[1], t[2]),) - vert += ((b[0], b[1], b[2]),) - # TODO: store just bitangent_sign in vert, not whole bitangent? - - for tex_coord_index in range(0, tex_coord_max): - uv = blender_mesh.uv_layers[tex_coord_index].data[loop_index].uv - uv = (uv.x, 1.0 - uv.y) - vert += (uv,) - - for color_index in range(0, color_max): - color = blender_mesh.vertex_colors[color_index].data[loop_index].color - col = ( - color_srgb_to_scene_linear(color[0]), - color_srgb_to_scene_linear(color[1]), - color_srgb_to_scene_linear(color[2]), - color[3], - ) - vert += (col,) - - if bone_max: - bones = [] - if vertex.groups: - for group_element in vertex.groups: - weight = group_element.weight - if weight <= 0.0: - continue - try: - joint = group_to_joint[group_element.group] - except Exception: - continue - if joint is None: - continue - bones.append((joint, weight)) - bones.sort(key=lambda x: x[1], reverse=True) - bones = tuple(bones) - if not bones: bones = ((0, 1.0),) # HACK for verts with zero weight (#308) - vert += (bones,) - - for shape_key in shape_keys: - v_morph = shape_key.shape_key.data[vertex_index].co - v_morph = v_morph - v # store delta - vert += ((v_morph[0], v_morph[1], v_morph[2]),) - - if use_morph_normals: - normals = shape_key.split_normals - n_morph = Vector(normals[loop_index * 3 : loop_index * 3 + 3]) - n_morph = n_morph - n # store delta - vert += ((n_morph[0], n_morph[1], n_morph[2]),) - - vert_idx = prim.verts.setdefault(vert, len(prim.verts)) - prim.indices.append(vert_idx) + prim_indices = {} # maps material index to TRIANGLES-style indices into dots - # - # Put the verts into attribute arrays. - # + if not use_materials: + # Put all vertices into one primitive + prim_indices[-1] = loop_indices + + else: + # Bucket by material index. + + tri_material_idxs = np.empty(len(blender_mesh.loop_triangles), dtype=np.uint32) + blender_mesh.loop_triangles.foreach_get('material_index', tri_material_idxs) + loop_material_idxs = np.repeat(tri_material_idxs, 3) # material index for every loop + unique_material_idxs = np.unique(tri_material_idxs) + del tri_material_idxs + + for material_idx in unique_material_idxs: + prim_indices[material_idx] = loop_indices[loop_material_idxs == material_idx] - result_primitives = [] + # Create all the primitives. - for material_idx, prim in prims.items(): - if not prim.indices: + primitives = [] + + for material_idx, dot_indices in prim_indices.items(): + # Extract just dots used by this primitive, deduplicate them, and + # calculate indices into this deduplicated list. + prim_dots = dots[dot_indices] + prim_dots, indices = np.unique(prim_dots, return_inverse=True) + + if len(prim_dots) == 0: continue - vs = [] - ns = [] - ts = [] - uvs = [[] for _ in range(tex_coord_max)] - cols = [[] for _ in range(color_max)] - joints = [[] for _ in range(bone_max)] - weights = [[] for _ in range(bone_max)] - vs_morph = [[] for _ in shape_keys] - ns_morph = [[] for _ in shape_keys] - ts_morph = [[] for _ in shape_keys] - - for vert in prim.verts.keys(): - i = 0 - - i += 1 # skip over Blender mesh index - - v = vert[i] - i += 1 - v = convert_swizzle_location(v, armature, blender_object, export_settings) - vs.extend(v) - - if use_normals: - n = vert[i] - i += 1 - n = convert_swizzle_normal(n, armature, blender_object, export_settings) - ns.extend(n) - - if use_tangents: - t = vert[i] - i += 1 - t = convert_swizzle_tangent(t, armature, blender_object, export_settings) - ts.extend(t) - - b = vert[i] - i += 1 - b = convert_swizzle_tangent(b, armature, blender_object, export_settings) - b_sign = -1.0 if (Vector(n).cross(Vector(t))).dot(Vector(b)) < 0.0 else 1.0 - ts.append(b_sign) - - for tex_coord_index in range(0, tex_coord_max): - uv = vert[i] - i += 1 - uvs[tex_coord_index].extend(uv) - - for color_index in range(0, color_max): - col = vert[i] - i += 1 - cols[color_index].extend(col) - - if bone_max: - bones = vert[i] - i += 1 - for j in range(0, 4 * bone_max): + # Now just move all the data for prim_dots into attribute arrays + + attributes = {} + + blender_idxs = prim_dots['vertex_index'] + + attributes['POSITION'] = locs[blender_idxs] + + for morph_i, vs in enumerate(morph_locs): + attributes['MORPH_POSITION_%d' % morph_i] = vs[blender_idxs] + + if use_normals: + normals = np.empty((len(prim_dots), 3), dtype=np.float32) + normals[:, 0] = prim_dots['nx'] + normals[:, 1] = prim_dots['ny'] + normals[:, 2] = prim_dots['nz'] + attributes['NORMAL'] = normals + + if use_tangents: + tangents = np.empty((len(prim_dots), 4), dtype=np.float32) + tangents[:, 0] = prim_dots['tx'] + tangents[:, 1] = prim_dots['ty'] + tangents[:, 2] = prim_dots['tz'] + tangents[:, 3] = prim_dots['tw'] + attributes['TANGENT'] = tangents + + if use_morph_normals: + for morph_i, _ in enumerate(key_blocks): + ns = np.empty((len(prim_dots), 3), dtype=np.float32) + ns[:, 0] = prim_dots['morph%dnx' % morph_i] + ns[:, 1] = prim_dots['morph%dny' % morph_i] + ns[:, 2] = prim_dots['morph%dnz' % morph_i] + attributes['MORPH_NORMAL_%d' % morph_i] = ns + + if use_morph_tangents: + attributes['MORPH_TANGENT_%d' % morph_i] = __calc_morph_tangents(normals, ns, tangents) + + for tex_coord_i in range(tex_coord_max): + uvs = np.empty((len(prim_dots), 2), dtype=np.float32) + uvs[:, 0] = prim_dots['uv%dx' % tex_coord_i] + uvs[:, 1] = prim_dots['uv%dy' % tex_coord_i] + attributes['TEXCOORD_%d' % tex_coord_i] = uvs + + for color_i in range(color_max): + colors = np.empty((len(prim_dots), 4), dtype=np.float32) + colors[:, 0] = prim_dots['color%dr' % color_i] + colors[:, 1] = prim_dots['color%dg' % color_i] + colors[:, 2] = prim_dots['color%db' % color_i] + colors[:, 3] = prim_dots['color%da' % color_i] + attributes['COLOR_%d' % color_i] = colors + + if skin: + joints = [[] for _ in range(num_joint_sets)] + weights = [[] for _ in range(num_joint_sets)] + + for vi in blender_idxs: + bones = vert_bones[vi] + for j in range(0, 4 * num_joint_sets): if j < len(bones): joint, weight = bones[j] else: @@ -373,42 +273,230 @@ def extract_primitives(glTF, blender_mesh, library, blender_object, blender_vert joints[j//4].append(joint) weights[j//4].append(weight) - for shape_key_index in range(0, len(shape_keys)): - v_morph = vert[i] - i += 1 - v_morph = convert_swizzle_location(v_morph, armature, blender_object, export_settings) - vs_morph[shape_key_index].extend(v_morph) - - if use_morph_normals: - n_morph = vert[i] - i += 1 - n_morph = convert_swizzle_normal(n_morph, armature, blender_object, export_settings) - ns_morph[shape_key_index].extend(n_morph) - - if use_morph_tangents: - rotation = n_morph.rotation_difference(n) - t_morph = Vector(t) - t_morph.rotate(rotation) - ts_morph[shape_key_index].extend(t_morph) + for i, (js, ws) in enumerate(zip(joints, weights)): + attributes['JOINTS_%d' % i] = js + attributes['WEIGHTS_%d' % i] = ws - attributes = {} - attributes['POSITION'] = vs - if ns: attributes['NORMAL'] = ns - if ts: attributes['TANGENT'] = ts - for i, uv in enumerate(uvs): attributes['TEXCOORD_%d' % i] = uv - for i, col in enumerate(cols): attributes['COLOR_%d' % i] = col - for i, js in enumerate(joints): attributes['JOINTS_%d' % i] = js - for i, ws in enumerate(weights): attributes['WEIGHTS_%d' % i] = ws - for i, vm in enumerate(vs_morph): attributes['MORPH_POSITION_%d' % i] = vm - for i, nm in enumerate(ns_morph): attributes['MORPH_NORMAL_%d' % i] = nm - for i, tm in enumerate(ts_morph): attributes['MORPH_TANGENT_%d' % i] = tm - - result_primitives.append({ + primitives.append({ 'attributes': attributes, - 'indices': prim.indices, + 'indices': indices, 'material': material_idx, }) - print_console('INFO', 'Primitives created: %d' % len(result_primitives)) + print_console('INFO', 'Primitives created: %d' % len(primitives)) + + return primitives + + +def __get_positions(blender_mesh, key_blocks, armature, blender_object, export_settings): + locs = np.empty(len(blender_mesh.vertices) * 3, dtype=np.float32) + blender_mesh.vertices.foreach_get('co', locs) + locs = locs.reshape(len(blender_mesh.vertices), 3) + + morph_locs = [] + for key_block in key_blocks: + vs = np.empty(len(blender_mesh.vertices) * 3, dtype=np.float32) + key_block.data.foreach_get('co', vs) + vs = vs.reshape(len(blender_mesh.vertices), 3) + morph_locs.append(vs) + + # Transform for skinning + if armature and blender_object: + apply_matrix = armature.matrix_world.inverted() @ blender_object.matrix_world + loc_transform = armature.matrix_world @ apply_matrix + + loc_transform = blender_object.matrix_world + locs[:] = __apply_mat_to_all(loc_transform, locs) + for vs in morph_locs: + vs[:] = __apply_mat_to_all(loc_transform, vs) + + # glTF stores deltas in morph targets + for vs in morph_locs: + vs -= locs + + if export_settings[gltf2_blender_export_keys.YUP]: + __zup2yup(locs) + for vs in morph_locs: + __zup2yup(vs) + + return locs, morph_locs + + +def __get_normals(blender_mesh, key_blocks, armature, blender_object, export_settings): + """Get normal for each loop.""" + blender_mesh.calc_normals_split() + + normals = np.empty(len(blender_mesh.loops) * 3, dtype=np.float32) + blender_mesh.loops.foreach_get('normal', normals) + normals = normals.reshape(len(blender_mesh.loops), 3) + + morph_normals = [] + for key_block in key_blocks: + ns = np.array(key_block.normals_split_get(), dtype=np.float32) + ns = ns.reshape(len(blender_mesh.loops), 3) + morph_normals.append(ns) + + # Transform for skinning + if armature and blender_object: + apply_matrix = (armature.matrix_world.inverted() @ blender_object.matrix_world) + apply_matrix = apply_matrix.to_3x3().inverted().transposed() + normal_transform = armature.matrix_world.to_3x3() @ apply_matrix + + normals[:] = __apply_mat_to_all(normal_transform, normals) + __normalize_vecs(normals) + for ns in morph_normals: + ns[:] = __apply_mat_to_all(normal_transform, ns) + __normalize_vecs(ns) + + # glTF stores deltas in morph targets + for ns in morph_normals: + ns -= normals + + if export_settings[gltf2_blender_export_keys.YUP]: + __zup2yup(normals) + for ns in morph_normals: + __zup2yup(ns) + + return normals, morph_normals + + +def __get_tangents(blender_mesh, armature, blender_object, export_settings): + """Get an array of the tangent for each loop.""" + tangents = np.empty(len(blender_mesh.loops) * 3, dtype=np.float32) + blender_mesh.loops.foreach_get('tangent', tangents) + tangents = tangents.reshape(len(blender_mesh.loops), 3) + + # Transform for skinning + if armature and blender_object: + apply_matrix = armature.matrix_world.inverted() @ blender_object.matrix_world + tangent_transform = apply_matrix.to_quaternion().to_matrix() + tangents = __apply_mat_to_all(tangent_transform, tangents) + __normalize_vecs(tangents) + + if export_settings[gltf2_blender_export_keys.YUP]: + __zup2yup(tangents) + + return tangents + + +def __get_bitangent_signs(blender_mesh, armature, blender_object, export_settings): + signs = np.empty(len(blender_mesh.loops), dtype=np.float32) + blender_mesh.loops.foreach_get('bitangent_sign', signs) + + # Transform for skinning + if armature and blender_object: + # Bitangent signs should flip when handedness changes + # TODO: confirm + apply_matrix = armature.matrix_world.inverted() @ blender_object.matrix_world + tangent_transform = apply_matrix.to_quaternion().to_matrix() + flipped = tangent_transform.determinant() < 0 + if flipped: + signs *= -1 + + # No change for Zup -> Yup + + return signs + + +def __calc_morph_tangents(normals, morph_normal_deltas, tangents): + # TODO: check if this works + morph_tangent_deltas = np.empty((len(normals), 3), dtype=np.float32) + + for i in range(len(normals)): + n = Vector(normals[i]) + morph_n = n + Vector(morph_normal_deltas[i]) # convert back to non-delta + t = Vector(tangents[i, :3]) + + rotation = morph_n.rotation_difference(n) + + t_morph = Vector(t) + t_morph.rotate(rotation) + morph_tangent_deltas[i] = t_morph - t # back to delta + + return morph_tangent_deltas + + +def __get_uvs(blender_mesh, uv_i): + layer = blender_mesh.uv_layers[uv_i] + uvs = np.empty(len(blender_mesh.loops) * 2, dtype=np.float32) + layer.data.foreach_get('uv', uvs) + uvs = uvs.reshape(len(blender_mesh.loops), 2) + + # Blender UV space -> glTF UV space + # u,v -> u,1-v + uvs[:, 1] *= -1 + uvs[:, 1] += 1 + + return uvs + + +def __get_colors(blender_mesh, color_i): + layer = blender_mesh.vertex_colors[color_i] + colors = np.empty(len(blender_mesh.loops) * 4, dtype=np.float32) + layer.data.foreach_get('color', colors) + colors = colors.reshape(len(blender_mesh.loops), 4) + + # sRGB -> Linear + rgb = colors[:, :-1] + not_small = rgb >= 0.04045 + small_result = np.where(rgb < 0.0, 0.0, rgb * (1.0 / 12.92)) + large_result = np.power((rgb + 0.055) * (1.0 / 1.055), 2.4, where=not_small) + rgb[:] = np.where(not_small, large_result, small_result) + + return colors + + +def __get_bone_data(blender_mesh, skin, blender_vertex_groups): + joint_name_to_index = {joint.name: index for index, joint in enumerate(skin.joints)} + group_to_joint = [joint_name_to_index.get(g.name) for g in blender_vertex_groups] + + # List of (joint, weight) pairs for each vert + vert_bones = [] + max_num_influences = 0 + + for vertex in blender_mesh.vertices: + bones = [] + if vertex.groups: + for group_element in vertex.groups: + weight = group_element.weight + if weight <= 0.0: + continue + try: + joint = group_to_joint[group_element.group] + except Exception: + continue + if joint is None: + continue + bones.append((joint, weight)) + bones.sort(key=lambda x: x[1], reverse=True) + if not bones: bones = ((0, 1.0),) # HACK for verts with zero weight (#308) + vert_bones.append(bones) + if len(bones) > max_num_influences: + max_num_influences = len(bones) + + # How many joint sets do we need? 1 set = 4 influences + num_joint_sets = (max_num_influences + 3) // 4 + + return vert_bones, num_joint_sets + + +def __zup2yup(array): + # x,y,z -> x,z,-y + array[:, [1,2]] = array[:, [2,1]] # x,z,y + array[:, 2] *= -1 # x,z,-y + + +def __apply_mat_to_all(matrix, vectors): + """Given matrix m and vectors [v1,v2,...], computes [m@v1,m@v2,...]""" + # Linear part + m = matrix.to_3x3() if len(matrix) == 4 else matrix + res = np.matmul(vectors, np.array(m.transposed())) + # Translation part + if len(matrix) == 4: + res += np.array(matrix.translation) + return res + - return result_primitives +def __normalize_vecs(vectors): + norms = np.linalg.norm(vectors, axis=1, keepdims=True) + np.divide(vectors, norms, out=vectors, where=norms != 0) diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py index b09e7aa123e1a0eabfbe77763c9a1c92a22e1d2b..1a1abcd029fb8eeae21f9dffadae463aa41b692a 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py @@ -14,7 +14,7 @@ import math import bpy -from mathutils import Matrix, Quaternion +from mathutils import Matrix, Quaternion, Vector from . import gltf2_blender_export_keys from io_scene_gltf2.blender.com import gltf2_blender_math @@ -23,7 +23,6 @@ from io_scene_gltf2.blender.exp import gltf2_blender_gather_skins from io_scene_gltf2.blender.exp import gltf2_blender_gather_cameras from io_scene_gltf2.blender.exp import gltf2_blender_gather_mesh from io_scene_gltf2.blender.exp import gltf2_blender_gather_joints -from io_scene_gltf2.blender.exp import gltf2_blender_extract from io_scene_gltf2.blender.exp import gltf2_blender_gather_lights from ..com.gltf2_blender_extras import generate_extras from io_scene_gltf2.io.com import gltf2_io @@ -418,13 +417,13 @@ def __gather_trans_rot_scale(blender_object, export_settings): # make sure the rotation is normalized rot.normalize() - trans = gltf2_blender_extract.convert_swizzle_location(trans, None, None, export_settings) - rot = gltf2_blender_extract.convert_swizzle_rotation(rot, export_settings) - sca = gltf2_blender_extract.convert_swizzle_scale(sca, export_settings) + trans = __convert_swizzle_location(trans, export_settings) + rot = __convert_swizzle_rotation(rot, export_settings) + sca = __convert_swizzle_scale(sca, export_settings) if blender_object.instance_type == 'COLLECTION' and blender_object.instance_collection: - trans -= gltf2_blender_extract.convert_swizzle_location( - blender_object.instance_collection.instance_offset, None, None, export_settings) + trans -= __convert_swizzle_location( + blender_object.instance_collection.instance_offset, export_settings) translation, rotation, scale = (None, None, None) trans[0], trans[1], trans[2] = gltf2_blender_math.round_if_near(trans[0], 0.0), gltf2_blender_math.round_if_near(trans[1], 0.0), \ gltf2_blender_math.round_if_near(trans[2], 0.0) @@ -476,7 +475,7 @@ def __gather_weights(blender_object, export_settings): def __get_correction_node(blender_object, export_settings): - correction_quaternion = gltf2_blender_extract.convert_swizzle_rotation( + correction_quaternion = __convert_swizzle_rotation( Quaternion((1.0, 0.0, 0.0), math.radians(-90.0)), export_settings) correction_quaternion = [correction_quaternion[1], correction_quaternion[2], correction_quaternion[3], correction_quaternion[0]] @@ -494,3 +493,31 @@ def __get_correction_node(blender_object, export_settings): translation=None, weights=None ) + + +def __convert_swizzle_location(loc, export_settings): + """Convert a location from Blender coordinate system to glTF coordinate system.""" + if export_settings[gltf2_blender_export_keys.YUP]: + return Vector((loc[0], loc[2], -loc[1])) + else: + return Vector((loc[0], loc[1], loc[2])) + + +def __convert_swizzle_rotation(rot, export_settings): + """ + Convert a quaternion rotation from Blender coordinate system to glTF coordinate system. + + 'w' is still at first position. + """ + if export_settings[gltf2_blender_export_keys.YUP]: + return Quaternion((rot[0], rot[1], rot[3], -rot[2])) + else: + return Quaternion((rot[0], rot[1], rot[2], rot[3])) + + +def __convert_swizzle_scale(scale, export_settings): + """Convert a scale from Blender coordinate system to glTF coordinate system.""" + if export_settings[gltf2_blender_export_keys.YUP]: + return Vector((scale[0], scale[2], scale[1])) + else: + return Vector((scale[0], scale[1], scale[2])) diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py index 61adea896ecdd1c9217a5f632a589bc6b9158088..e6a5881fc4b2cb8b79bcfc478263dac2c17291da 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py @@ -85,9 +85,9 @@ def __gather_position(blender_primitive, export_settings): def __gather_normal(blender_primitive, export_settings): if not export_settings[gltf2_blender_export_keys.NORMALS]: return {} - normal = blender_primitive["attributes"].get('NORMAL') - if not normal: + if 'NORMAL' not in blender_primitive["attributes"]: return {} + normal = blender_primitive["attributes"]['NORMAL'] return { "NORMAL": array_to_accessor( normal, @@ -100,9 +100,9 @@ def __gather_normal(blender_primitive, export_settings): def __gather_tangent(blender_primitive, export_settings): if not export_settings[gltf2_blender_export_keys.TANGENTS]: return {} - tangent = blender_primitive["attributes"].get('TANGENT') - if not tangent: + if 'TANGENT' not in blender_primitive["attributes"]: return {} + tangent = blender_primitive["attributes"]['TANGENT'] return { "TANGENT": array_to_accessor( tangent, diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py index 1a2ae00d4affd26fa72d1be52d9a048449d9b759..4fe498e10d26f59adaa2762c75e31ff715729c0a 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py @@ -14,6 +14,7 @@ import bpy from typing import List, Optional, Tuple +import numpy as np from .gltf2_blender_export_keys import NORMALS, MORPH_NORMAL, TANGENTS, MORPH_TANGENT, MORPH @@ -114,21 +115,23 @@ def __gather_indices(blender_primitive, blender_mesh, modifiers, export_settings # https://github.com/KhronosGroup/glTF/pull/1476/files # Also, UINT8 mode is not supported: # https://github.com/KhronosGroup/glTF/issues/1471 - max_index = max(indices) + max_index = indices.max() if max_index < 65535: component_type = gltf2_io_constants.ComponentType.UnsignedShort + indices = indices.astype(np.uint16, copy=False) elif max_index < 4294967295: component_type = gltf2_io_constants.ComponentType.UnsignedInt + indices = indices.astype(np.uint32, copy=False) else: print_console('ERROR', 'A mesh contains too many vertices (' + str(max_index) + ') and needs to be split before export.') return None element_type = gltf2_io_constants.DataType.Scalar - binary_data = gltf2_io_binary_data.BinaryData.from_list(indices, component_type) + binary_data = gltf2_io_binary_data.BinaryData(indices.tobytes()) return gltf2_blender_gather_accessors.gather_accessor( binary_data, component_type, - len(indices) // gltf2_io_constants.DataType.num_elements(element_type), + len(indices), None, None, element_type, @@ -156,7 +159,7 @@ def __gather_targets(blender_primitive, blender_mesh, modifiers, export_settings target_normal_id = 'MORPH_NORMAL_' + str(morph_index) target_tangent_id = 'MORPH_TANGENT_' + str(morph_index) - if blender_primitive["attributes"].get(target_position_id): + if blender_primitive["attributes"].get(target_position_id) is not None: target = {} internal_target_position = blender_primitive["attributes"][target_position_id] target["POSITION"] = gltf2_blender_gather_primitive_attributes.array_to_accessor( @@ -168,7 +171,7 @@ def __gather_targets(blender_primitive, blender_mesh, modifiers, export_settings if export_settings[NORMALS] \ and export_settings[MORPH_NORMAL] \ - and blender_primitive["attributes"].get(target_normal_id): + and blender_primitive["attributes"].get(target_normal_id) is not None: internal_target_normal = blender_primitive["attributes"][target_normal_id] target['NORMAL'] = gltf2_blender_gather_primitive_attributes.array_to_accessor( @@ -179,7 +182,7 @@ def __gather_targets(blender_primitive, blender_mesh, modifiers, export_settings if export_settings[TANGENTS] \ and export_settings[MORPH_TANGENT] \ - and blender_primitive["attributes"].get(target_tangent_id): + and blender_primitive["attributes"].get(target_tangent_id) is not None: internal_target_tangent = blender_primitive["attributes"][target_tangent_id] target['TANGENT'] = gltf2_blender_gather_primitive_attributes.array_to_accessor( internal_target_tangent,