File headers: use SPDX license identifiers
[blender-addons.git] / io_scene_gltf2 / blender / imp / gltf2_blender_animation_node.py
blobacc7767fd5e006e8c242d058b1a7687f22afc534
1 # SPDX-License-Identifier: Apache-2.0
2 # Copyright 2018-2021 The glTF-Blender-IO authors.
4 import bpy
5 from mathutils import Vector
7 from ...io.imp.gltf2_io_binary import BinaryData
8 from .gltf2_blender_animation_utils import make_fcurve
9 from .gltf2_blender_vnode import VNode
10 from io_scene_gltf2.io.imp.gltf2_io_user_extensions import import_user_extensions
13 class BlenderNodeAnim():
14 """Blender Object Animation."""
15 def __new__(cls, *args, **kwargs):
16 raise RuntimeError("%s should not be instantiated" % cls)
18 @staticmethod
19 def anim(gltf, anim_idx, node_idx):
20 """Manage animation targeting a node's TRS."""
21 animation = gltf.data.animations[anim_idx]
22 node = gltf.data.nodes[node_idx]
23 if anim_idx not in node.animations.keys():
24 return
26 for channel_idx in node.animations[anim_idx]:
27 channel = animation.channels[channel_idx]
28 if channel.target.path not in ['translation', 'rotation', 'scale']:
29 continue
31 BlenderNodeAnim.do_channel(gltf, anim_idx, node_idx, channel)
33 @staticmethod
34 def do_channel(gltf, anim_idx, node_idx, channel):
35 animation = gltf.data.animations[anim_idx]
36 vnode = gltf.vnodes[node_idx]
37 path = channel.target.path
39 import_user_extensions('gather_import_animation_channel_before_hook', gltf, animation, vnode, path, channel)
41 action = BlenderNodeAnim.get_or_create_action(gltf, node_idx, animation.track_name)
43 keys = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].input)
44 values = BinaryData.get_data_from_accessor(gltf, animation.samplers[channel.sampler].output)
46 if animation.samplers[channel.sampler].interpolation == "CUBICSPLINE":
47 # TODO manage tangent?
48 values = values[1::3]
50 # Convert the curve from glTF to Blender.
52 if path == "translation":
53 blender_path = "location"
54 group_name = "Location"
55 num_components = 3
56 values = [gltf.loc_gltf_to_blender(vals) for vals in values]
57 values = vnode.base_locs_to_final_locs(values)
59 elif path == "rotation":
60 blender_path = "rotation_quaternion"
61 group_name = "Rotation"
62 num_components = 4
63 values = [gltf.quaternion_gltf_to_blender(vals) for vals in values]
64 values = vnode.base_rots_to_final_rots(values)
66 elif path == "scale":
67 blender_path = "scale"
68 group_name = "Scale"
69 num_components = 3
70 values = [gltf.scale_gltf_to_blender(vals) for vals in values]
71 values = vnode.base_scales_to_final_scales(values)
73 # Objects parented to a bone are translated to the bone tip by default.
74 # Correct for this by translating backwards from the tip to the root.
75 if vnode.type == VNode.Object and path == "translation":
76 if vnode.parent is not None and gltf.vnodes[vnode.parent].type == VNode.Bone:
77 bone_length = gltf.vnodes[vnode.parent].bone_length
78 off = Vector((0, -bone_length, 0))
79 values = [vals + off for vals in values]
81 if vnode.type == VNode.Bone:
82 # Need to animate the pose bone when the node is a bone.
83 group_name = vnode.blender_bone_name
84 blender_path = 'pose.bones["%s"].%s' % (
85 bpy.utils.escape_identifier(vnode.blender_bone_name),
86 blender_path
89 # We have the final TRS of the bone in values. We need to give
90 # the TRS of the pose bone though, which is relative to the edit
91 # bone.
93 # Final = EditBone * PoseBone
94 # where
95 # Final = Trans[ft] Rot[fr] Scale[fs]
96 # EditBone = Trans[et] Rot[er]
97 # PoseBone = Trans[pt] Rot[pr] Scale[ps]
99 # Solving for PoseBone gives
101 # pt = Rot[er^{-1}] (ft - et)
102 # pr = er^{-1} fr
103 # ps = fs
105 if path == 'translation':
106 edit_trans, edit_rot = vnode.editbone_trans, vnode.editbone_rot
107 edit_rot_inv = edit_rot.conjugated()
108 values = [
109 edit_rot_inv @ (trans - edit_trans)
110 for trans in values
113 elif path == 'rotation':
114 edit_rot = vnode.editbone_rot
115 edit_rot_inv = edit_rot.conjugated()
116 values = [
117 edit_rot_inv @ rot
118 for rot in values
121 elif path == 'scale':
122 pass # no change needed
124 # To ensure rotations always take the shortest path, we flip
125 # adjacent antipodal quaternions.
126 if path == 'rotation':
127 for i in range(1, len(values)):
128 if values[i].dot(values[i-1]) < 0:
129 values[i] = -values[i]
131 fps = bpy.context.scene.render.fps
133 coords = [0] * (2 * len(keys))
134 coords[::2] = (key[0] * fps for key in keys)
136 for i in range(0, num_components):
137 coords[1::2] = (vals[i] for vals in values)
138 make_fcurve(
139 action,
140 coords,
141 data_path=blender_path,
142 index=i,
143 group_name=group_name,
144 interpolation=animation.samplers[channel.sampler].interpolation,
147 import_user_extensions('gather_import_animation_channel_after_hook', gltf, animation, vnode, path, channel, action)
149 @staticmethod
150 def get_or_create_action(gltf, node_idx, anim_name):
151 vnode = gltf.vnodes[node_idx]
153 if vnode.type == VNode.Bone:
154 # For bones, the action goes on the armature.
155 vnode = gltf.vnodes[vnode.bone_arma]
157 obj = vnode.blender_object
159 action = gltf.action_cache.get(obj.name)
160 if not action:
161 name = anim_name + "_" + obj.name
162 action = bpy.data.actions.new(name)
163 action.id_root = 'OBJECT'
164 gltf.needs_stash.append((obj, action))
165 gltf.action_cache[obj.name] = action
167 return action