diff options
author | Julien Duroure <julien.duroure@gmail.com> | 2020-04-11 16:36:52 +0300 |
---|---|---|
committer | Julien Duroure <julien.duroure@gmail.com> | 2020-04-11 16:36:52 +0300 |
commit | 8dd0687a6796227c3846f94454dcd40358896c92 (patch) | |
tree | a805b559132de9151b290e58f63dea420b684781 /io_scene_gltf2 | |
parent | 7a3fdf08f3fe4984bc81219a075a8bd3234c7d72 (diff) |
glTF importer: refactoring animation import
Diffstat (limited to 'io_scene_gltf2')
7 files changed, 165 insertions, 332 deletions
diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index ffcfeb7d..5317f0f8 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, 61), + "version": (1, 2, 62), 'blender': (2, 82, 7), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_animation.py b/io_scene_gltf2/blender/imp/gltf2_blender_animation.py index 71f3ec00..f01e5ee9 100755 --- a/io_scene_gltf2/blender/imp/gltf2_blender_animation.py +++ b/io_scene_gltf2/blender/imp/gltf2_blender_animation.py @@ -14,48 +14,47 @@ import bpy -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_animation_utils import simulate_stash, restore_animation_on_object from .gltf2_blender_vnode import VNode class BlenderAnimation(): - """Dispatch Animation to bone or object animation.""" + """Dispatch Animation to node or morph weights animation.""" def __new__(cls, *args, **kwargs): raise RuntimeError("%s should not be instantiated" % cls) @staticmethod - def anim(gltf, anim_idx, vnode_id): - """Dispatch Animation to bone or object.""" - 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: + def anim(gltf, anim_idx): + """Create actions/tracks for one animation.""" + # 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 = [] + + for vnode_id in gltf.vnodes: + if isinstance(vnode_id, int): BlenderNodeAnim.anim(gltf, anim_idx, vnode_id) + BlenderWeightAnim.anim(gltf, anim_idx, vnode_id) - BlenderWeightAnim.anim(gltf, anim_idx, vnode_id) - - for child in gltf.vnodes[vnode_id].children: - BlenderAnimation.anim(gltf, anim_idx, child) + # Push all actions onto NLA tracks with this animation's name + track_name = gltf.data.animations[anim_idx].track_name + for (obj, action) in gltf.needs_stash: + simulate_stash(obj, track_name, action) @staticmethod - 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.""" - vnode = gltf.vnodes[vnode_id] - - obj = None - if vnode.type == VNode.Bone: - obj = gltf.vnodes[vnode.bone_arma].blender_object - elif vnode.type == VNode.Object: - obj = vnode.blender_object - - if obj is not None: + def restore_animation(gltf, animation_name): + """Restores the actions for an animation by its track name.""" + for vnode_id in gltf.vnodes: + vnode = gltf.vnodes[vnode_id] + if vnode.type == VNode.Bone: + obj = gltf.vnodes[vnode.bone_arma].blender_object + elif vnode.type == VNode.Object: + obj = vnode.blender_object + else: + continue + 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) - - 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 deleted file mode 100755 index 6f65de43..00000000 --- a/io_scene_gltf2/blender/imp/gltf2_blender_animation_bone.py +++ /dev/null @@ -1,206 +0,0 @@ -# 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 json -import bpy -from mathutils import Vector - -from ...io.imp.gltf2_io_binary import BinaryData -from .gltf2_blender_animation_utils import simulate_stash, make_fcurve - - -# In Blender we animate a pose bone. The final TRS of the bone depends on -# both the edit bone and pose 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] -# -# Given Final we can solve for the PoseBone we need to use with -# -# 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, 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) - - if animation.samplers[channel.sampler].interpolation == "CUBICSPLINE": - # TODO manage tangent? - translation_keyframes = ( - gltf.loc_gltf_to_blender(values[idx * 3 + 1]) - for idx in range(0, len(keys)) - ) - else: - translation_keyframes = (gltf.loc_gltf_to_blender(vals) for vals in values) - - final_translations = vnode.base_locs_to_final_locs(translation_keyframes) - - # Calculate pose bone trans from final bone trans - edit_trans, edit_rot = vnode.editbone_trans, vnode.editbone_rot - edit_rot_inv = edit_rot.conjugated() - pose_translations = [ - edit_rot_inv @ (trans - edit_trans) - for trans in final_translations - ] - - BlenderBoneAnim.fill_fcurves( - obj.animation_data.action, - keys, - pose_translations, - group_name, - blender_path, - animation.samplers[channel.sampler].interpolation - ) - - @staticmethod - 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) - - if animation.samplers[channel.sampler].interpolation == "CUBICSPLINE": - # TODO manage tangent? - quat_keyframes = [ - gltf.quaternion_gltf_to_blender(values[idx * 3 + 1]) - for idx in range(0, len(keys)) - ] - else: - quat_keyframes = [gltf.quaternion_gltf_to_blender(vals) for vals in values] - - final_rots = vnode.base_rots_to_final_rots(quat_keyframes) - - # Calculate pose bone rotation from final bone rotation - edit_rot = vnode.editbone_rot - edit_rot_inv = edit_rot.conjugated() - pose_rots = [ - edit_rot_inv @ rot - for rot in final_rots - ] - - # Manage antipodal quaternions - for i in range(1, len(pose_rots)): - if pose_rots[i].dot(pose_rots[i-1]) < 0: - pose_rots[i] = -pose_rots[i] - - BlenderBoneAnim.fill_fcurves( - obj.animation_data.action, - keys, - pose_rots, - group_name, - blender_path, - animation.samplers[channel.sampler].interpolation - ) - - @staticmethod - 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) - - if animation.samplers[channel.sampler].interpolation == "CUBICSPLINE": - # TODO manage tangent? - scale_keyframes = [ - gltf.scale_gltf_to_blender(values[idx * 3 + 1]) - for idx in range(0, len(keys)) - ] - else: - scale_keyframes = [gltf.scale_gltf_to_blender(vals) for vals in values] - - final_scales = vnode.base_scales_to_final_scales(scale_keyframes) - pose_scales = final_scales # no change needed - - BlenderBoneAnim.fill_fcurves( - obj.animation_data.action, - keys, - pose_scales, - group_name, - blender_path, - animation.samplers[channel.sampler].interpolation - ) - - @staticmethod - def fill_fcurves(action, keys, values, group_name, blender_path, interpolation): - """Create FCurves from the keyframe-value pairs (one per component).""" - fps = bpy.context.scene.render.fps - - coords = [0] * (2 * len(keys)) - coords[::2] = (key[0] * fps for key in keys) - - for i in range(0, len(values[0])): - coords[1::2] = (vals[i] for vals in values) - make_fcurve( - action, - coords, - data_path=blender_path, - index=i, - group_name=group_name, - interpolation=interpolation, - ) - - @staticmethod - def anim(gltf, anim_idx, node_idx): - """Manage animation.""" - node = gltf.data.nodes[node_idx] - 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.action_cache.get(obj.name) - if not action: - name = animation.track_name + "_" + obj.name - action = bpy.data.actions.new(name) - action.id_root = 'OBJECT' - gltf.needs_stash.append((obj, animation.track_name, action)) - gltf.action_cache[obj.name] = action - - if not obj.animation_data: - obj.animation_data_create() - obj.animation_data.action = action - - for channel_idx in node.animations[anim_idx]: - channel = animation.channels[channel_idx] - - if channel.target.path == "translation": - BlenderBoneAnim.parse_translation_channel(gltf, vnode, obj, bone, channel, animation) - - elif channel.target.path == "rotation": - BlenderBoneAnim.parse_rotation_channel(gltf, vnode, obj, bone, channel, animation) - - elif channel.target.path == "scale": - 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 b6369b8b..d41b7892 100755 --- a/io_scene_gltf2/blender/imp/gltf2_blender_animation_node.py +++ b/io_scene_gltf2/blender/imp/gltf2_blender_animation_node.py @@ -12,11 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json import bpy from mathutils import Vector from ...io.imp.gltf2_io_binary import BinaryData -from .gltf2_blender_animation_utils import simulate_stash, make_fcurve +from .gltf2_blender_animation_utils import make_fcurve from .gltf2_blender_vnode import VNode @@ -27,92 +28,144 @@ class BlenderNodeAnim(): @staticmethod def anim(gltf, anim_idx, node_idx): - """Manage animation.""" - node = gltf.data.nodes[node_idx] - vnode = gltf.vnodes[node_idx] - obj = vnode.blender_object - fps = bpy.context.scene.render.fps - + """Manage animation targeting a node's TRS.""" animation = gltf.data.animations[anim_idx] - + node = gltf.data.nodes[node_idx] if anim_idx not in node.animations.keys(): return for channel_idx in node.animations[anim_idx]: channel = animation.channels[channel_idx] - if channel.target.path in ['translation', 'rotation', 'scale']: - break - else: - return + if channel.target.path not in ['translation', 'rotation', 'scale']: + continue + + BlenderNodeAnim.do_channel(gltf, anim_idx, node_idx, channel) + + @staticmethod + def do_channel(gltf, anim_idx, node_idx, channel): + animation = gltf.data.animations[anim_idx] + vnode = gltf.vnodes[node_idx] + path = channel.target.path + + action = BlenderNodeAnim.get_or_create_action(gltf, node_idx, animation.track_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) + + if animation.samplers[channel.sampler].interpolation == "CUBICSPLINE": + # TODO manage tangent? + values = values[1::3] + + # Convert the curve from glTF to Blender. + + if path == "translation": + blender_path = "location" + group_name = "Location" + num_components = 3 + values = [gltf.loc_gltf_to_blender(vals) for vals in values] + values = vnode.base_locs_to_final_locs(values) + + elif path == "rotation": + blender_path = "rotation_quaternion" + group_name = "Rotation" + num_components = 4 + values = [gltf.quaternion_gltf_to_blender(vals) for vals in values] + values = vnode.base_rots_to_final_rots(values) + + elif path == "scale": + blender_path = "scale" + group_name = "Scale" + num_components = 3 + values = [gltf.scale_gltf_to_blender(vals) for vals in values] + values = vnode.base_scales_to_final_scales(values) + + # Objects parented to a bone are translated to the bone tip by default. + # Correct for this by translating backwards from the tip to the root. + if vnode.type == VNode.Object and path == "translation": + if vnode.parent is not None and gltf.vnodes[vnode.parent].type == VNode.Bone: + bone_length = gltf.vnodes[vnode.parent].bone_length + off = Vector((0, -bone_length, 0)) + values = [vals + off for vals in values] + + if vnode.type == VNode.Bone: + # Need to animate the pose bone when the node is a bone. + group_name = vnode.blender_bone_name + blender_path = "pose.bones[%s].%s" % (json.dumps(vnode.blender_bone_name), blender_path) + + # We have the final TRS of the bone in values. We need to give + # the TRS of the pose bone though, which is relative to the edit + # bone. + # + # Final = EditBone * PoseBone + # where + # Final = Trans[ft] Rot[fr] Scale[fs] + # EditBone = Trans[et] Rot[er] + # PoseBone = Trans[pt] Rot[pr] Scale[ps] + # + # Solving for PoseBone gives + # + # pt = Rot[er^{-1}] (ft - et) + # pr = er^{-1} fr + # ps = fs + + if path == 'translation': + edit_trans, edit_rot = vnode.editbone_trans, vnode.editbone_rot + edit_rot_inv = edit_rot.conjugated() + values = [ + edit_rot_inv @ (trans - edit_trans) + for trans in values + ] + + elif path == 'rotation': + edit_rot = vnode.editbone_rot + edit_rot_inv = edit_rot.conjugated() + values = [ + edit_rot_inv @ rot + for rot in values + ] + + elif path == 'scale': + pass # no change needed + + # To ensure rotations always take the shortest path, we flip + # adjacent antipodal quaternions. + if path == 'rotation': + for i in range(1, len(values)): + if values[i].dot(values[i-1]) < 0: + values[i] = -values[i] + + fps = bpy.context.scene.render.fps + + coords = [0] * (2 * len(keys)) + coords[::2] = (key[0] * fps for key in keys) + + for i in range(0, num_components): + coords[1::2] = (vals[i] for vals in values) + make_fcurve( + action, + coords, + data_path=blender_path, + index=i, + group_name=group_name, + interpolation=animation.samplers[channel.sampler].interpolation, + ) + + @staticmethod + def get_or_create_action(gltf, node_idx, anim_name): + vnode = gltf.vnodes[node_idx] + + if vnode.type == VNode.Bone: + # For bones, the action goes on the armature. + vnode = gltf.vnodes[vnode.bone_arma] + + obj = vnode.blender_object action = gltf.action_cache.get(obj.name) if not action: - name = animation.track_name + "_" + obj.name + name = anim_name + "_" + obj.name action = bpy.data.actions.new(name) action.id_root = 'OBJECT' - gltf.needs_stash.append((obj, animation.track_name, action)) + gltf.needs_stash.append((obj, action)) gltf.action_cache[obj.name] = action - if not obj.animation_data: - obj.animation_data_create() - obj.animation_data.action = action - - for channel_idx in node.animations[anim_idx]: - channel = animation.channels[channel_idx] - - keys = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].input) - values = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].output) - - if channel.target.path not in ['translation', 'rotation', 'scale']: - continue - - if animation.samplers[channel.sampler].interpolation == "CUBICSPLINE": - # TODO manage tangent? - values = [values[idx * 3 + 1] for idx in range(0, len(keys))] - - if channel.target.path == "translation": - blender_path = "location" - group_name = "Location" - num_components = 3 - values = [gltf.loc_gltf_to_blender(vals) for vals in values] - values = vnode.base_locs_to_final_locs(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 from the tip to the root - bone_length = gltf.vnodes[vnode.parent].bone_length - off = Vector((0, -bone_length, 0)) - values = [vals + off for vals in values] - - elif channel.target.path == "rotation": - blender_path = "rotation_quaternion" - group_name = "Rotation" - num_components = 4 - values = [gltf.quaternion_gltf_to_blender(vals) for vals in values] - values = vnode.base_rots_to_final_rots(values) - - # Manage antipodal quaternions - for i in range(1, len(values)): - if values[i].dot(values[i-1]) < 0: - values[i] = -values[i] - - elif channel.target.path == "scale": - blender_path = "scale" - group_name = "Scale" - num_components = 3 - values = [gltf.scale_gltf_to_blender(vals) for vals in values] - values = vnode.base_scales_to_final_scales(values) - - coords = [0] * (2 * len(keys)) - coords[::2] = (key[0] * fps for key in keys) - - for i in range(0, num_components): - coords[1::2] = (vals[i] for vals in values) - make_fcurve( - action, - coords, - data_path=blender_path, - index=i, - group_name=group_name, - interpolation=animation.samplers[channel.sampler].interpolation, - ) + return action diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_animation_utils.py b/io_scene_gltf2/blender/imp/gltf2_blender_animation_utils.py index 6c933852..2d0d8568 100644 --- a/io_scene_gltf2/blender/imp/gltf2_blender_animation_utils.py +++ b/io_scene_gltf2/blender/imp/gltf2_blender_animation_utils.py @@ -19,16 +19,16 @@ def simulate_stash(obj, track_name, action, start_frame=None): # * add a track # * add an action on track # * lock & mute the track - # * remove active action from object + if not obj.animation_data: + obj.animation_data_create() tracks = obj.animation_data.nla_tracks new_track = tracks.new(prev=None) new_track.name = track_name if start_frame is None: start_frame = bpy.context.scene.frame_start - strip = new_track.strips.new(action.name, start_frame, action) + _strip = new_track.strips.new(action.name, start_frame, action) new_track.lock = True new_track.mute = True - obj.animation_data.action = None def restore_animation_on_object(obj, anim_name): if not getattr(obj, 'animation_data', None): 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 1329243d..5ce4532e 100644 --- a/io_scene_gltf2/blender/imp/gltf2_blender_animation_weight.py +++ b/io_scene_gltf2/blender/imp/gltf2_blender_animation_weight.py @@ -52,11 +52,7 @@ class BlenderWeightAnim(): name = animation.track_name + "_" + obj.name action = bpy.data.actions.new(name) action.id_root = "KEY" - gltf.needs_stash.append((obj.data.shape_keys, animation.track_name, action)) - - if not obj.data.shape_keys.animation_data: - obj.data.shape_keys.animation_data_create() - obj.data.shape_keys.animation_data.action = action + gltf.needs_stash.append((obj.data.shape_keys, action)) keys = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].input) values = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].output) diff --git a/io_scene_gltf2/blender/imp/gltf2_blender_scene.py b/io_scene_gltf2/blender/imp/gltf2_blender_scene.py index 109d6b28..1bc3fe0b 100755 --- a/io_scene_gltf2/blender/imp/gltf2_blender_scene.py +++ b/io_scene_gltf2/blender/imp/gltf2_blender_scene.py @@ -17,7 +17,6 @@ from math import sqrt from mathutils import Quaternion from .gltf2_blender_node import BlenderNode from .gltf2_blender_animation import BlenderAnimation -from .gltf2_blender_animation_utils import simulate_stash from .gltf2_blender_vnode import VNode, compute_vnodes @@ -51,20 +50,12 @@ class BlenderScene(): def create_animations(gltf): """Create animations.""" if gltf.data.animations: - for anim_idx, anim in enumerate(gltf.data.animations): - # 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 = [] - - BlenderAnimation.anim(gltf, anim_idx, 'root') - - for (obj, anim_name, action) in gltf.needs_stash: - simulate_stash(obj, anim_name, action) + for anim_idx, _anim in enumerate(gltf.data.animations): + BlenderAnimation.anim(gltf, anim_idx) # Restore first animation anim_name = gltf.data.animations[0].track_name - BlenderAnimation.restore_animation(gltf, 'root', anim_name) + BlenderAnimation.restore_animation(gltf, anim_name) @staticmethod def set_active_object(gltf): |