From 564fdcdf7159013bc7087c9d12f2a034cfa3a945 Mon Sep 17 00:00:00 2001 From: Julien Duroure Date: Tue, 29 Mar 2022 17:44:36 +0200 Subject: glTF exporter: Manage skinning when some vertices are not weights at all --- io_scene_gltf2/__init__.py | 2 +- .../blender/exp/gltf2_blender_extract.py | 19 +++++- io_scene_gltf2/blender/exp/gltf2_blender_gather.py | 2 + .../blender/exp/gltf2_blender_gather_skins.py | 3 + .../blender/exp/gltf2_blender_gather_tree.py | 69 ++++++++++++++++++++++ io_scene_gltf2/io/imp/gltf2_io_binary.py | 31 ++++++++++ 6 files changed, 122 insertions(+), 4 deletions(-) diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index 11dbdce6..4ff7ff69 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, 2, 16), + "version": (3, 2, 17), 'blender': (3, 1, 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 d81bd706..98e2ac19 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_extract.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_extract.py @@ -82,7 +82,14 @@ def extract_primitives(blender_mesh, uuid_for_skined_data, blender_vertex_groups 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) + 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 @@ -535,6 +542,9 @@ def __get_colors(blender_mesh, color_i): def __get_bone_data(blender_mesh, skin, blender_vertex_groups): + + need_neutral_bone = False + 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] @@ -557,7 +567,10 @@ def __get_bone_data(blender_mesh, skin, blender_vertex_groups): 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) + 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) @@ -565,7 +578,7 @@ def __get_bone_data(blender_mesh, skin, blender_vertex_groups): # 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 + return vert_bones, num_joint_sets, need_neutral_bone def __zup2yup(array): diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather.py index f515da8c..b3f4fd2a 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather.py @@ -68,6 +68,8 @@ def __gather_scene(blender_scene, export_settings): if node is not None: scene.nodes.append(node) + vtree.add_neutral_bones() + export_user_extensions('gather_scene_hook', export_settings, scene, blender_scene) return scene diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_skins.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_skins.py index 136d654d..3e4673e1 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_skins.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_skins.py @@ -73,6 +73,9 @@ def __gather_inverse_bind_matrices(armature_uuid, export_settings): axis_basis_change = mathutils.Matrix( ((1.0, 0.0, 0.0, 0.0), (0.0, 0.0, 1.0, 0.0), (0.0, -1.0, 0.0, 0.0), (0.0, 0.0, 0.0, 1.0))) + # store matrix_world of armature in case we need to add a neutral bone + export_settings['vtree'].nodes[armature_uuid].matrix_world_armature = blender_armature_object.matrix_world.copy() + bones_uuid = export_settings['vtree'].get_all_bones(armature_uuid) def __collect_matrices(bone): inverse_bind_matrix = ( 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 643cbea0..3a7b0fa5 100644 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_tree.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_tree.py @@ -3,10 +3,17 @@ import bpy import uuid +import numpy as np from . import gltf2_blender_export_keys from io_scene_gltf2.io.exp.gltf2_io_user_extensions import export_user_extensions 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 class VExportNode: @@ -375,6 +382,68 @@ class VExportTree: n.armature = candidates[0].uuid del n.armature_needed + def add_neutral_bones(self): + for n in [n for n in self.nodes.values() if n.armature is not None and n.blender_type == VExportNode.OBJECT and hasattr(self.nodes[n.armature], "need_neutral_bone")]: #all skin meshes objects where neutral bone is needed + # First add a new node + + axis_basis_change = Matrix.Identity(4) + if self.export_settings[gltf2_blender_export_keys.YUP]: + axis_basis_change = Matrix(((1.0, 0.0, 0.0, 0.0), (0.0, 0.0, 1.0, 0.0), (0.0, -1.0, 0.0, 0.0), (0.0, 0.0, 0.0, 1.0))) + + trans, rot, sca = axis_basis_change.decompose() + translation, rotation, scale = (None, None, None) + if trans[0] != 0.0 or trans[1] != 0.0 or trans[2] != 0.0: + translation = [trans[0], trans[1], trans[2]] + if rot[0] != 1.0 or rot[1] != 0.0 or rot[2] != 0.0 or rot[3] != 0.0: + rotation = [rot[1], rot[2], rot[3], rot[0]] + if sca[0] != 1.0 or sca[1] != 1.0 or sca[2] != 1.0: + scale = [sca[0], sca[1], sca[2]] + neutral_bone = gltf2_io.Node( + camera=None, + children=None, + extensions=None, + extras=None, + matrix=None, + mesh=None, + name='neutral_bone', + rotation=rotation, + scale=scale, + skin=None, + translation=translation, + weights=None + ) + # Add it to child list of armature + self.nodes[n.armature].node.children.append(neutral_bone) + # Add it to joint list + n.node.skin.joints.append(neutral_bone) + + # Need to add an InverseBindMatrix + array = BinaryData.decode_accessor_internal(n.node.skin.inverse_bind_matrices) + + axis_basis_change = Matrix.Identity(4) + if self.export_settings[gltf2_blender_export_keys.YUP]: + axis_basis_change = Matrix( + ((1.0, 0.0, 0.0, 0.0), (0.0, 0.0, 1.0, 0.0), (0.0, -1.0, 0.0, 0.0), (0.0, 0.0, 0.0, 1.0))) + + inverse_bind_matrix = ( + axis_basis_change @ self.nodes[n.armature].matrix_world_armature).inverted_safe() + + matrix = [] + for column in range(0, 4): + for row in range(0, 4): + matrix.append(inverse_bind_matrix[row][column]) + + array = np.append(array, np.array([matrix]), axis=0) + binary_data = gltf2_io_binary_data.BinaryData.from_list(array.flatten(), gltf2_io_constants.ComponentType.Float) + n.node.skin.inverse_bind_matrices = gltf2_blender_gather_accessors.gather_accessor( + binary_data, + gltf2_io_constants.ComponentType.Float, + len(array.flatten()) // gltf2_io_constants.DataType.num_elements(gltf2_io_constants.DataType.Mat4), + None, + None, + gltf2_io_constants.DataType.Mat4, + self.export_settings + ) def get_unused_skins(self): from .gltf2_blender_gather_skins import gather_skin skins = [] diff --git a/io_scene_gltf2/io/imp/gltf2_io_binary.py b/io_scene_gltf2/io/imp/gltf2_io_binary.py index 21bbb41b..995fd3c9 100755 --- a/io_scene_gltf2/io/imp/gltf2_io_binary.py +++ b/io_scene_gltf2/io/imp/gltf2_io_binary.py @@ -77,6 +77,37 @@ class BinaryData(): return array + + @staticmethod + def decode_accessor_internal(accessor): + # Is use internally when accessor binary data is not yet in a glTF buffer_view + # MAT2/3 have special alignment requirements that aren't handled. But it + # doesn't matter because nothing uses them. + assert accessor.type not in ['MAT2', 'MAT3'] + + dtype = ComponentType.to_numpy_dtype(accessor.component_type) + component_nb = DataType.num_elements(accessor.type) + + buffer_data = accessor.buffer_view.data + + accessor_offset = accessor.byte_offset or 0 + buffer_data = buffer_data[accessor_offset:] + + bytes_per_elem = dtype(1).nbytes + default_stride = bytes_per_elem * component_nb + stride = default_stride + + array = np.frombuffer( + buffer_data, + dtype=np.dtype(dtype).newbyteorder('<'), + count=accessor.count * component_nb, + ) + array = array.reshape(accessor.count, component_nb) + + return array + + + @staticmethod def decode_accessor_obj(gltf, accessor): # MAT2/3 have special alignment requirements that aren't handled. But it -- cgit v1.2.3