diff options
author | Julien Duroure <julien.duroure@gmail.com> | 2020-01-24 00:10:48 +0300 |
---|---|---|
committer | Julien Duroure <julien.duroure@gmail.com> | 2020-01-24 00:10:48 +0300 |
commit | 75855d723895e25da855087bccbd0266773bad15 (patch) | |
tree | c4d159ce2e5921f2d9239cc641c02746f173d5ed | |
parent | b3b274c5739de01685572032ac26ac5dcb50b950 (diff) |
glTF importer: fix skinning & hierarchy issues
See https://github.com/KhronosGroup/glTF-Blender-IO/pull/857 for details
17 files changed, 763 insertions, 864 deletions
diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index 092bb830..f051b08e 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, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors', - "version": (1, 2, 4), + "version": (1, 2, 5), 'blender': (2, 81, 6), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', diff --git a/io_scene_gltf2/blender/com/gltf2_blender_conversion.py b/io_scene_gltf2/blender/com/gltf2_blender_conversion.py index 6d9a901a..fccfce95 100755 --- a/io_scene_gltf2/blender/com/gltf2_blender_conversion.py +++ b/io_scene_gltf2/blender/com/gltf2_blender_conversion.py @@ -15,38 +15,6 @@ from mathutils import Matrix, Quaternion from math import sqrt, sin, cos -def matrix_gltf_to_blender(mat_input): - """Matrix from glTF format to Blender format.""" - mat = Matrix([mat_input[0:4], mat_input[4:8], mat_input[8:12], mat_input[12:16]]) - mat.transpose() - return mat - -def loc_gltf_to_blender(loc): - """Location.""" - return loc - -def scale_gltf_to_blender(scale): - """Scaling.""" - return scale - -def quaternion_gltf_to_blender(q): - """Quaternion from glTF to Blender.""" - return Quaternion([q[3], q[0], q[1], q[2]]) - -def scale_to_matrix(scale): - """Scale to matrix.""" - mat = Matrix() - for i in range(3): - mat[i][i] = scale[i] - - return mat - -def correction_rotation(): - """Correction of Rotation.""" - # Correction is needed for lamps, because Yup2Zup is not written in vertices - # and lamps has no vertices :) - return Quaternion((sqrt(2)/2, -sqrt(2)/2, 0.0, 0.0)).to_matrix().to_4x4() - def texture_transform_blender_to_gltf(mapping_transform): """ Converts the offset/rotation/scale from a Mapping node applied in Blender's diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_animation.py b/io_scene_gltf2/blender/imp/gltf2_blender_animation.py index 6394145d..ed1d938d 100755 --- a/io_scene_gltf2/blender/imp/gltf2_blender_animation.py +++ b/io_scene_gltf2/blender/imp/gltf2_blender_animation.py @@ -18,6 +18,7 @@ from .gltf2_blender_animation_bone import BlenderBoneAnim from .gltf2_blender_animation_node import BlenderNodeAnim from .gltf2_blender_animation_weight import BlenderWeightAnim from .gltf2_blender_animation_utils import restore_animation_on_object +from .gltf2_blender_vnode import VNode class BlenderAnimation(): @@ -26,34 +27,36 @@ class BlenderAnimation(): raise RuntimeError("%s should not be instantiated" % cls) @staticmethod - def anim(gltf, anim_idx, node_idx): + def anim(gltf, anim_idx, vnode_id): """Dispatch Animation to bone or object.""" - if gltf.data.nodes[node_idx].is_joint: - BlenderBoneAnim.anim(gltf, anim_idx, node_idx) - else: - BlenderNodeAnim.anim(gltf, anim_idx, node_idx) - BlenderWeightAnim.anim(gltf, anim_idx, node_idx) + if isinstance(vnode_id, int): + if gltf.vnodes[vnode_id].type == VNode.Bone: + BlenderBoneAnim.anim(gltf, anim_idx, vnode_id) + elif gltf.vnodes[vnode_id].type == VNode.Object: + BlenderNodeAnim.anim(gltf, anim_idx, vnode_id) - if gltf.data.nodes[node_idx].children: - for child in gltf.data.nodes[node_idx].children: - BlenderAnimation.anim(gltf, anim_idx, child) + BlenderWeightAnim.anim(gltf, anim_idx, vnode_id) + + for child in gltf.vnodes[vnode_id].children: + BlenderAnimation.anim(gltf, anim_idx, child) @staticmethod - def restore_animation(gltf, node_idx, animation_name): + def restore_animation(gltf, vnode_id, animation_name): """Restores the actions for an animation by its track name on the subtree starting at node_idx.""" - node = gltf.data.nodes[node_idx] + vnode = gltf.vnodes[vnode_id] - if node.is_joint: - obj = bpy.data.objects[gltf.data.skins[node.skin_id].blender_armature_name] - else: - obj = bpy.data.objects[node.blender_object] + obj = None + if vnode.type == VNode.Bone: + obj = gltf.vnodes[vnode.bone_arma].blender_object + elif vnode.type == VNode.Object: + obj = vnode.blender_object - restore_animation_on_object(obj, animation_name) - if obj.data and hasattr(obj.data, 'shape_keys'): - restore_animation_on_object(obj.data.shape_keys, animation_name) + if obj is not None: + restore_animation_on_object(obj, animation_name) + if obj.data and hasattr(obj.data, 'shape_keys'): + restore_animation_on_object(obj.data.shape_keys, animation_name) - if gltf.data.nodes[node_idx].children: - for child in gltf.data.nodes[node_idx].children: - BlenderAnimation.restore_animation(gltf, child, animation_name) + for child in gltf.vnodes[vnode_id].children: + BlenderAnimation.restore_animation(gltf, child, animation_name) diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_animation_bone.py b/io_scene_gltf2/blender/imp/gltf2_blender_animation_bone.py index 465a801d..7e32487c 100755 --- a/io_scene_gltf2/blender/imp/gltf2_blender_animation_bone.py +++ b/io_scene_gltf2/blender/imp/gltf2_blender_animation_bone.py @@ -14,51 +14,56 @@ import json import bpy -from mathutils import Matrix +from mathutils import Vector -from ..com.gltf2_blender_conversion import loc_gltf_to_blender, quaternion_gltf_to_blender, scale_to_matrix from ...io.imp.gltf2_io_binary import BinaryData from .gltf2_blender_animation_utils import simulate_stash, make_fcurve +# The glTF curves store the value of the final transform, but in Blender +# curves animate a pose bone that is relative to the edit bone +# +# Final = EditBone * PoseBone +# where +# Final = Trans[ft] Rot[fr] Scale[fs] +# EditBone = Trans[et] Rot[er] (edit bones have no scale) +# PoseBone = Trans[pt] Rot[pr] Scale[ps] +# +# Solving for the PoseBone gives the change we need to apply to the curves +# +# pt = Rot[er^{-1}] (ft - et) +# pr = er^{-1} fr +# ps = fs + class BlenderBoneAnim(): """Blender Bone Animation.""" def __new__(cls, *args, **kwargs): raise RuntimeError("%s should not be instantiated" % cls) @staticmethod - def parse_translation_channel(gltf, node, obj, bone, channel, animation): + def parse_translation_channel(gltf, vnode, obj, bone, channel, animation): """Manage Location animation.""" blender_path = "pose.bones[" + json.dumps(bone.name) + "].location" group_name = bone.name keys = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].input) values = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].output) - inv_bind_matrix = node.blender_bone_matrix.to_quaternion().to_matrix().to_4x4().inverted() \ - @ Matrix.Translation(node.blender_bone_matrix.to_translation()).inverted() if animation.samplers[channel.sampler].interpolation == "CUBICSPLINE": # TODO manage tangent? translation_keyframes = ( - loc_gltf_to_blender(values[idx * 3 + 1]) + gltf.loc_gltf_to_blender(values[idx * 3 + 1]) for idx in range(0, len(keys)) ) else: - translation_keyframes = (loc_gltf_to_blender(vals) for vals in values) - if node.parent is None: - parent_mat = Matrix() - else: - if not gltf.data.nodes[node.parent].is_joint: - parent_mat = Matrix() - else: - parent_mat = gltf.data.nodes[node.parent].blender_bone_matrix - - # Pose is in object (armature) space and it's value if the offset from the bind pose - # (which is also in object space) - # Scale is not taken into account + translation_keyframes = (gltf.loc_gltf_to_blender(vals) for vals in values) + + bind_trans, bind_rot, _ = vnode.trs + bind_rot_inv = bind_rot.conjugated() + final_translations = [ - inv_bind_matrix @ (parent_mat @ Matrix.Translation(translation_keyframe)).to_translation() - for translation_keyframe in translation_keyframes + bind_rot_inv @ (trans - bind_trans) + for trans in translation_keyframes ] BlenderBoneAnim.fill_fcurves( @@ -71,48 +76,31 @@ class BlenderBoneAnim(): ) @staticmethod - def parse_rotation_channel(gltf, node, obj, bone, channel, animation): + def parse_rotation_channel(gltf, vnode, obj, bone, channel, animation): """Manage rotation animation.""" blender_path = "pose.bones[" + json.dumps(bone.name) + "].rotation_quaternion" group_name = bone.name keys = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].input) values = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].output) - bind_rotation = node.blender_bone_matrix.to_quaternion() if animation.samplers[channel.sampler].interpolation == "CUBICSPLINE": # TODO manage tangent? quat_keyframes = [ - quaternion_gltf_to_blender(values[idx * 3 + 1]) + gltf.quaternion_gltf_to_blender(values[idx * 3 + 1]) for idx in range(0, len(keys)) ] else: - quat_keyframes = [quaternion_gltf_to_blender(vals) for vals in values] + quat_keyframes = [gltf.quaternion_gltf_to_blender(vals) for vals in values] + _, bind_rot, _ = vnode.trs + bind_rot_inv = bind_rot.conjugated() - if node.parent is None: - final_rots = [ - bind_rotation.inverted() @ quat_keyframe - for quat_keyframe in quat_keyframes - ] - else: - if not gltf.data.nodes[node.parent].is_joint: - parent_mat = Matrix() - else: - parent_mat = gltf.data.nodes[node.parent].blender_bone_matrix - - if parent_mat != parent_mat.inverted(): - final_rots = [ - bind_rotation.rotation_difference( - (parent_mat @ quat_keyframe.to_matrix().to_4x4()).to_quaternion() - ) - for quat_keyframe in quat_keyframes - ] - else: - final_rots = [ - bind_rotation.rotation_difference(quat_keyframe) - for quat_keyframe in quat_keyframes - ] + + final_rots = [ + bind_rot_inv @ rot + for rot in quat_keyframes + ] # Manage antipodal quaternions for i in range(1, len(final_rots)): @@ -129,38 +117,22 @@ class BlenderBoneAnim(): ) @staticmethod - def parse_scale_channel(gltf, node, obj, bone, channel, animation): + def parse_scale_channel(gltf, vnode, obj, bone, channel, animation): """Manage scaling animation.""" blender_path = "pose.bones[" + json.dumps(bone.name) + "].scale" group_name = bone.name keys = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].input) values = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].output) - bind_scale = scale_to_matrix(node.blender_bone_matrix.to_scale()) if animation.samplers[channel.sampler].interpolation == "CUBICSPLINE": # TODO manage tangent? - scale_mats = ( - scale_to_matrix(loc_gltf_to_blender(values[idx * 3 + 1])) - for idx in range(0, len(keys)) - ) - else: - scale_mats = (scale_to_matrix(loc_gltf_to_blender(vals)) for vals in values) - if node.parent is None: final_scales = [ - (bind_scale.inverted() @ scale_mat).to_scale() - for scale_mat in scale_mats + gltf.scale_gltf_to_blender(values[idx * 3 + 1]) + for idx in range(0, len(keys)) ] else: - if not gltf.data.nodes[node.parent].is_joint: - parent_mat = Matrix() - else: - parent_mat = gltf.data.nodes[node.parent].blender_bone_matrix - - final_scales = [ - (bind_scale.inverted() @ scale_to_matrix(parent_mat.to_scale()) @ scale_mat).to_scale() - for scale_mat in scale_mats - ] + final_scales = [gltf.scale_gltf_to_blender(vals) for vals in values] BlenderBoneAnim.fill_fcurves( obj.animation_data.action, @@ -194,21 +166,21 @@ class BlenderBoneAnim(): def anim(gltf, anim_idx, node_idx): """Manage animation.""" node = gltf.data.nodes[node_idx] - blender_armature_name = gltf.data.skins[node.skin_id].blender_armature_name - obj = bpy.data.objects[blender_armature_name] - bone = obj.pose.bones[node.blender_bone_name] + vnode = gltf.vnodes[node_idx] + obj = gltf.vnodes[vnode.bone_arma].blender_object + bone = obj.pose.bones[vnode.blender_bone_name] if anim_idx not in node.animations.keys(): return animation = gltf.data.animations[anim_idx] - action = gltf.arma_cache.get(blender_armature_name) + action = gltf.action_cache.get(obj.name) if not action: name = animation.track_name + "_" + obj.name action = bpy.data.actions.new(name) gltf.needs_stash.append((obj, animation.track_name, action)) - gltf.arma_cache[blender_armature_name] = action + gltf.action_cache[obj.name] = action if not obj.animation_data: obj.animation_data_create() @@ -218,11 +190,11 @@ class BlenderBoneAnim(): channel = animation.channels[channel_idx] if channel.target.path == "translation": - BlenderBoneAnim.parse_translation_channel(gltf, node, obj, bone, channel, animation) + BlenderBoneAnim.parse_translation_channel(gltf, vnode, obj, bone, channel, animation) elif channel.target.path == "rotation": - BlenderBoneAnim.parse_rotation_channel(gltf, node, obj, bone, channel, animation) + BlenderBoneAnim.parse_rotation_channel(gltf, vnode, obj, bone, channel, animation) elif channel.target.path == "scale": - BlenderBoneAnim.parse_scale_channel(gltf, node, obj, bone, channel, animation) + BlenderBoneAnim.parse_scale_channel(gltf, vnode, obj, bone, channel, animation) diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_animation_node.py b/io_scene_gltf2/blender/imp/gltf2_blender_animation_node.py index 23a37228..6a0959c2 100755 --- a/io_scene_gltf2/blender/imp/gltf2_blender_animation_node.py +++ b/io_scene_gltf2/blender/imp/gltf2_blender_animation_node.py @@ -15,10 +15,9 @@ import bpy from mathutils import Vector -from ..com.gltf2_blender_conversion import loc_gltf_to_blender, quaternion_gltf_to_blender, scale_gltf_to_blender -from ..com.gltf2_blender_conversion import correction_rotation from ...io.imp.gltf2_io_binary import BinaryData from .gltf2_blender_animation_utils import simulate_stash, make_fcurve +from .gltf2_blender_vnode import VNode class BlenderNodeAnim(): @@ -30,7 +29,8 @@ class BlenderNodeAnim(): def anim(gltf, anim_idx, node_idx): """Manage animation.""" node = gltf.data.nodes[node_idx] - obj = bpy.data.objects[node.blender_object] + vnode = gltf.vnodes[node_idx] + obj = vnode.blender_object fps = bpy.context.scene.render.fps animation = gltf.data.animations[anim_idx] @@ -45,9 +45,12 @@ class BlenderNodeAnim(): else: return - name = animation.track_name + "_" + obj.name - action = bpy.data.actions.new(name) - gltf.needs_stash.append((obj, animation.track_name, action)) + action = gltf.action_cache.get(obj.name) + if not action: + name = animation.track_name + "_" + obj.name + action = bpy.data.actions.new(name) + gltf.needs_stash.append((obj, animation.track_name, action)) + gltf.action_cache[obj.name] = action if not obj.animation_data: obj.animation_data_create() @@ -62,10 +65,6 @@ class BlenderNodeAnim(): if channel.target.path not in ['translation', 'rotation', 'scale']: continue - # There is an animation on object - # We can't remove Yup2Zup object - gltf.animation_object = True - if animation.samplers[channel.sampler].interpolation == "CUBICSPLINE": # TODO manage tangent? values = [values[idx * 3 + 1] for idx in range(0, len(keys))] @@ -74,20 +73,20 @@ class BlenderNodeAnim(): blender_path = "location" group_name = "Location" num_components = 3 - values = [loc_gltf_to_blender(vals) for vals in values] + + if vnode.parent is not None and gltf.vnodes[vnode.parent].type == VNode.Bone: + # Nodes with a bone parent need to be translated + # backwards by their bone length (always 1 currently) + off = Vector((0, -1, 0)) + values = [gltf.loc_gltf_to_blender(vals) + off for vals in values] + else: + values = [gltf.loc_gltf_to_blender(vals) for vals in values] elif channel.target.path == "rotation": blender_path = "rotation_quaternion" group_name = "Rotation" num_components = 4 - if node.correction_needed is True: - values = [ - (quaternion_gltf_to_blender(vals).to_matrix().to_4x4() @ correction_rotation()).to_quaternion() - for vals in values - ] - else: - values = [quaternion_gltf_to_blender(vals) for vals in values] - + values = [gltf.quaternion_gltf_to_blender(vals) for vals in values] # Manage antipodal quaternions for i in range(1, len(values)): @@ -98,7 +97,7 @@ class BlenderNodeAnim(): blender_path = "scale" group_name = "Scale" num_components = 3 - values = [scale_gltf_to_blender(vals) for vals in values] + values = [gltf.scale_gltf_to_blender(vals) for vals in values] coords = [0] * (2 * len(keys)) coords[::2] = (key[0] * fps for key in keys) diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_animation_weight.py b/io_scene_gltf2/blender/imp/gltf2_blender_animation_weight.py index bd8586ab..c89e4dfe 100644 --- a/io_scene_gltf2/blender/imp/gltf2_blender_animation_weight.py +++ b/io_scene_gltf2/blender/imp/gltf2_blender_animation_weight.py @@ -25,10 +25,16 @@ class BlenderWeightAnim(): raise RuntimeError("%s should not be instantiated" % cls) @staticmethod - def anim(gltf, anim_idx, node_idx): + def anim(gltf, anim_idx, vnode_id): """Manage animation.""" + vnode = gltf.vnodes[vnode_id] + + node_idx = vnode.mesh_node_idx + if node_idx is None: + return + node = gltf.data.nodes[node_idx] - obj = bpy.data.objects[node.blender_object] + obj = vnode.blender_object fps = bpy.context.scene.render.fps animation = gltf.data.animations[anim_idx] diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_camera.py b/io_scene_gltf2/blender/imp/gltf2_blender_camera.py index ec5f0ee4..78946858 100755 --- a/io_scene_gltf2/blender/imp/gltf2_blender_camera.py +++ b/io_scene_gltf2/blender/imp/gltf2_blender_camera.py @@ -44,9 +44,5 @@ class BlenderCamera(): cam.clip_end = pycamera.zfar obj = bpy.data.objects.new(pycamera.name, cam) - if gltf.blender_active_collection is not None: - bpy.data.collections[gltf.blender_active_collection].objects.link(obj) - else: - bpy.data.scenes[gltf.blender_scene].collection.objects.link(obj) return obj diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_gltf.py b/io_scene_gltf2/blender/imp/gltf2_blender_gltf.py index 5904a974..7b259759 100755 --- a/io_scene_gltf2/blender/imp/gltf2_blender_gltf.py +++ b/io_scene_gltf2/blender/imp/gltf2_blender_gltf.py @@ -13,8 +13,8 @@ # limitations under the License. import bpy +from mathutils import Vector, Quaternion, Matrix from .gltf2_blender_scene import BlenderScene -from ...io.com.gltf2_io_trs import TRS class BlenderGlTF(): @@ -25,79 +25,52 @@ class BlenderGlTF(): @staticmethod def create(gltf): """Create glTF main method.""" - if bpy.context.scene.render.engine not in ['CYCLES', 'BLENDER_EEVEE']: - bpy.context.scene.render.engine = 'BLENDER_EEVEE' + BlenderGlTF.set_convert_functions(gltf) BlenderGlTF.pre_compute(gltf) + BlenderScene.create(gltf) + + @staticmethod + def set_convert_functions(gltf): + yup2zup = bpy.app.debug_value != 100 + + if yup2zup: + # glTF Y-Up space --> Blender Z-up space + # X,Y,Z --> X,-Z,Y + def convert_loc(x): return Vector([x[0], -x[2], x[1]]) + def convert_quat(q): return Quaternion([q[3], q[0], -q[2], q[1]]) + def convert_normal(n): return Vector([n[0], -n[2], n[1]]) + def convert_scale(s): return Vector([s[0], s[2], s[1]]) + def convert_matrix(m): + return Matrix([ + [ m[0], -m[ 8], m[4], m[12]], + [-m[2], m[10], -m[6], -m[14]], + [ m[1], -m[ 9], m[5], m[13]], + [ m[3], -m[11], m[7], m[15]], + ]) + + # Correction for cameras and lights. + # glTF: right = +X, forward = -Z, up = +Y + # glTF after Yup2Zup: right = +X, forward = +Y, up = +Z + # Blender: right = +X, forward = -Z, up = +Y + # Need to carry Blender --> glTF after Yup2Zup + gltf.camera_correction = Quaternion((2**0.5/2, 2**0.5/2, 0.0, 0.0)) - gltf.display_current_node = 0 - if gltf.data.nodes is not None: - gltf.display_total_nodes = len(gltf.data.nodes) - else: - gltf.display_total_nodes = "?" - - active_object_name_at_end = None - if gltf.data.scenes is not None: - for scene_idx, scene in enumerate(gltf.data.scenes): - BlenderScene.create(gltf, scene_idx) - # keep active object name if needed (to be able to set as active object at end) - if gltf.data.scene is not None: - if scene_idx == gltf.data.scene: - active_object_name_at_end = bpy.context.view_layer.objects.active.name - else: - if scene_idx == 0: - active_object_name_at_end = bpy.context.view_layer.objects.active.name else: - # special case where there is no scene in glTF file - # generate all objects in current scene - BlenderScene.create(gltf, None) - active_object_name_at_end = bpy.context.view_layer.objects.active.name - - # Armature correction - # Try to detect bone chains, and set bone lengths - # To detect if a bone is in a chain, we try to detect if a bone head is aligned - # with parent_bone : - # Parent bone defined a line (between head & tail) - # Bone head defined a point - # Calcul of distance between point and line - # If < threshold --> In a chain - # Based on an idea of @Menithal, but added alignment detection to avoid some bad cases - - threshold = 0.001 - for armobj in [obj for obj in bpy.data.objects if obj.type == "ARMATURE"]: - # Take into account only armature from this scene - if armobj.name not in bpy.context.view_layer.objects: - continue - bpy.context.view_layer.objects.active = armobj - armature = armobj.data - bpy.ops.object.mode_set(mode="EDIT") - for bone in armature.edit_bones: - if bone.parent is None: - continue - - parent = bone.parent - - # case where 2 bones are aligned (not in chain, same head) - if (bone.head - parent.head).length < threshold: - continue - - u = (parent.tail - parent.head).normalized() - point = bone.head - distance = ((point - parent.head).cross(u)).length / u.length - if distance < threshold: - save_parent_direction = (parent.tail - parent.head).normalized().copy() - save_parent_tail = parent.tail.copy() - parent.tail = bone.head - - # case where 2 bones are aligned (not in chain, same head) - # bone is no more is same direction - if (parent.tail - parent.head).normalized().dot(save_parent_direction) < 0.9: - parent.tail = save_parent_tail - - bpy.ops.object.mode_set(mode="OBJECT") - - # Set active object - if active_object_name_at_end is not None: - bpy.context.view_layer.objects.active = bpy.data.objects[active_object_name_at_end] + def convert_loc(x): return Vector(x) + def convert_quat(q): return Quaternion([q[3], q[0], q[1], q[2]]) + def convert_normal(n): return Vector(n) + def convert_scale(s): return Vector(s) + def convert_matrix(m): + return Matrix([m[0::4], m[1::4], m[2::4], m[3::4]]) + + # Same convention, no correction needed. + gltf.camera_correction = None + + gltf.loc_gltf_to_blender = convert_loc + gltf.quaternion_gltf_to_blender = convert_quat + gltf.normal_gltf_to_blender = convert_normal + gltf.scale_gltf_to_blender = convert_scale + gltf.matrix_gltf_to_blender = convert_matrix @staticmethod def pre_compute(gltf): @@ -208,45 +181,6 @@ class BlenderGlTF(): # Lights management node.correction_needed = False - # transform management - if node.matrix: - node.transform = node.matrix - continue - - # No matrix, but TRS - mat = [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0] # init - - if node.scale: - mat = TRS.scale_to_matrix(node.scale) - - if node.rotation: - q_mat = TRS.quaternion_to_matrix(node.rotation) - mat = TRS.matrix_multiply(q_mat, mat) - - if node.translation: - loc_mat = TRS.translation_to_matrix(node.translation) - mat = TRS.matrix_multiply(loc_mat, mat) - - node.transform = mat - - - # joint management - for node_idx, node in enumerate(gltf.data.nodes): - is_joint, skin_idx = gltf.is_node_joint(node_idx) - if is_joint: - node.is_joint = True - node.skin_id = skin_idx - else: - node.is_joint = False - - if gltf.data.skins: - for skin_id, skin in enumerate(gltf.data.skins): - # init blender values - skin.blender_armature_name = None - # if skin.skeleton and skin.skeleton not in skin.joints: - # gltf.data.nodes[skin.skeleton].is_joint = True - # gltf.data.nodes[skin.skeleton].skin_id = skin_id - # Dispatch animation if gltf.data.animations: for node_idx, node in enumerate(gltf.data.nodes): @@ -274,7 +208,7 @@ class BlenderGlTF(): # Meshes if gltf.data.meshes: for mesh in gltf.data.meshes: - mesh.blender_name = None + mesh.blender_name = {} # cache Blender mesh (keyed by skin_idx) mesh.is_weight_animated = False # Calculate names for each mesh's shapekeys diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_light.py b/io_scene_gltf2/blender/imp/gltf2_blender_light.py index 6213091e..e75864f9 100644 --- a/io_scene_gltf2/blender/imp/gltf2_blender_light.py +++ b/io_scene_gltf2/blender/imp/gltf2_blender_light.py @@ -42,11 +42,6 @@ class BlenderLight(): # TODO range - if gltf.blender_active_collection is not None: - bpy.data.collections[gltf.blender_active_collection].objects.link(obj) - else: - bpy.data.scenes[gltf.blender_scene].collection.objects.link(obj) - set_extras(obj.data, pylight.get('extras')) return obj diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py b/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py index 318e7049..e069069e 100755 --- a/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py +++ b/io_scene_gltf2/blender/imp/gltf2_blender_mesh.py @@ -20,7 +20,6 @@ from ..com.gltf2_blender_extras import set_extras from .gltf2_blender_material import BlenderMaterial from .gltf2_blender_primitive import BlenderPrimitive from ...io.imp.gltf2_io_binary import BinaryData -from ..com.gltf2_blender_conversion import loc_gltf_to_blender class BlenderMesh(): @@ -29,7 +28,7 @@ class BlenderMesh(): raise RuntimeError("%s should not be instantiated" % cls) @staticmethod - def create(gltf, mesh_idx, node_idx, parent): + def create(gltf, mesh_idx, skin_idx): """Mesh creation.""" pymesh = gltf.data.meshes[mesh_idx] @@ -64,7 +63,7 @@ class BlenderMesh(): materials.append(material.name) material_idx = len(materials) - 1 - BlenderPrimitive.add_primitive_to_bmesh(gltf, bme, pymesh, prim, material_idx) + BlenderPrimitive.add_primitive_to_bmesh(gltf, bme, pymesh, prim, skin_idx, material_idx) name = pymesh.name or 'Mesh_' + str(mesh_idx) mesh = bpy.data.meshes.new(name) @@ -76,7 +75,7 @@ class BlenderMesh(): set_extras(mesh, pymesh.extras, exclude=['targetNames']) - pymesh.blender_name = mesh.name + pymesh.blender_name[skin_idx] = mesh.name # Clear accessor cache after all primitives are done gltf.accessor_cache = {} @@ -84,7 +83,7 @@ class BlenderMesh(): return mesh @staticmethod - def set_mesh(gltf, pymesh, mesh, obj): + def set_mesh(gltf, pymesh, obj): """Sets mesh data after creation.""" # set default weights for shape keys, and names, if not set by convention on extras data if pymesh.weights is not None: diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_node.py b/io_scene_gltf2/blender/imp/gltf2_blender_node.py index a02514de..2f5d893a 100755 --- a/io_scene_gltf2/blender/imp/gltf2_blender_node.py +++ b/io_scene_gltf2/blender/imp/gltf2_blender_node.py @@ -13,13 +13,12 @@ # limitations under the License. import bpy +from mathutils import Vector from ..com.gltf2_blender_extras import set_extras from .gltf2_blender_mesh import BlenderMesh from .gltf2_blender_camera import BlenderCamera -from .gltf2_blender_skin import BlenderSkin from .gltf2_blender_light import BlenderLight -from ..com.gltf2_blender_conversion import scale_to_matrix, matrix_gltf_to_blender, correction_rotation - +from .gltf2_blender_vnode import VNode class BlenderNode(): """Blender Node.""" @@ -27,216 +26,161 @@ class BlenderNode(): raise RuntimeError("%s should not be instantiated" % cls) @staticmethod - def create(gltf, node_idx, parent): - """Node creation.""" - pynode = gltf.data.nodes[node_idx] - - # Blender attributes initialization - pynode.blender_object = "" - pynode.parent = parent + def create_vnode(gltf, vnode_id): + """Create VNode and all its descendants.""" + vnode = gltf.vnodes[vnode_id] + name = vnode.name gltf.display_current_node += 1 if bpy.app.debug_value == 101: - gltf.log.critical("Node " + str(gltf.display_current_node) + " of " + str(gltf.display_total_nodes) + " (idx " + str(node_idx) + ")") + gltf.log.critical("Node %d of %d (id %s)", gltf.display_current_node, len(gltf.vnodes), vnode_id) - if pynode.mesh is not None: + if vnode.type == VNode.Object: + BlenderNode.create_object(gltf, vnode_id) - instance = False - if gltf.data.meshes[pynode.mesh].blender_name is not None: - # Mesh is already created, only create instance - # Except is current node is animated with path weight - # Or if previous instance is animation at node level - if pynode.weight_animation is True: - instance = False - else: - if gltf.data.meshes[pynode.mesh].is_weight_animated is True: - instance = False - else: - instance = True - mesh = bpy.data.meshes[gltf.data.meshes[pynode.mesh].blender_name] - - if instance is False: - if pynode.name: - gltf.log.info("Blender create Mesh node " + pynode.name) - else: - gltf.log.info("Blender create Mesh node") + elif vnode.type == VNode.Bone: + BlenderNode.create_bone(gltf, vnode_id) - mesh = BlenderMesh.create(gltf, pynode.mesh, node_idx, parent) + elif vnode.type == VNode.DummyRoot: + # Don't actually create this + vnode.blender_object = None - if pynode.weight_animation is True: - # flag this mesh instance as created only for this node, because of weight animation - gltf.data.meshes[pynode.mesh].is_weight_animated = True + for child in vnode.children: + BlenderNode.create_vnode(gltf, child) - if pynode.name: - name = pynode.name - else: - # Take mesh name if exist - if gltf.data.meshes[pynode.mesh].name: - name = gltf.data.meshes[pynode.mesh].name - else: - name = "Object_" + str(node_idx) + @staticmethod + def create_object(gltf, vnode_id): + vnode = gltf.vnodes[vnode_id] + + if vnode.mesh_node_idx is not None: + pynode = gltf.data.nodes[vnode.mesh_node_idx] + obj = BlenderNode.create_mesh_object(gltf, pynode, name=vnode.name) + elif vnode.camera_node_idx is not None: + pynode = gltf.data.nodes[vnode.camera_node_idx] + obj = BlenderCamera.create(gltf, pynode.camera) + elif vnode.light_node_idx is not None: + pynode = gltf.data.nodes[vnode.light_node_idx] + obj = BlenderLight.create(gltf, pynode.extensions['KHR_lights_punctual']['light']) + elif vnode.is_arma: + armature = bpy.data.armatures.new(vnode.arma_name) + obj = bpy.data.objects.new(vnode.name, armature) + else: + obj = bpy.data.objects.new(vnode.name, None) + + vnode.blender_object = obj - obj = bpy.data.objects.new(name, mesh) + # Set extras (if came from a glTF node) + if isinstance(vnode_id, int): + pynode = gltf.data.nodes[vnode_id] set_extras(obj, pynode.extras) - obj.rotation_mode = 'QUATERNION' - if gltf.blender_active_collection is not None: - bpy.data.collections[gltf.blender_active_collection].objects.link(obj) - else: - bpy.data.scenes[gltf.blender_scene].collection.objects.link(obj) - # Transforms apply only if this mesh is not skinned - # See implementation node of gltf2 specification - if not (pynode.mesh is not None and pynode.skin is not None): - BlenderNode.set_transforms(gltf, node_idx, pynode, obj, parent) - pynode.blender_object = obj.name - BlenderNode.set_parent(gltf, obj, parent) + # Set transform + trans, rot, scale = vnode.trs + obj.location = trans + obj.rotation_mode = 'QUATERNION' + obj.rotation_quaternion = rot + obj.scale = scale - if instance == False: - BlenderMesh.set_mesh(gltf, gltf.data.meshes[pynode.mesh], mesh, obj) + # Set parent + if vnode.parent is not None: + parent_vnode = gltf.vnodes[vnode.parent] + if parent_vnode.type == VNode.Object: + obj.parent = parent_vnode.blender_object + elif parent_vnode.type == VNode.Bone: + arma_vnode = gltf.vnodes[parent_vnode.bone_arma] + obj.parent = arma_vnode.blender_object + obj.parent_type = 'BONE' + obj.parent_bone = parent_vnode.blender_bone_name - if pynode.children: - for child_idx in pynode.children: - BlenderNode.create(gltf, child_idx, node_idx) + # Nodes with a bone parent need to be translated + # backwards by their bone length (always 1 currently) + obj.location += Vector((0, -1, 0)) - return + bpy.data.scenes[gltf.blender_scene].collection.objects.link(obj) - if pynode.camera is not None: - if pynode.name: - gltf.log.info("Blender create Camera node " + pynode.name) - else: - gltf.log.info("Blender create Camera node") - obj = BlenderCamera.create(gltf, pynode.camera) - set_extras(obj, pynode.extras) - BlenderNode.set_transforms(gltf, node_idx, pynode, obj, parent) # TODO default rotation of cameras ? - pynode.blender_object = obj.name - BlenderNode.set_parent(gltf, obj, parent) + return obj - if pynode.children: - for child_idx in pynode.children: - BlenderNode.create(gltf, child_idx, node_idx) + @staticmethod + def create_bone(gltf, vnode_id): + vnode = gltf.vnodes[vnode_id] + blender_arma = gltf.vnodes[vnode.bone_arma].blender_object + armature = blender_arma.data + + # Switch into edit mode to create edit bone + if bpy.context.mode != 'OBJECT': + bpy.ops.object.mode_set(mode='OBJECT') + bpy.context.window.scene = bpy.data.scenes[gltf.blender_scene] + bpy.context.view_layer.objects.active = blender_arma + bpy.ops.object.mode_set(mode="EDIT") + editbone = armature.edit_bones.new(vnode.name) + vnode.blender_bone_name = editbone.name + + # Set extras (if came from a glTF node) + if isinstance(vnode_id, int): + pynode = gltf.data.nodes[vnode_id] + set_extras(editbone, pynode.extras) + + # TODO + editbone.use_connect = False + + # Give the position of the bone in armature space + arma_mat = vnode.bone_arma_mat + editbone.head = arma_mat @ Vector((0, 0, 0)) + editbone.tail = arma_mat @ Vector((0, 1, 0)) + editbone.align_roll(arma_mat @ Vector((0, 0, 1)) - editbone.head) + + # Set parent + parent_vnode = gltf.vnodes[vnode.parent] + if parent_vnode.type == VNode.Bone: + editbone.parent = armature.edit_bones[parent_vnode.blender_bone_name] + + bpy.ops.object.mode_set(mode="OBJECT") + pose_bone = blender_arma.pose.bones[vnode.blender_bone_name] + + # Put scale on the pose bone (can't go on the edit bone) + _, _, s = vnode.trs + pose_bone.scale = s + + if isinstance(vnode_id, int): + pynode = gltf.data.nodes[vnode_id] + set_extras(pose_bone, pynode.extras) - return + @staticmethod + def create_mesh_object(gltf, pynode, name): + instance = False + if gltf.data.meshes[pynode.mesh].blender_name.get(pynode.skin) is not None: + # Mesh is already created, only create instance + # Except is current node is animated with path weight + # Or if previous instance is animation at node level + if pynode.weight_animation is True: + instance = False + else: + if gltf.data.meshes[pynode.mesh].is_weight_animated is True: + instance = False + else: + instance = True + mesh = bpy.data.meshes[gltf.data.meshes[pynode.mesh].blender_name[pynode.skin]] - if pynode.is_joint: + if instance is False: if pynode.name: - gltf.log.info("Blender create Bone node " + pynode.name) + gltf.log.info("Blender create Mesh node " + pynode.name) else: - gltf.log.info("Blender create Bone node") - # Check if corresponding armature is already created, create it if needed - if gltf.data.skins[pynode.skin_id].blender_armature_name is None: - BlenderSkin.create_armature(gltf, pynode.skin_id, parent) - - BlenderSkin.create_bone(gltf, pynode.skin_id, node_idx, parent) - - if pynode.children: - for child_idx in pynode.children: - BlenderNode.create(gltf, child_idx, node_idx) - - return + gltf.log.info("Blender create Mesh node") - if pynode.extensions is not None: - if 'KHR_lights_punctual' in pynode.extensions.keys(): - obj = BlenderLight.create(gltf, pynode.extensions['KHR_lights_punctual']['light']) - set_extras(obj, pynode.extras) - obj.rotation_mode = 'QUATERNION' - BlenderNode.set_transforms(gltf, node_idx, pynode, obj, parent, correction=True) - pynode.blender_object = obj.name - pynode.correction_needed = True - BlenderNode.set_parent(gltf, obj, parent) + mesh = BlenderMesh.create(gltf, pynode.mesh, pynode.skin) - if pynode.children: - for child_idx in pynode.children: - BlenderNode.create(gltf, child_idx, node_idx) + if pynode.weight_animation is True: + # flag this mesh instance as created only for this node, because of weight animation + gltf.data.meshes[pynode.mesh].is_weight_animated = True - return + mesh_name = gltf.data.meshes[pynode.mesh].name + if not name and mesh_name: + name = mesh_name - # No mesh, no camera, no light. For now, create empty #TODO + obj = bpy.data.objects.new(name, mesh) - if pynode.name: - gltf.log.info("Blender create Empty node " + pynode.name) - obj = bpy.data.objects.new(pynode.name, None) - else: - gltf.log.info("Blender create Empty node") - obj = bpy.data.objects.new("Node", None) - set_extras(obj, pynode.extras) - obj.rotation_mode = 'QUATERNION' - if gltf.blender_active_collection is not None: - bpy.data.collections[gltf.blender_active_collection].objects.link(obj) - else: - bpy.data.scenes[gltf.blender_scene].collection.objects.link(obj) - - BlenderNode.set_transforms(gltf, node_idx, pynode, obj, parent) - pynode.blender_object = obj.name - BlenderNode.set_parent(gltf, obj, parent) - - if pynode.children: - for child_idx in pynode.children: - BlenderNode.create(gltf, child_idx, node_idx) + if instance == False: + BlenderMesh.set_mesh(gltf, gltf.data.meshes[pynode.mesh], obj) - @staticmethod - def set_parent(gltf, obj, parent): - """Set parent.""" - if parent is None: - return - - for node_idx, node in enumerate(gltf.data.nodes): - if node_idx == parent: - if node.is_joint is True: - bpy.ops.object.select_all(action='DESELECT') - bpy.data.objects[node.blender_armature_name].select_set(True) - bpy.context.view_layer.objects.active = bpy.data.objects[node.blender_armature_name] - - bpy.ops.object.mode_set(mode='EDIT') - bpy.data.objects[node.blender_armature_name].data.edit_bones.active = \ - bpy.data.objects[node.blender_armature_name].data.edit_bones[node.blender_bone_name] - bpy.ops.object.mode_set(mode='OBJECT') - bpy.ops.object.select_all(action='DESELECT') - obj.select_set(True) - bpy.data.objects[node.blender_armature_name].select_set(True) - bpy.context.view_layer.objects.active = bpy.data.objects[node.blender_armature_name] - bpy.context.view_layer.update() - bpy.ops.object.parent_set(type='BONE_RELATIVE', keep_transform=True) - # From world transform to local (-armature transform -bone transform) - bone_trans = bpy.data.objects[node.blender_armature_name] \ - .pose.bones[node.blender_bone_name].matrix.to_translation().copy() - bone_rot = bpy.data.objects[node.blender_armature_name] \ - .pose.bones[node.blender_bone_name].matrix.to_quaternion().copy() - bone_scale_mat = scale_to_matrix(node.blender_bone_matrix.to_scale()) - obj.location = bone_scale_mat @ obj.location - obj.location = bone_rot @ obj.location - obj.location += bone_trans - obj.location = bpy.data.objects[node.blender_armature_name].matrix_world.to_quaternion() \ - @ obj.location - obj.rotation_quaternion = obj.rotation_quaternion \ - @ bpy.data.objects[node.blender_armature_name].matrix_world.to_quaternion() - obj.scale = bone_scale_mat @ obj.scale - - return - if node.blender_object: - obj.parent = bpy.data.objects[node.blender_object] - return - - gltf.log.error("ERROR, parent not found") - - @staticmethod - def set_transforms(gltf, node_idx, pynode, obj, parent, correction=False): - """Set transforms.""" - if parent is None: - obj.matrix_world = matrix_gltf_to_blender(pynode.transform) - if correction is True: - obj.matrix_world = obj.matrix_world @ correction_rotation() - return - - for idx, node in enumerate(gltf.data.nodes): - if idx == parent: - if node.is_joint is True: - obj.matrix_world = matrix_gltf_to_blender(pynode.transform) - if correction is True: - obj.matrix_world = obj.matrix_world @ correction_rotation() - return - else: - if correction is True: - obj.matrix_world = obj.matrix_world @ correction_rotation() - obj.matrix_world = matrix_gltf_to_blender(pynode.transform) - return + return obj diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_primitive.py b/io_scene_gltf2/blender/imp/gltf2_blender_primitive.py index 58fc9b9c..a046204b 100755 --- a/io_scene_gltf2/blender/imp/gltf2_blender_primitive.py +++ b/io_scene_gltf2/blender/imp/gltf2_blender_primitive.py @@ -13,10 +13,9 @@ # limitations under the License. import bpy -from mathutils import Vector +from mathutils import Vector, Matrix from .gltf2_blender_material import BlenderMaterial -from ..com.gltf2_blender_conversion import loc_gltf_to_blender from ...io.imp.gltf2_io_binary import BinaryData from ...io.com.gltf2_io_color_management import color_linear_to_srgb from ...io.com import gltf2_io_debug @@ -37,7 +36,7 @@ class BlenderPrimitive(): return bme_layers[name] @staticmethod - def add_primitive_to_bmesh(gltf, bme, pymesh, pyprimitive, material_index): + def add_primitive_to_bmesh(gltf, bme, pymesh, pyprimitive, skin_idx, material_index): attributes = pyprimitive.attributes if 'POSITION' not in attributes: @@ -57,6 +56,62 @@ class BlenderPrimitive(): bme_edges = bme.edges bme_faces = bme.faces + # Gather up the joints/weights (multiple sets allow >4 influences) + joint_sets = [] + weight_sets = [] + set_num = 0 + while 'JOINTS_%d' % set_num in attributes and 'WEIGHTS_%d' % set_num in attributes: + joint_data = BinaryData.get_data_from_accessor(gltf, attributes['JOINTS_%d' % set_num], cache=True) + weight_data = BinaryData.get_data_from_accessor(gltf, attributes['WEIGHTS_%d' % set_num], cache=True) + + joint_sets.append(joint_data) + weight_sets.append(weight_data) + + set_num += 1 + + # For skinned meshes, we will need to calculate the position of the + # verts in the bind pose, ie. the pose the edit bones are in. + if skin_idx is not None: + pyskin = gltf.data.skins[skin_idx] + if pyskin.inverse_bind_matrices is not None: + inv_binds = BinaryData.get_data_from_accessor(gltf, pyskin.inverse_bind_matrices) + inv_binds = [gltf.matrix_gltf_to_blender(m) for m in inv_binds] + else: + inv_binds = [Matrix.Identity(4) for i in range(len(pyskin.joints))] + arma_mats = [gltf.vnodes[joint].bone_arma_mat for joint in pyskin.joints] + joint_mats = [arma_mat @ inv_bind for arma_mat, inv_bind in zip(arma_mats, inv_binds)] + + def skin_vert(pos, pidx): + out = Vector((0, 0, 0)) + # Spec says weights should already sum to 1 but some models + # don't do it (ex. CesiumMan), so normalize. + weight_sum = 0 + for joint_set, weight_set in zip(joint_sets, weight_sets): + for j in range(0, 4): + weight = weight_set[pidx][j] + if weight != 0.0: + weight_sum += weight + joint = joint_set[pidx][j] + out += weight * (joint_mats[joint] @ pos) + out /= weight_sum + return out + + def skin_normal(norm, pidx): + # TODO: not sure this is right + norm = Vector([norm[0], norm[1], norm[2], 0]) + out = Vector((0, 0, 0, 0)) + weight_sum = 0 + for joint_set, weight_set in zip(joint_sets, weight_sets): + for j in range(0, 4): + weight = weight_set[pidx][j] + if weight != 0.0: + weight_sum += weight + joint = joint_set[pidx][j] + out += weight * (joint_mats[joint] @ norm) + out /= weight_sum + out = out.to_3d().normalized() + return out + # Every vertex has an index into the primitive's attribute arrays and a # *different* index into the BMesh's list of verts. Call the first one the # pidx and the second the bidx. Need to keep them straight! @@ -74,7 +129,11 @@ class BlenderPrimitive(): used_pidxs = list(used_pidxs) used_pidxs.sort() for pidx in used_pidxs: - bme_verts.new(positions[pidx]) + pos = gltf.loc_gltf_to_blender(positions[pidx]) + if skin_idx is not None: + pos = skin_vert(pos, pidx) + + bme_verts.new(pos) vert_idxs.append((bidx, pidx)) pidx_to_bidx[pidx] = bidx bidx += 1 @@ -114,8 +173,13 @@ class BlenderPrimitive(): if 'NORMAL' in attributes: normals = BinaryData.get_data_from_accessor(gltf, attributes['NORMAL'], cache=True) - for bidx, pidx in vert_idxs: - bme_verts[bidx].normal = normals[pidx] + if skin_idx is None: + for bidx, pidx in vert_idxs: + bme_verts[bidx].normal = gltf.normal_gltf_to_blender(normals[pidx]) + else: + for bidx, pidx in vert_idxs: + normal = gltf.normal_gltf_to_blender(normals[pidx]) + bme_verts[bidx].normal = skin_normal(normal, pidx) # Set vertex colors. Add them in the order COLOR_0, COLOR_1, etc. set_num = 0 @@ -176,19 +240,7 @@ class BlenderPrimitive(): set_num += 1 - # Set joints/weights for skinning (multiple sets allow > 4 influences) - joint_sets = [] - weight_sets = [] - set_num = 0 - while 'JOINTS_%d' % set_num in attributes and 'WEIGHTS_%d' % set_num in attributes: - joint_data = BinaryData.get_data_from_accessor(gltf, attributes['JOINTS_%d' % set_num], cache=True) - weight_data = BinaryData.get_data_from_accessor(gltf, attributes['WEIGHTS_%d' % set_num], cache=True) - - joint_sets.append(joint_data) - weight_sets.append(weight_data) - - set_num += 1 - + # Set joints/weights for skinning if joint_sets: layer = BlenderPrimitive.get_layer(bme.verts.layers.deform, 'Vertex Weights') @@ -210,11 +262,19 @@ class BlenderPrimitive(): morph_positions = BinaryData.get_data_from_accessor(gltf, target['POSITION'], cache=True) - for bidx, pidx in vert_idxs: - bme_verts[bidx][layer] = ( - Vector(positions[pidx]) + - Vector(morph_positions[pidx]) - ) + if skin_idx is None: + for bidx, pidx in vert_idxs: + bme_verts[bidx][layer] = ( + gltf.loc_gltf_to_blender(positions[pidx]) + + gltf.loc_gltf_to_blender(morph_positions[pidx]) + ) + else: + for bidx, pidx in vert_idxs: + pos = ( + gltf.loc_gltf_to_blender(positions[pidx]) + + gltf.loc_gltf_to_blender(morph_positions[pidx]) + ) + bme_verts[bidx][layer] = skin_vert(pos, pidx) @staticmethod def edges_and_faces(mode, indices): diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_scene.py b/io_scene_gltf2/blender/imp/gltf2_blender_scene.py index d2d35f64..ca2f8052 100755 --- a/io_scene_gltf2/blender/imp/gltf2_blender_scene.py +++ b/io_scene_gltf2/blender/imp/gltf2_blender_scene.py @@ -19,6 +19,7 @@ from .gltf2_blender_node import BlenderNode from .gltf2_blender_skin import BlenderSkin from .gltf2_blender_animation import BlenderAnimation from .gltf2_blender_animation_utils import simulate_stash +from .gltf2_blender_vnode import VNode, compute_vnodes class BlenderScene(): @@ -27,168 +28,67 @@ class BlenderScene(): raise RuntimeError("%s should not be instantiated" % cls) @staticmethod - def create(gltf, scene_idx): + def create(gltf): """Scene creation.""" - gltf.blender_active_collection = None - if scene_idx is not None: - pyscene = gltf.data.scenes[scene_idx] - list_nodes = pyscene.nodes - - # Create a new scene only if not already exists in .blend file - # TODO : put in current scene instead ? - if pyscene.name not in [scene.name for scene in bpy.data.scenes]: - # TODO: There is a bug in 2.8 alpha that break CLEAR_KEEP_TRANSFORM - # if we are creating a new scene - scene = bpy.context.scene - if bpy.context.collection.name in bpy.data.collections: # avoid master collection - gltf.blender_active_collection = bpy.context.collection.name - if scene.render.engine not in ['CYCLES', 'BLENDER_EEVEE']: - scene.render.engine = "BLENDER_EEVEE" - - gltf.blender_scene = scene.name - else: - gltf.blender_scene = pyscene.name - - # Switch to newly created main scene - bpy.context.window.scene = bpy.data.scenes[gltf.blender_scene] - if bpy.context.collection.name in bpy.data.collections: # avoid master collection - gltf.blender_active_collection = bpy.context.collection.name + scene = bpy.context.scene + gltf.blender_scene = scene.name + if bpy.context.collection.name in bpy.data.collections: # avoid master collection + gltf.blender_active_collection = bpy.context.collection.name + if scene.render.engine not in ['CYCLES', 'BLENDER_EEVEE']: + scene.render.engine = "BLENDER_EEVEE" - else: - # No scene in glTF file, create all objects in current scene - scene = bpy.context.scene - if scene.render.engine not in ['CYCLES', 'BLENDER_EEVEE']: - scene.render.engine = "BLENDER_EEVEE" - if bpy.context.collection.name in bpy.data.collections: # avoid master collection - gltf.blender_active_collection = bpy.context.collection.name - gltf.blender_scene = scene.name - list_nodes = BlenderScene.get_root_nodes(gltf) - - if bpy.app.debug_value != 100: - # Create Yup2Zup empty - obj_rotation = bpy.data.objects.new("Yup2Zup", None) - obj_rotation.rotation_mode = 'QUATERNION' - obj_rotation.rotation_quaternion = Quaternion((sqrt(2) / 2, sqrt(2) / 2, 0.0, 0.0)) - - if gltf.blender_active_collection is not None: - bpy.data.collections[gltf.blender_active_collection].objects.link(obj_rotation) - else: - bpy.data.scenes[gltf.blender_scene].collection.objects.link(obj_rotation) - - if list_nodes is not None: - for node_idx in list_nodes: - BlenderNode.create(gltf, node_idx, None) # None => No parent + compute_vnodes(gltf) + + gltf.display_current_node = 0 # for debugging + BlenderNode.create_vnode(gltf, 'root') # Now that all mesh / bones are created, create vertex groups on mesh if gltf.data.skins: - for skin_id, skin in enumerate(gltf.data.skins): - if hasattr(skin, "node_ids"): - BlenderSkin.create_vertex_groups(gltf, skin_id) + BlenderSkin.create_vertex_groups(gltf) + BlenderSkin.create_armature_modifiers(gltf) - for skin_id, skin in enumerate(gltf.data.skins): - if hasattr(skin, "node_ids"): - BlenderSkin.create_armature_modifiers(gltf, skin_id) + BlenderScene.create_animations(gltf) + if bpy.context.mode != 'OBJECT': + bpy.ops.object.mode_set(mode='OBJECT') + BlenderScene.set_active_object(gltf) + + @staticmethod + def create_animations(gltf): + """Create animations.""" if gltf.data.animations: for anim_idx, anim in enumerate(gltf.data.animations): - # Blender armature name -> action all its bones should use - gltf.arma_cache = {} + # Caches the action for each object (keyed by object name) + gltf.action_cache = {} # Things we need to stash when we're done. gltf.needs_stash = [] - if list_nodes is not None: - for node_idx in list_nodes: - BlenderAnimation.anim(gltf, anim_idx, node_idx) + BlenderAnimation.anim(gltf, anim_idx, 'root') for (obj, anim_name, action) in gltf.needs_stash: simulate_stash(obj, anim_name, action) # Restore first animation anim_name = gltf.data.animations[0].track_name - for node_idx in list_nodes: - BlenderAnimation.restore_animation(gltf, node_idx, anim_name) - - if bpy.app.debug_value != 100: - # Parent root node to rotation object - if list_nodes is not None: - exclude_nodes = [] - for node_idx in list_nodes: - if gltf.data.nodes[node_idx].is_joint: - # Do not change parent if root node is already parented (can be the case for skinned mesh) - if not bpy.data.objects[gltf.data.nodes[node_idx].blender_armature_name].parent: - bpy.data.objects[gltf.data.nodes[node_idx].blender_armature_name].parent = obj_rotation - else: - exclude_nodes.append(node_idx) - else: - # Do not change parent if root node is already parented (can be the case for skinned mesh) - if not bpy.data.objects[gltf.data.nodes[node_idx].blender_object].parent: - bpy.data.objects[gltf.data.nodes[node_idx].blender_object].parent = obj_rotation - else: - exclude_nodes.append(node_idx) - - if gltf.animation_object is False: - - - - - - # Avoid rotation bug if collection is hidden or disabled - if gltf.blender_active_collection is not None: - gltf.collection_hide_viewport = bpy.data.collections[gltf.blender_active_collection].hide_viewport - bpy.data.collections[gltf.blender_active_collection].hide_viewport = False - # TODO for visibility ... but seems not exposed on bpy for now - - for node_idx in list_nodes: - - if node_idx in exclude_nodes: - continue # for root node that are parented by the process - # for example skinned meshes - - for obj_ in bpy.context.scene.objects: - obj_.select_set(False) - if gltf.data.nodes[node_idx].is_joint: - bpy.data.objects[gltf.data.nodes[node_idx].blender_armature_name].select_set(True) - bpy.context.view_layer.objects.active = bpy.data.objects[gltf.data.nodes[node_idx].blender_armature_name] - - else: - bpy.data.objects[gltf.data.nodes[node_idx].blender_object].select_set(True) - bpy.context.view_layer.objects.active = bpy.data.objects[gltf.data.nodes[node_idx].blender_object] - - bpy.ops.object.parent_clear(type='CLEAR_KEEP_TRANSFORM') - - # remove object - #bpy.context.scene.collection.objects.unlink(obj_rotation) - bpy.data.objects.remove(obj_rotation) - - # Restore collection hidden / disabled values - if gltf.blender_active_collection is not None: - bpy.data.collections[gltf.blender_active_collection].hide_viewport = gltf.collection_hide_viewport - # TODO restore visibility when expose in bpy - - # Make first root object the new active one - if list_nodes is not None: - if gltf.data.nodes[list_nodes[0]].blender_object: - bl_name = gltf.data.nodes[list_nodes[0]].blender_object - else: - bl_name = gltf.data.nodes[list_nodes[0]].blender_armature_name - bpy.context.view_layer.objects.active = bpy.data.objects[bl_name] + BlenderAnimation.restore_animation(gltf, 'root', anim_name) @staticmethod - def get_root_nodes(gltf): - if gltf.data.nodes is None: - return None - - parents = {} - for idx, node in enumerate(gltf.data.nodes): - pynode = gltf.data.nodes[idx] - if pynode.children: - for child_idx in pynode.children: - parents[child_idx] = idx - - roots = [] - for idx, node in enumerate(gltf.data.nodes): - if idx not in parents.keys(): - roots.append(idx) - - return roots + def set_active_object(gltf): + """Make the first root object from the default glTF scene active. + If no default scene, use the first scene, or just any root object. + """ + if gltf.data.scenes: + pyscene = gltf.data.scenes[gltf.data.scene or 0] + vnode = gltf.vnodes[pyscene.nodes[0]] + if gltf.vnodes[vnode.parent].type != VNode.DummyRoot: + vnode = gltf.vnodes[vnode.parent] + + else: + vnode = gltf.vnodes['root'] + if vnode.type == VNode.DummyRoot: + if not vnode.children: + return # no nodes + vnode = gltf.vnodes[vnode.children[0]] + + bpy.context.view_layer.objects.active = vnode.blender_object diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_skin.py b/io_scene_gltf2/blender/imp/gltf2_blender_skin.py index 1d88e0e7..a9c50b58 100755 --- a/io_scene_gltf2/blender/imp/gltf2_blender_skin.py +++ b/io_scene_gltf2/blender/imp/gltf2_blender_skin.py @@ -14,10 +14,6 @@ import bpy -from mathutils import Vector, Matrix -from ..com.gltf2_blender_conversion import matrix_gltf_to_blender, scale_to_matrix -from ...io.imp.gltf2_io_binary import BinaryData -from ..com.gltf2_blender_extras import set_extras class BlenderSkin(): """Blender Skinning / Armature.""" @@ -25,154 +21,36 @@ class BlenderSkin(): raise RuntimeError("%s should not be instantiated" % cls) @staticmethod - def create_armature(gltf, skin_id, parent): - """Armature creation.""" - pyskin = gltf.data.skins[skin_id] - - if pyskin.name is not None: - name = pyskin.name - else: - name = "Armature_" + str(skin_id) - - armature = bpy.data.armatures.new(name) - obj = bpy.data.objects.new(name, armature) - if gltf.blender_active_collection is not None: - bpy.data.collections[gltf.blender_active_collection].objects.link(obj) - else: - bpy.data.scenes[gltf.blender_scene].collection.objects.link(obj) - - pyskin.blender_armature_name = obj.name - if parent is not None: - obj.parent = bpy.data.objects[gltf.data.nodes[parent].blender_object] - - @staticmethod - def set_bone_transforms(gltf, skin_id, bone, node_id, parent): - """Set bone transformations.""" - pyskin = gltf.data.skins[skin_id] - pynode = gltf.data.nodes[node_id] - - obj = bpy.data.objects[pyskin.blender_armature_name] - - # Set bone bind_pose by inverting bindpose matrix - if node_id in pyskin.joints: - index_in_skel = pyskin.joints.index(node_id) - if pyskin.inverse_bind_matrices is not None: - inverse_bind_matrices = BinaryData.get_data_from_accessor(gltf, pyskin.inverse_bind_matrices) - # Needed to keep scale in matrix, as bone.matrix seems to drop it - if index_in_skel < len(inverse_bind_matrices): - pynode.blender_bone_matrix = matrix_gltf_to_blender( - inverse_bind_matrices[index_in_skel] - ).inverted() - bone.matrix = pynode.blender_bone_matrix - else: - gltf.log.error("Error with inverseBindMatrix for skin " + pyskin) - else: - pynode.blender_bone_matrix = Matrix() # 4x4 identity matrix - else: - print('No invBindMatrix for bone ' + str(node_id)) - pynode.blender_bone_matrix = Matrix() - - # Parent the bone - if parent is not None and hasattr(gltf.data.nodes[parent], "blender_bone_name"): - bone.parent = obj.data.edit_bones[gltf.data.nodes[parent].blender_bone_name] # TODO if in another scene - - # Switch to Pose mode - bpy.ops.object.mode_set(mode="POSE") - obj.data.pose_position = 'POSE' - - # Set posebone location/rotation/scale (in armature space) - # location is actual bone location minus it's original (bind) location - bind_location = Matrix.Translation(pynode.blender_bone_matrix.to_translation()) - bind_rotation = pynode.blender_bone_matrix.to_quaternion() - bind_scale = scale_to_matrix(pynode.blender_bone_matrix.to_scale()) - - location, rotation, scale = matrix_gltf_to_blender(pynode.transform).decompose() - if parent is not None and hasattr(gltf.data.nodes[parent], "blender_bone_matrix"): - parent_mat = gltf.data.nodes[parent].blender_bone_matrix - - # Get armature space location (bindpose + pose) - # Then, remove original bind location from armspace location, and bind rotation - final_location = (bind_location.inverted() @ parent_mat @ Matrix.Translation(location)).to_translation() - obj.pose.bones[pynode.blender_bone_name].location = \ - bind_rotation.inverted().to_matrix().to_4x4() @ final_location - - # Do the same for rotation & scale - obj.pose.bones[pynode.blender_bone_name].rotation_quaternion = \ - (pynode.blender_bone_matrix.inverted() @ parent_mat @ - matrix_gltf_to_blender(pynode.transform)).to_quaternion() - obj.pose.bones[pynode.blender_bone_name].scale = \ - (bind_scale.inverted() @ parent_mat @ scale_to_matrix(scale)).to_scale() - - else: - obj.pose.bones[pynode.blender_bone_name].location = bind_location.inverted() @ location - obj.pose.bones[pynode.blender_bone_name].rotation_quaternion = bind_rotation.inverted() @ rotation - obj.pose.bones[pynode.blender_bone_name].scale = bind_scale.inverted() @ scale + def create_vertex_groups(gltf): + """Create vertex groups for all skinned meshes.""" + for vnode in gltf.vnodes.values(): + if vnode.mesh_node_idx is None: + continue + pynode = gltf.data.nodes[vnode.mesh_node_idx] + if pynode.skin is None: + continue + pyskin = gltf.data.skins[pynode.skin] + + obj = vnode.blender_object + for node_idx in pyskin.joints: + bone = gltf.vnodes[node_idx] + obj.vertex_groups.new(name=bone.blender_bone_name) @staticmethod - def create_bone(gltf, skin_id, node_id, parent): - """Bone creation.""" - pyskin = gltf.data.skins[skin_id] - pynode = gltf.data.nodes[node_id] - - scene = bpy.data.scenes[gltf.blender_scene] - obj = bpy.data.objects[pyskin.blender_armature_name] - - bpy.context.window.scene = scene - bpy.context.view_layer.objects.active = obj - bpy.ops.object.mode_set(mode="EDIT") - - if pynode.name: - name = pynode.name - else: - name = "Bone_" + str(node_id) - - bone = obj.data.edit_bones.new(name) - pynode.blender_bone_name = bone.name - pynode.blender_armature_name = pyskin.blender_armature_name - bone.tail = Vector((0.0, 1.0 / obj.matrix_world.to_scale()[1], 0.0)) # Needed to keep bone alive - # Custom prop on edit bone - set_extras(bone, pynode.extras) - - # set bind and pose transforms - BlenderSkin.set_bone_transforms(gltf, skin_id, bone, node_id, parent) - bpy.ops.object.mode_set(mode="OBJECT") - # Custom prop on pose bone - if pynode.blender_bone_name in obj.pose.bones: - set_extras(obj.pose.bones[pynode.blender_bone_name], pynode.extras) - - @staticmethod - def create_vertex_groups(gltf, skin_id): - """Vertex Group creation.""" - pyskin = gltf.data.skins[skin_id] - for node_id in pyskin.node_ids: - obj = bpy.data.objects[gltf.data.nodes[node_id].blender_object] - for bone in pyskin.joints: - obj.vertex_groups.new(name=gltf.data.nodes[bone].blender_bone_name) - - @staticmethod - def create_armature_modifiers(gltf, skin_id): - """Create Armature modifier.""" - pyskin = gltf.data.skins[skin_id] - - if pyskin.blender_armature_name is None: - # TODO seems something is wrong - # For example, some joints are in skin 0, and are in another skin too - # Not sure this is glTF compliant, will check it - return - - for node_id in pyskin.node_ids: - node = gltf.data.nodes[node_id] - obj = bpy.data.objects[node.blender_object] - - for obj_sel in bpy.context.scene.objects: - obj_sel.select_set(False) - obj.select_set(True) - bpy.context.view_layer.objects.active = obj - - # bpy.ops.object.parent_clear(type='CLEAR_KEEP_TRANSFORM') - # Reparent skinned mesh to it's armature to avoid breaking - # skinning with interleaved transforms - obj.parent = bpy.data.objects[pyskin.blender_armature_name] - arma = obj.modifiers.new(name="Armature", type="ARMATURE") - arma.object = bpy.data.objects[pyskin.blender_armature_name] + def create_armature_modifiers(gltf): + """Create Armature modifiers for all skinned meshes.""" + for vnode in gltf.vnodes.values(): + if vnode.mesh_node_idx is None: + continue + pynode = gltf.data.nodes[vnode.mesh_node_idx] + if pynode.skin is None: + continue + pyskin = gltf.data.skins[pynode.skin] + + first_bone = gltf.vnodes[pyskin.joints[0]] + arma = gltf.vnodes[first_bone.bone_arma] + + obj = vnode.blender_object + mod = obj.modifiers.new(name="Armature", type="ARMATURE") + mod.object = arma.blender_object diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_vnode.py b/io_scene_gltf2/blender/imp/gltf2_blender_vnode.py new file mode 100644 index 00000000..114d7193 --- /dev/null +++ b/io_scene_gltf2/blender/imp/gltf2_blender_vnode.py @@ -0,0 +1,324 @@ +# Copyright 2018-2019 The glTF-Blender-IO authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bpy +from mathutils import Vector, Quaternion, Matrix + +def compute_vnodes(gltf): + """Computes the tree of virtual nodes. + Copies the glTF nodes into a tree of VNodes, then performs a series of + passes to transform it into a form that we can import into Blender. + """ + init_vnodes(gltf) + mark_bones_and_armas(gltf) + move_skinned_meshes(gltf) + fixup_multitype_nodes(gltf) + correct_cameras_and_lights(gltf) + calc_bone_matrices(gltf) + + +class VNode: + """A "virtual" node. + These are what eventually get turned into nodes + in the Blender scene. + """ + # Types + Object = 0 + Bone = 1 + DummyRoot = 2 + + def __init__(self): + self.name = '' + self.children = [] + self.parent = None + self.type = VNode.Object + self.is_arma = False + self.trs = ( + Vector((0, 0, 0)), + Quaternion((1, 0, 0, 0)), + Vector((1, 1, 1)), + ) + # Indices of the glTF node where the mesh, etc. came from. + # (They can get moved around.) + self.mesh_node_idx = None + self.camera_node_idx = None + self.light_node_idx = None + + +def init_vnodes(gltf): + # Map of all VNodes. The keys are arbitrary IDs. + # Nodes coming from glTF use the index into gltf.data.nodes for an ID. + gltf.vnodes = {} + + for i, pynode in enumerate(gltf.data.nodes or []): + vnode = VNode() + gltf.vnodes[i] = vnode + vnode.name = pynode.name or 'Node_%d' % i + vnode.children = list(pynode.children or []) + vnode.trs = get_node_trs(gltf, pynode) + if pynode.mesh is not None: + vnode.mesh_node_idx = i + if pynode.camera is not None: + vnode.camera_node_idx = i + if 'KHR_lights_punctual' in (pynode.extensions or {}): + vnode.light_node_idx = i + + for id in gltf.vnodes: + for child in gltf.vnodes[id].children: + assert gltf.vnodes[child].parent is None + gltf.vnodes[child].parent = id + + # Inserting a root node will simplify things. + roots = [id for id in gltf.vnodes if gltf.vnodes[id].parent is None] + gltf.vnodes['root'] = VNode() + gltf.vnodes['root'].type = VNode.DummyRoot + gltf.vnodes['root'].name = 'Root' + gltf.vnodes['root'].children = roots + for root in roots: + gltf.vnodes[root].parent = 'root' + +def get_node_trs(gltf, pynode): + if pynode.matrix is not None: + m = gltf.matrix_gltf_to_blender(pynode.matrix) + return m.decompose() + + t = gltf.loc_gltf_to_blender(pynode.translation or [0, 0, 0]) + r = gltf.quaternion_gltf_to_blender(pynode.rotation or [0, 0, 0, 1]) + s = gltf.scale_gltf_to_blender(pynode.scale or [1, 1, 1]) + return t, r, s + + +def mark_bones_and_armas(gltf): + """ + Mark nodes as armatures so that every node that is used as joint is a + descendant of an armature. Mark everything between an armature and a + joint as a bone. + """ + for skin in gltf.data.skins or []: + descendants = list(skin.joints) + if skin.skeleton is not None: + descendants.append(skin.skeleton) + arma_id = deepest_common_ancestor(gltf, descendants) + + if arma_id in skin.joints: + arma_id = gltf.vnodes[arma_id].parent + + if gltf.vnodes[arma_id].type != VNode.Bone: + gltf.vnodes[arma_id].type = VNode.Object + gltf.vnodes[arma_id].is_arma = True + gltf.vnodes[arma_id].arma_name = skin.name or 'Armature' + + for joint in skin.joints: + while joint != arma_id: + gltf.vnodes[joint].type = VNode.Bone + gltf.vnodes[joint].is_arma = False + joint = gltf.vnodes[joint].parent + + # Mark the armature each bone is a descendant of. + + def visit(vnode_id, cur_arma): # Depth-first walk + vnode = gltf.vnodes[vnode_id] + + if vnode.is_arma: + cur_arma = vnode_id + elif vnode.type == VNode.Bone: + vnode.bone_arma = cur_arma + else: + cur_arma = None + + for child in vnode.children: + visit(child, cur_arma) + + visit('root', cur_arma=None) + +def deepest_common_ancestor(gltf, vnode_ids): + """Find the deepest (improper) ancestor of a set of vnodes.""" + path_to_ancestor = [] # path to deepest ancestor so far + for vnode_id in vnode_ids: + path = path_from_root(gltf, vnode_id) + if not path_to_ancestor: + path_to_ancestor = path + else: + path_to_ancestor = longest_common_prefix(path, path_to_ancestor) + return path_to_ancestor[-1] + +def path_from_root(gltf, vnode_id): + """Returns the ids of all vnodes from the root to vnode_id.""" + path = [] + while vnode_id is not None: + path.append(vnode_id) + vnode_id = gltf.vnodes[vnode_id].parent + path.reverse() + return path + +def longest_common_prefix(list1, list2): + i = 0 + while i != min(len(list1), len(list2)): + if list1[i] != list2[i]: + break + i += 1 + return list1[:i] + + +def move_skinned_meshes(gltf): + """ + In glTF, where in the node hierarchy a skinned mesh is instantiated has + no effect on its world space position: only the world transforms of the + joints in its skin affect it. + + To do this in Blender: + * Move a skinned mesh to become a child of the armature that affects it + * When we do mesh creation, we will also need to put all the verts in + their rest pose (ie. the pose the edit bones are in) + """ + # TODO: this leaves behind empty "husk" nodes where the skinned meshes + # used to be, which is ugly. + ids = list(gltf.vnodes.keys()) + for id in ids: + vnode = gltf.vnodes[id] + + if vnode.mesh_node_idx is None: + continue + + mesh = gltf.data.nodes[vnode.mesh_node_idx].mesh + skin = gltf.data.nodes[vnode.mesh_node_idx].skin + if skin is None: + continue + + pyskin = gltf.data.skins[skin] + arma = gltf.vnodes[pyskin.joints[0]].bone_arma + + new_id = str(id) + '.skinned' + gltf.vnodes[new_id] = VNode() + gltf.vnodes[new_id].name = gltf.data.meshes[mesh].name or 'Mesh_%d' % mesh + gltf.vnodes[new_id].parent = arma + gltf.vnodes[arma].children.append(new_id) + + gltf.vnodes[new_id].mesh_node_idx = vnode.mesh_node_idx + vnode.mesh_node_idx = None + + +def fixup_multitype_nodes(gltf): + """ + Blender only lets each object have one of: an armature, a mesh, a + camera, a light. Also bones cannot have any of these either. Find any + nodes like this and move the mesh/camera/light onto new children. + """ + ids = list(gltf.vnodes.keys()) + for id in ids: + vnode = gltf.vnodes[id] + + needs_move = False + + if vnode.is_arma or vnode.type == VNode.Bone: + needs_move = True + + if vnode.mesh_node_idx is not None: + if needs_move: + new_id = str(id) + '.mesh' + gltf.vnodes[new_id] = VNode() + gltf.vnodes[new_id].name = vnode.name + ' Mesh' + gltf.vnodes[new_id].mesh_node_idx = vnode.mesh_node_idx + gltf.vnodes[new_id].parent = id + vnode.children.append(new_id) + vnode.mesh_node_idx = None + needs_move = True + + if vnode.camera_node_idx is not None: + if needs_move: + new_id = str(id) + '.camera' + gltf.vnodes[new_id] = VNode() + gltf.vnodes[new_id].name = vnode.name + ' Camera' + gltf.vnodes[new_id].camera_node_idx = vnode.camera_node_idx + gltf.vnodes[new_id].parent = id + vnode.children.append(new_id) + vnode.camera_node_idx = None + needs_move = True + + if vnode.light_node_idx is not None: + if needs_move: + new_id = str(id) + '.light' + gltf.vnodes[new_id] = VNode() + gltf.vnodes[new_id].name = vnode.name + ' Light' + gltf.vnodes[new_id].light_node_idx = vnode.light_node_idx + gltf.vnodes[new_id].parent = id + vnode.children.append(new_id) + vnode.light_node_idx = None + needs_move = True + + +def correct_cameras_and_lights(gltf): + """ + Depending on the coordinate change, lights and cameras might need to be + rotated to match Blender conventions for which axes they point along. + """ + if gltf.camera_correction is None: + return + + trs = (Vector((0, 0, 0)), gltf.camera_correction, Vector((1, 1, 1))) + + ids = list(gltf.vnodes.keys()) + for id in ids: + vnode = gltf.vnodes[id] + + # Move the camera/light onto a new child and set its rotation + # TODO: "hard apply" the rotation without creating a new node + # (like we'll need to do for bones) + + if vnode.camera_node_idx is not None: + new_id = str(id) + '.camera-correction' + gltf.vnodes[new_id] = VNode() + gltf.vnodes[new_id].name = vnode.name + ' Correction' + gltf.vnodes[new_id].trs = trs + gltf.vnodes[new_id].camera_node_idx = vnode.camera_node_idx + gltf.vnodes[new_id].parent = id + vnode.children.append(new_id) + vnode.camera_node_idx = None + + if vnode.light_node_idx is not None: + new_id = str(id) + '.light-correction' + gltf.vnodes[new_id] = VNode() + gltf.vnodes[new_id].name = vnode.name + ' Correction' + gltf.vnodes[new_id].trs = trs + gltf.vnodes[new_id].light_node_idx = vnode.light_node_idx + gltf.vnodes[new_id].parent = id + vnode.children.append(new_id) + vnode.light_node_idx = None + + +def calc_bone_matrices(gltf): + """ + Calculate bone_arma_mat, the transformation from bone space to armature + space for the edit bone, for each bone. + """ + def visit(vnode_id): # Depth-first walk + vnode = gltf.vnodes[vnode_id] + if vnode.type == VNode.Bone: + if gltf.vnodes[vnode.parent].type == VNode.Bone: + parent_arma_mat = gltf.vnodes[vnode.parent].bone_arma_mat + else: + parent_arma_mat = Matrix.Identity(4) + + t, r, _ = vnode.trs + local_to_parent = Matrix.Translation(t) @ Quaternion(r).to_matrix().to_4x4() + vnode.bone_arma_mat = parent_arma_mat @ local_to_parent + + for child in vnode.children: + visit(child) + + visit('root') + + +# TODO: add pass to rotate/resize bones so they look pretty + diff --git a/io_scene_gltf2/io/com/gltf2_io_trs.py b/io_scene_gltf2/io/com/gltf2_io_trs.py deleted file mode 100755 index 59f30830..00000000 --- a/io_scene_gltf2/io/com/gltf2_io_trs.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright 2018 The glTF-Blender-IO authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -class TRS: - - def __new__(cls, *args, **kwargs): - raise RuntimeError("{} should not be instantiated".format(cls.__name__)) - - @staticmethod - def scale_to_matrix(scale): - # column major ! - return [scale[0], 0, 0, 0, - 0, scale[1], 0, 0, - 0, 0, scale[2], 0, - 0, 0, 0, 1] - - @staticmethod - def quaternion_to_matrix(q): - x, y, z, w = q - # TODO : is q normalized ? --> if not, multiply by 1/(w*w + x*x + y*y + z*z) - # column major ! - return [ - 1 - 2 * y * y - 2 * z * z, 2 * x * y + 2 * w * z, 2 * x * z - 2 * w * y, 0, - 2 * x * y - 2 * w * z, 1 - 2 * x * x - 2 * z * z, 2 * y * z + 2 * w * x, 0, - 2 * x * z + 2 * y * w, 2 * y * z - 2 * w * x, 1 - 2 * x * x - 2 * y * y, 0, - 0, 0, 0, 1] - - @staticmethod - def matrix_multiply(m, n): - # column major ! - - return [ - m[0] * n[0] + m[4] * n[1] + m[8] * n[2] + m[12] * n[3], - m[1] * n[0] + m[5] * n[1] + m[9] * n[2] + m[13] * n[3], - m[2] * n[0] + m[6] * n[1] + m[10] * n[2] + m[14] * n[3], - m[3] * n[0] + m[7] * n[1] + m[11] * n[2] + m[15] * n[3], - m[0] * n[4] + m[4] * n[5] + m[8] * n[6] + m[12] * n[7], - m[1] * n[4] + m[5] * n[5] + m[9] * n[6] + m[13] * n[7], - m[2] * n[4] + m[6] * n[5] + m[10] * n[6] + m[14] * n[7], - m[3] * n[4] + m[7] * n[5] + m[11] * n[6] + m[15] * n[7], - m[0] * n[8] + m[4] * n[9] + m[8] * n[10] + m[12] * n[11], - m[1] * n[8] + m[5] * n[9] + m[9] * n[10] + m[13] * n[11], - m[2] * n[8] + m[6] * n[9] + m[10] * n[10] + m[14] * n[11], - m[3] * n[8] + m[7] * n[9] + m[11] * n[10] + m[15] * n[11], - m[0] * n[12] + m[4] * n[13] + m[8] * n[14] + m[12] * n[15], - m[1] * n[12] + m[5] * n[13] + m[9] * n[14] + m[13] * n[15], - m[2] * n[12] + m[6] * n[13] + m[10] * n[14] + m[14] * n[15], - m[3] * n[12] + m[7] * n[13] + m[11] * n[14] + m[15] * n[15], - ] - - @staticmethod - def translation_to_matrix(translation): - # column major ! - return [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, - translation[0], translation[1], translation[2], 1.0] - diff --git a/io_scene_gltf2/io/imp/gltf2_io_gltf.py b/io_scene_gltf2/io/imp/gltf2_io_gltf.py index 1a30f258..dc5c5f8f 100755 --- a/io_scene_gltf2/io/imp/gltf2_io_gltf.py +++ b/io_scene_gltf2/io/imp/gltf2_io_gltf.py @@ -174,17 +174,6 @@ class glTFImporter(): self.content = None return success, txt - def is_node_joint(self, node_idx): - """Check if node is a joint.""" - if not self.data.skins: # if no skin in gltf file - return False, None - - for skin_idx, skin in enumerate(self.data.skins): - if node_idx in skin.joints: - return True, skin_idx - - return False, None - def load_buffer(self, buffer_idx): """Load buffer.""" buffer = self.data.buffers[buffer_idx] |