def wrapper_bonecache(*args, **kwargs): armature = args[-1]['vtree'].nodes[args[0]].blender_object cache_key_args = args cache_key_args = args[:-1] if cache_key_args[2] is None: pose_bone_if_armature = gltf2_blender_get.get_object_from_datapath( armature, cache_key_args[1][0].data_path) else: pose_bone_if_armature = armature.pose.bones[cache_key_args[2]] if not hasattr(func, "__current_action_name"): func.reset_cache() if cache_key_args[6] != func.__current_action_name or cache_key_args[ 0] != func.__current_armature_uuid: result = func(*args) func.__bonecache = result func.__current_action_name = cache_key_args[6] func.__current_armature_uuid = cache_key_args[0] return result[cache_key_args[7]][pose_bone_if_armature.name] else: return func.__bonecache[cache_key_args[7]][ pose_bone_if_armature.name]
def __get_channel_groups(blender_action: bpy.types.Action, blender_object: bpy.types.Object): targets = {} for fcurve in blender_action.fcurves: target_property = get_target_property_name(fcurve.data_path) object_path = get_target_object_path(fcurve.data_path) # find the object affected by this action if not object_path: target = blender_object else: try: target = gltf2_blender_get.get_object_from_datapath(blender_object, object_path) except ValueError as e: # if the object is a mesh and the action target path can not be resolved, we know that this is a morph # animation. if blender_object.type == "MESH": # if you need the specific shape key for some reason, this is it: # shape_key = blender_object.data.shape_keys.path_resolve(object_path) target = blender_object.data.shape_keys else: gltf2_io_debug.print_console("WARNING", "Animation target {} not found".format(object_path)) continue # group channels by target object and affected property of the target target_properties = targets.get(target, {}) channels = target_properties.get(target_property, []) channels.append(fcurve) target_properties[target_property] = channels targets[target] = target_properties groups = [] for p in targets.values(): groups += list(p.values()) return map(tuple, groups)
def __gather_armature_object_channel_groups(blender_action: bpy.types.Action, blender_object: bpy.types.Object, export_settings): targets = {} if blender_object.type != "ARMATURE": return tuple() delta_rotation_detection = [False, False] # Normal / Delta for fcurve in blender_action.fcurves: object_path = get_target_object_path(fcurve.data_path) if object_path != "": continue # In some invalid files, channel hasn't any keyframes ... this channel need to be ignored if len(fcurve.keyframe_points) == 0: continue try: target_property = get_target_property_name(fcurve.data_path) except: gltf2_io_debug.print_console( "WARNING", "Invalid animation fcurve name on action {}".format( blender_action.name)) continue target = gltf2_blender_get.get_object_from_datapath( blender_object, object_path) # Detect that armature is not multiple keyed for euler and quaternion # Keep only the current rotation mode used by bone rotation, delta, rotation_modes = get_rotation_modes(target_property) # Delta rotation management if delta is False: if delta_rotation_detection[ 1] is True: # normal rotation coming, but delta is already present continue delta_rotation_detection[0] = True else: if delta_rotation_detection[ 0] is True: # delta rotation coming, but normal is already present continue delta_rotation_detection[1] = True if rotation and target.rotation_mode not in rotation_modes: continue # group channels by target object and affected property of the target target_properties = targets.get(target, {}) channels = target_properties.get(target_property, []) channels.append(fcurve) target_properties[target_property] = channels targets[target] = target_properties groups = [] for p in targets.values(): groups += list(p.values()) return map(tuple, groups)
def needs_baking(blender_object_if_armature: typing.Optional[bpy.types.Object], channels: typing.Tuple[bpy.types.FCurve], export_settings ) -> bool: """ Check if baking is needed. Some blender animations need to be baked as they can not directly be expressed in glTF. """ def all_equal(lst): return lst[1:] == lst[:-1] # Sampling is forced if export_settings[gltf2_blender_export_keys.FORCE_SAMPLING]: return True # Sampling due to unsupported interpolation interpolation = channels[0].keyframe_points[0].interpolation if interpolation not in ["BEZIER", "LINEAR", "CONSTANT"]: gltf2_io_debug.print_console("WARNING", "Baking animation because of an unsupported interpolation method: {}".format( interpolation) ) return True if any(any(k.interpolation != interpolation for k in c.keyframe_points) for c in channels): # There are different interpolation methods in one action group gltf2_io_debug.print_console("WARNING", "Baking animation because there are keyframes with different " "interpolation methods in one channel" ) return True if not all_equal([len(c.keyframe_points) for c in channels]): gltf2_io_debug.print_console("WARNING", "Baking animation because the number of keyframes is not " "equal for all channel tracks") return True if len(channels[0].keyframe_points) <= 1: # we need to bake to 'STEP', as at least two keyframes are required to interpolate return True if not all(all_equal(key_times) for key_times in zip([[k.co[0] for k in c.keyframe_points] for c in channels])): # The channels have differently located keyframes gltf2_io_debug.print_console("WARNING", "Baking animation because of differently located keyframes in one channel") return True if blender_object_if_armature is not None: animation_target = gltf2_blender_get.get_object_from_datapath(blender_object_if_armature, channels[0].data_path) if isinstance(animation_target, bpy.types.PoseBone): if len(animation_target.constraints) != 0: # Constraints such as IK act on the bone -> can not be represented in glTF atm gltf2_io_debug.print_console("WARNING", "Baking animation because of unsupported constraints acting on the bone") return True return False
def needs_baking(blender_object_if_armature: typing.Optional[bpy.types.Object], channels: typing.Tuple[bpy.types.FCurve], export_settings ) -> bool: """ Check if baking is needed. Some blender animations need to be baked as they can not directly be expressed in glTF. """ def all_equal(lst): return lst[1:] == lst[:-1] # Sampling is forced if export_settings[gltf2_blender_export_keys.FORCE_SAMPLING]: return True # Sampling due to unsupported interpolation interpolation = channels[0].keyframe_points[0].interpolation if interpolation not in ["BEZIER", "LINEAR", "CONSTANT"]: gltf2_io_debug.print_console("WARNING", "Baking animation because of an unsupported interpolation method: {}".format( interpolation) ) return True if any(any(k.interpolation != interpolation for k in c.keyframe_points) for c in channels): # There are different interpolation methods in one action group gltf2_io_debug.print_console("WARNING", "Baking animation because there are keyframes with different " "interpolation methods in one channel" ) return True if not all_equal([len(c.keyframe_points) for c in channels]): gltf2_io_debug.print_console("WARNING", "Baking animation because the number of keyframes is not " "equal for all channel tracks") return True if len(channels[0].keyframe_points) <= 1: # we need to bake to 'STEP', as at least two keyframes are required to interpolate return True if not all_equal(list(zip([[k.co[0] for k in c.keyframe_points] for c in channels]))): # The channels have differently located keyframes gltf2_io_debug.print_console("WARNING", "Baking animation because of differently located keyframes in one channel") return True if blender_object_if_armature is not None: animation_target = gltf2_blender_get.get_object_from_datapath(blender_object_if_armature, channels[0].data_path) if isinstance(animation_target, bpy.types.PoseBone): if len(animation_target.constraints) != 0: # Constraints such as IK act on the bone -> can not be represented in glTF atm gltf2_io_debug.print_console("WARNING", "Baking animation because of unsupported constraints acting on the bone") return True return False
def gather_animation_sampler(channels: typing.Tuple[bpy.types.FCurve], blender_object: bpy.types.Object, bake_bone: typing.Union[str, None], bake_channel: typing.Union[str, None], bake_range_start, bake_range_end, action_name: str, driver_obj, export_settings ) -> gltf2_io.AnimationSampler: blender_object_if_armature = blender_object if blender_object.type == "ARMATURE" else None if blender_object_if_armature is not None and driver_obj is None: if bake_bone is None: pose_bone_if_armature = gltf2_blender_get.get_object_from_datapath(blender_object_if_armature, channels[0].data_path) else: pose_bone_if_armature = blender_object.pose.bones[bake_bone] else: pose_bone_if_armature = None non_keyed_values = __gather_non_keyed_values(channels, blender_object, blender_object_if_armature, pose_bone_if_armature, bake_channel, driver_obj, export_settings) sampler = gltf2_io.AnimationSampler( extensions=__gather_extensions(channels, blender_object_if_armature, export_settings, bake_bone, bake_channel), extras=__gather_extras(channels, blender_object_if_armature, export_settings, bake_bone, bake_channel), input=__gather_input(channels, blender_object_if_armature, non_keyed_values, bake_bone, bake_channel, bake_range_start, bake_range_end, action_name, driver_obj, export_settings), interpolation=__gather_interpolation(channels, blender_object_if_armature, export_settings, bake_bone, bake_channel), output=__gather_output(channels, blender_object.matrix_parent_inverse.copy().freeze(), blender_object_if_armature, non_keyed_values, bake_bone, bake_channel, bake_range_start, bake_range_end, action_name, driver_obj, export_settings) ) export_user_extensions('gather_animation_sampler_hook', export_settings, sampler, channels, blender_object, bake_bone, bake_channel, bake_range_start, bake_range_end, action_name) return sampler
def __get_channel_groups(blender_action: bpy.types.Action, blender_object: bpy.types.Object, export_settings): targets = {} for fcurve in blender_action.fcurves: # In some invalid files, channel hasn't any keyframes ... this channel need to be ignored if len(fcurve.keyframe_points) == 0: continue try: target_property = get_target_property_name(fcurve.data_path) except: gltf2_io_debug.print_console( "WARNING", "Invalid animation fcurve name on action {}".format( blender_action.name)) continue object_path = get_target_object_path(fcurve.data_path) # find the object affected by this action if not object_path: target = blender_object else: try: target = gltf2_blender_get.get_object_from_datapath( blender_object, object_path) if blender_object.type == "MESH" and object_path.startswith( "key_blocks"): shape_key = blender_object.data.shape_keys.path_resolve( object_path) if shape_key.mute is True: continue target = blender_object.data.shape_keys except ValueError as e: # if the object is a mesh and the action target path can not be resolved, we know that this is a morph # animation. if blender_object.type == "MESH": shape_key = blender_object.data.shape_keys.path_resolve( object_path) if shape_key.mute is True: continue target = blender_object.data.shape_keys else: gltf2_io_debug.print_console( "WARNING", "Animation target {} not found".format(object_path)) continue # group channels by target object and affected property of the target target_properties = targets.get(target, {}) channels = target_properties.get(target_property, []) channels.append(fcurve) target_properties[target_property] = channels targets[target] = target_properties groups = [] for p in targets.values(): groups += list(p.values()) return map(tuple, groups)
def wrapper_bonecache(*args, **kwargs): if args[2] is None: pose_bone_if_armature = gltf2_blender_get.get_object_from_datapath( args[0], args[1][0].data_path) else: pose_bone_if_armature = args[0].pose.bones[args[2]] if not hasattr(func, "__current_action_name"): func.__current_action_name = None func.__bonecache = {} if args[6] != func.__current_action_name: result = func(*args) func.__bonecache = result func.__current_action_name = args[6] return result[args[7]][pose_bone_if_armature.name] else: return func.__bonecache[args[7]][pose_bone_if_armature.name]
def __get_channel_groups(blender_action: bpy.types.Action, blender_object: bpy.types.Object, export_settings): targets = {} for fcurve in blender_action.fcurves: try: target_property = get_target_property_name(fcurve.data_path) except: gltf2_io_debug.print_console( "WARNING", "Invalid animation fcurve name on action {}".format( blender_action.name)) continue object_path = get_target_object_path(fcurve.data_path) # find the object affected by this action if not object_path: target = blender_object else: try: target = gltf2_blender_get.get_object_from_datapath( blender_object, object_path) except ValueError as e: # if the object is a mesh and the action target path can not be resolved, we know that this is a morph # animation. if blender_object.type == "MESH": # if you need the specific shape key for some reason, this is it: # shape_key = blender_object.data.shape_keys.path_resolve(object_path) target = blender_object.data.shape_keys else: gltf2_io_debug.print_console( "WARNING", "Animation target {} not found".format(object_path)) continue # group channels by target object and affected property of the target target_properties = targets.get(target, {}) channels = target_properties.get(target_property, []) channels.append(fcurve) target_properties[target_property] = channels targets[target] = target_properties groups = [] for p in targets.values(): groups += list(p.values()) return map(tuple, groups)
def __gather_output(channels: typing.Tuple[bpy.types.FCurve], parent_inverse, blender_object_if_armature: typing.Optional[bpy.types.Object], non_keyed_values: typing.Tuple[typing.Optional[float]], bake_bone: typing.Union[str, None], bake_channel: typing.Union[str, None], bake_range_start, bake_range_end, action_name, driver_obj, export_settings ) -> gltf2_io.Accessor: """Gather the data of the keyframes.""" keyframes = gltf2_blender_gather_animation_sampler_keyframes.gather_keyframes(blender_object_if_armature, channels, non_keyed_values, bake_bone, bake_channel, bake_range_start, bake_range_end, action_name, driver_obj, export_settings) if bake_bone is not None: target_datapath = "pose.bones['" + bake_bone + "']." + bake_channel else: target_datapath = [c for c in channels if c is not None][0].data_path is_yup = export_settings[gltf2_blender_export_keys.YUP] # bone animations need to be handled differently as they are in a different coordinate system if bake_bone is None: object_path = get_target_object_path(target_datapath) else: object_path = None is_armature_animation = bake_bone is not None or (blender_object_if_armature is not None and object_path != "") if is_armature_animation: if bake_bone is None: bone = gltf2_blender_get.get_object_from_datapath(blender_object_if_armature, object_path) else: bone = blender_object_if_armature.pose.bones[bake_bone] if isinstance(bone, bpy.types.PoseBone): if bone.parent is None: axis_basis_change = mathutils.Matrix.Identity(4) if export_settings[gltf2_blender_export_keys.YUP]: axis_basis_change = mathutils.Matrix( ((1.0, 0.0, 0.0, 0.0), (0.0, 0.0, 1.0, 0.0), (0.0, -1.0, 0.0, 0.0), (0.0, 0.0, 0.0, 1.0))) correction_matrix_local = axis_basis_change @ bone.bone.matrix_local else: correction_matrix_local = ( bone.parent.bone.matrix_local.inverted() @ bone.bone.matrix_local ) transform = correction_matrix_local else: transform = mathutils.Matrix.Identity(4) else: transform = parent_inverse values = [] for keyframe in keyframes: # Transform the data and build gltf control points value = gltf2_blender_math.transform(keyframe.value, target_datapath, transform) if is_yup and not is_armature_animation: value = gltf2_blender_math.swizzle_yup(value, target_datapath) keyframe_value = gltf2_blender_math.mathutils_to_gltf(value) if keyframe.in_tangent is not None: # we can directly transform the tangent as it currently is represented by a control point in_tangent = gltf2_blender_math.transform(keyframe.in_tangent, target_datapath, transform) if is_yup and blender_object_if_armature is None: in_tangent = gltf2_blender_math.swizzle_yup(in_tangent, target_datapath) # the tangent in glTF is relative to the keyframe value if not isinstance(value, list): in_tangent = value - in_tangent else: in_tangent = [value[i] - in_tangent[i] for i in range(len(value))] keyframe_value = gltf2_blender_math.mathutils_to_gltf(in_tangent) + keyframe_value # append if keyframe.out_tangent is not None: # we can directly transform the tangent as it currently is represented by a control point out_tangent = gltf2_blender_math.transform(keyframe.out_tangent, target_datapath, transform) if is_yup and blender_object_if_armature is None: out_tangent = gltf2_blender_math.swizzle_yup(out_tangent, target_datapath) # the tangent in glTF is relative to the keyframe value if not isinstance(value, list): out_tangent = value - out_tangent else: out_tangent = [value[i] - out_tangent[i] for i in range(len(value))] keyframe_value = keyframe_value + gltf2_blender_math.mathutils_to_gltf(out_tangent) # append values += keyframe_value # store the keyframe data in a binary buffer component_type = gltf2_io_constants.ComponentType.Float if get_target_property_name(target_datapath) == "value": # channels with 'weight' targets must have scalar accessors data_type = gltf2_io_constants.DataType.Scalar else: data_type = gltf2_io_constants.DataType.vec_type_from_num(len(keyframes[0].value)) return gltf2_io.Accessor( buffer_view=gltf2_io_binary_data.BinaryData.from_list(values, component_type), byte_offset=None, component_type=component_type, count=len(values) // gltf2_io_constants.DataType.num_elements(data_type), extensions=None, extras=None, max=None, min=None, name=None, normalized=None, sparse=None, type=data_type )
def gather_keyframes(blender_object_if_armature: typing.Optional[ bpy.types.Object], channels: typing.Tuple[bpy.types.FCurve], non_keyed_values: typing.Tuple[typing.Optional[float]], bake_bone: typing.Union[str, None], bake_channel: typing.Union[str, None], bake_range_start, bake_range_end, action_name: str, driver_obj, export_settings) -> typing.List[Keyframe]: """Convert the blender action groups' fcurves to keyframes for use in glTF.""" if bake_bone is None and driver_obj is None: # Find the start and end of the whole action group # Note: channels has some None items only for SK if some SK are not animated ranges = [ channel.range() for channel in channels if channel is not None ] start_frame = min([ channel.range()[0] for channel in channels if channel is not None ]) end_frame = max([ channel.range()[1] for channel in channels if channel is not None ]) else: start_frame = bake_range_start end_frame = bake_range_end keyframes = [] if needs_baking(blender_object_if_armature, channels, export_settings): # Bake the animation, by evaluating the animation for all frames # TODO: maybe baking can also be done with FCurve.convert_to_samples if blender_object_if_armature is not None and driver_obj is None: if bake_bone is None: pose_bone_if_armature = gltf2_blender_get.get_object_from_datapath( blender_object_if_armature, channels[0].data_path) else: pose_bone_if_armature = blender_object_if_armature.pose.bones[ bake_bone] else: pose_bone_if_armature = None # sample all frames frame = start_frame step = export_settings['gltf_frame_step'] while frame <= end_frame: key = Keyframe(channels, frame, bake_channel) if isinstance(pose_bone_if_armature, bpy.types.PoseBone): mat = get_bone_matrix(blender_object_if_armature, channels, bake_bone, bake_channel, bake_range_start, bake_range_end, action_name, frame, step) trans, rot, scale = mat.decompose() if bake_channel is None: target_property = channels[0].data_path.split('.')[-1] else: target_property = bake_channel key.value = { "location": trans, "rotation_axis_angle": rot, "rotation_euler": rot, "rotation_quaternion": rot, "scale": scale }[target_property] else: if driver_obj is None: # Note: channels has some None items only for SK if some SK are not animated key.value = [ c.evaluate(frame) for c in channels if c is not None ] complete_key(key, non_keyed_values) else: key.value = get_sk_driver_values(driver_obj, frame, channels) complete_key(key, non_keyed_values) keyframes.append(key) frame += step else: # Just use the keyframes as they are specified in blender # Note: channels has some None items only for SK if some SK are not animated frames = [ keyframe.co[0] for keyframe in [c for c in channels if c is not None][0].keyframe_points ] # some weird files have duplicate frame at same time, removed them frames = sorted(set(frames)) for i, frame in enumerate(frames): key = Keyframe(channels, frame, bake_channel) # key.value = [c.keyframe_points[i].co[0] for c in action_group.channels] key.value = [c.evaluate(frame) for c in channels if c is not None] # Complete key with non keyed values, if needed if len([c for c in channels if c is not None ]) != key.get_target_len(): complete_key(key, non_keyed_values) # compute tangents for cubic spline interpolation if [c for c in channels if c is not None ][0].keyframe_points[0].interpolation == "BEZIER": # Construct the in tangent if frame == frames[0]: # start in-tangent should become all zero key.set_first_tangent() else: # otherwise construct an in tangent coordinate from the keyframes control points. We intermediately # use a point at t-1 to define the tangent. This allows the tangent control point to be transformed # normally key.in_tangent = [ c.keyframe_points[i].co[1] + ((c.keyframe_points[i].co[1] - c.keyframe_points[i].handle_left[1]) / (frame - frames[i - 1])) for c in channels if c is not None ] # Construct the out tangent if frame == frames[-1]: # end out-tangent should become all zero key.set_last_tangent() else: # otherwise construct an in tangent coordinate from the keyframes control points. We intermediately # use a point at t+1 to define the tangent. This allows the tangent control point to be transformed # normally key.out_tangent = [ c.keyframe_points[i].co[1] + ((c.keyframe_points[i].handle_right[1] - c.keyframe_points[i].co[1]) / (frames[i + 1] - frame)) for c in channels if c is not None ] complete_key_tangents(key, non_keyed_values) keyframes.append(key) return keyframes
def gather_animation_sampler(channels: typing.Tuple[bpy.types.FCurve], blender_object: bpy.types.Object, bake_bone: typing.Union[str, None], bake_channel: typing.Union[str, None], bake_range_start, bake_range_end, action_name: str, driver_obj, export_settings) -> gltf2_io.AnimationSampler: if blender_object.animation_data and blender_object.animation_data.nla_tracks and export_settings[ gltf2_blender_export_keys.FRAME_RANGE]: # Attempt to adjust the bake range to match the nla track strip that matches the action_name. for track in blender_object.animation_data.nla_tracks: if not track.strips: continue for strip in track.strips: if strip.name == action_name: bake_range_start = strip.frame_start bake_range_end = strip.frame_end break blender_object_if_armature = blender_object if blender_object.type == "ARMATURE" else None if blender_object_if_armature is not None and driver_obj is None: if bake_bone is None: pose_bone_if_armature = gltf2_blender_get.get_object_from_datapath( blender_object_if_armature, channels[0].data_path) else: pose_bone_if_armature = blender_object.pose.bones[bake_bone] else: pose_bone_if_armature = None non_keyed_values = __gather_non_keyed_values(channels, blender_object, blender_object_if_armature, pose_bone_if_armature, bake_channel, driver_obj, export_settings) if blender_object.parent is not None: matrix_parent_inverse = blender_object.matrix_parent_inverse.copy( ).freeze() else: matrix_parent_inverse = mathutils.Matrix.Identity(4).freeze() sampler = gltf2_io.AnimationSampler( extensions=__gather_extensions(channels, blender_object_if_armature, export_settings, bake_bone, bake_channel), extras=__gather_extras(channels, blender_object_if_armature, export_settings, bake_bone, bake_channel), input=__gather_input(channels, blender_object_if_armature, non_keyed_values, bake_bone, bake_channel, bake_range_start, bake_range_end, action_name, driver_obj, export_settings), interpolation=__gather_interpolation(channels, blender_object_if_armature, export_settings, bake_bone, bake_channel), output=__gather_output(channels, matrix_parent_inverse, blender_object_if_armature, non_keyed_values, bake_bone, bake_channel, bake_range_start, bake_range_end, action_name, driver_obj, export_settings)) export_user_extensions('gather_animation_sampler_hook', export_settings, sampler, channels, blender_object, bake_bone, bake_channel, bake_range_start, bake_range_end, action_name) return sampler
def gather_animation_sampler(channels: typing.Tuple[bpy.types.FCurve], obj_uuid: str, bake_bone: typing.Union[str, None], bake_channel: typing.Union[str, None], bake_range_start, bake_range_end, force_range: bool, action_name: str, driver_obj_uuid, node_channel_is_animated: bool, need_rotation_correction, export_settings) -> gltf2_io.AnimationSampler: blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object is_armature = True if blender_object.type == "ARMATURE" else False blender_object_if_armature = blender_object if is_armature is True else None if is_armature is True and driver_obj_uuid is None: if bake_bone is None: pose_bone_if_armature = gltf2_blender_get.get_object_from_datapath( blender_object_if_armature, channels[0].data_path) else: pose_bone_if_armature = blender_object.pose.bones[bake_bone] else: pose_bone_if_armature = None non_keyed_values = __gather_non_keyed_values(channels, blender_object, blender_object_if_armature, pose_bone_if_armature, bake_channel, driver_obj_uuid, export_settings) if blender_object.parent is not None: matrix_parent_inverse = blender_object.matrix_parent_inverse.copy( ).freeze() else: matrix_parent_inverse = mathutils.Matrix.Identity(4).freeze() input = __gather_input(channels, obj_uuid, is_armature, non_keyed_values, bake_bone, bake_channel, bake_range_start, bake_range_end, force_range, action_name, driver_obj_uuid, node_channel_is_animated, export_settings) if input is None: # After check, no need to animate this node for this channel return None sampler = gltf2_io.AnimationSampler( extensions=__gather_extensions(channels, blender_object_if_armature, export_settings, bake_bone, bake_channel), extras=__gather_extras(channels, blender_object_if_armature, export_settings, bake_bone, bake_channel), input=input, interpolation=__gather_interpolation(channels, blender_object_if_armature, export_settings, bake_bone, bake_channel), output=__gather_output(channels, matrix_parent_inverse, obj_uuid, is_armature, non_keyed_values, bake_bone, bake_channel, bake_range_start, bake_range_end, force_range, action_name, driver_obj_uuid, node_channel_is_animated, need_rotation_correction, export_settings)) export_user_extensions('gather_animation_sampler_hook', export_settings, sampler, channels, blender_object, bake_bone, bake_channel, bake_range_start, bake_range_end, action_name) return sampler
def gather_keyframes(blender_object_if_armature: typing.Optional[ bpy.types.Object], channels: typing.Tuple[bpy.types.FCurve], export_settings) -> typing.List[Keyframe]: """Convert the blender action groups' fcurves to keyframes for use in glTF.""" # Find the start and end of the whole action group ranges = [channel.range() for channel in channels] start_frame = min([channel.range()[0] for channel in channels]) end_frame = max([channel.range()[1] for channel in channels]) keyframes = [] if needs_baking(blender_object_if_armature, channels, export_settings): # Bake the animation, by evaluating the animation for all frames # TODO: maybe baking can also be done with FCurve.convert_to_samples if blender_object_if_armature is not None: pose_bone_if_armature = gltf2_blender_get.get_object_from_datapath( blender_object_if_armature, channels[0].data_path) else: pose_bone_if_armature = None # sample all frames frame = start_frame step = export_settings['gltf_frame_step'] while frame <= end_frame: key = Keyframe(channels, frame) if isinstance(pose_bone_if_armature, bpy.types.PoseBone): # we need to bake in the constraints bpy.context.scene.frame_set(frame) trans, rot, scale = pose_bone_if_armature.matrix_basis.decompose( ) target_property = channels[0].data_path.split('.')[-1] key.value = { "location": trans, "rotation_axis_angle": rot, "rotation_euler": rot, "rotation_quaternion": rot, "scale": scale }[target_property] else: key.value = [c.evaluate(frame) for c in channels] keyframes.append(key) frame += step else: # Just use the keyframes as they are specified in blender frames = [keyframe.co[0] for keyframe in channels[0].keyframe_points] for i, frame in enumerate(frames): key = Keyframe(channels, frame) # key.value = [c.keyframe_points[i].co[0] for c in action_group.channels] key.value = [c.evaluate(frame) for c in channels] # compute tangents for cubic spline interpolation if channels[0].keyframe_points[0].interpolation == "BEZIER": # Construct the in tangent if frame == frames[0]: # start in-tangent should become all zero key.in_tangent = key.value else: # otherwise construct an in tangent coordinate from the keyframes control points. We intermediately # use a point at t-1 to define the tangent. This allows the tangent control point to be transformed # normally key.in_tangent = [ c.keyframe_points[i].co[1] + ((c.keyframe_points[i].co[1] - c.keyframe_points[i].handle_left[1]) / (frame - frames[i - 1])) for c in channels ] # Construct the out tangent if frame == frames[-1]: # end out-tangent should become all zero key.out_tangent = key.value else: # otherwise construct an in tangent coordinate from the keyframes control points. We intermediately # use a point at t+1 to define the tangent. This allows the tangent control point to be transformed # normally key.out_tangent = [ c.keyframe_points[i].co[1] + ((c.keyframe_points[i].handle_right[1] - c.keyframe_points[i].co[1]) / (frames[i + 1] - frame)) for c in channels ] keyframes.append(key) return keyframes
def __get_channel_groups(blender_action: bpy.types.Action, blender_object: bpy.types.Object, export_settings): targets = {} multiple_rotation_mode_detected = False for fcurve in blender_action.fcurves: # In some invalid files, channel hasn't any keyframes ... this channel need to be ignored if len(fcurve.keyframe_points) == 0: continue try: target_property = get_target_property_name(fcurve.data_path) except: gltf2_io_debug.print_console( "WARNING", "Invalid animation fcurve name on action {}".format( blender_action.name)) continue object_path = get_target_object_path(fcurve.data_path) # find the object affected by this action if not object_path: target = blender_object else: try: target = gltf2_blender_get.get_object_from_datapath( blender_object, object_path) if blender_object.type == "MESH" and object_path.startswith( "key_blocks"): shape_key = blender_object.data.shape_keys.path_resolve( object_path) if shape_key.mute is True: continue target = blender_object.data.shape_keys except ValueError as e: # if the object is a mesh and the action target path can not be resolved, we know that this is a morph # animation. if blender_object.type == "MESH": try: shape_key = blender_object.data.shape_keys.path_resolve( object_path) if shape_key.mute is True: continue target = blender_object.data.shape_keys except: # Something is wrong, for example a bone animation is linked to an object mesh... gltf2_io_debug.print_console( "WARNING", "Animation target {} not found".format( object_path)) continue else: gltf2_io_debug.print_console( "WARNING", "Animation target {} not found".format(object_path)) continue # Detect that object or bone are not multiple keyed for euler and quaternion # Keep only the current rotation mode used by object / bone rotation, rotation_modes = get_rotation_modes(target_property) if rotation and target.rotation_mode not in rotation_modes: multiple_rotation_mode_detected = True continue # group channels by target object and affected property of the target target_properties = targets.get(target, {}) channels = target_properties.get(target_property, []) channels.append(fcurve) target_properties[target_property] = channels targets[target] = target_properties groups = [] for p in targets.values(): groups += list(p.values()) if multiple_rotation_mode_detected is True: gltf2_io_debug.print_console( "WARNING", "Multiple rotation mode detected for {}".format( blender_object.name)) return map(tuple, groups)
def gather_animation_channels( obj_uuid: int, blender_action: bpy.types.Action, export_settings) -> typing.List[gltf2_io.AnimationChannel]: channels = [] blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object # First calculate range of animation for baking # This is need if user set 'Force sampling' and in case we need to bake bake_range_start = None bake_range_end = None force_range = False # If range is manually set, use it. Else, calculate it if blender_action.use_frame_range is True: bake_range_start = blender_action.frame_start bake_range_end = blender_action.frame_end force_range = True # keyframe_points is read-only, we cant restrict here else: groups = __get_channel_groups(blender_action, blender_object, export_settings) # Note: channels has some None items only for SK if some SK are not animated for chans in groups: ranges = [ channel.range() for channel in chans if channel is not None ] if bake_range_start is None: bake_range_start = min([ channel.range()[0] for channel in chans if channel is not None ]) else: bake_range_start = min( bake_range_start, min([ channel.range()[0] for channel in chans if channel is not None ])) if bake_range_end is None: bake_range_end = max([ channel.range()[1] for channel in chans if channel is not None ]) else: bake_range_end = max( bake_range_end, max([ channel.range()[1] for channel in chans if channel is not None ])) if blender_object.type == "ARMATURE" and export_settings[ 'gltf_force_sampling'] is True: # We have to store sampled animation data for every deformation bones # Check that there are some anim in this action if bake_range_start is None: return [] # Then bake all bones bones_to_be_animated = [] bones_uuid = export_settings["vtree"].get_all_bones(obj_uuid) bones_to_be_animated = [ blender_object.pose.bones[ export_settings["vtree"].nodes[b].blender_bone.name] for b in bones_uuid ] list_of_animated_bone_channels = [] for channel_group in __get_channel_groups(blender_action, blender_object, export_settings): channel_group_sorted = __get_channel_group_sorted( channel_group, blender_object) list_of_animated_bone_channels.extend([ (gltf2_blender_get.get_object_from_datapath( blender_object, get_target_object_path(i.data_path)).name, get_target_property_name(i.data_path)) for i in channel_group ]) for bone in bones_to_be_animated: for p in ["location", "rotation_quaternion", "scale"]: channel = gather_animation_channel( obj_uuid, (), export_settings, bone.name, p, bake_range_start, bake_range_end, force_range, blender_action.name, None, (bone.name, p) in list_of_animated_bone_channels) if channel is not None: channels.append(channel) # Retrieve animation on armature object itself, if any fcurves_armature = __gather_armature_object_channel_groups( blender_action, blender_object, export_settings) for channel_group in fcurves_armature: # No need to sort on armature, that can't have SK if len(channel_group) == 0: # Only errors on channels, ignoring continue channel = gather_animation_channel(obj_uuid, channel_group, export_settings, None, None, bake_range_start, bake_range_end, force_range, blender_action.name, None, True) if channel is not None: channels.append(channel) # Retrieve channels for drivers, if needed drivers_to_manage = gltf2_blender_gather_drivers.get_sk_drivers( obj_uuid, export_settings) for obj_driver_uuid, fcurves in drivers_to_manage: channel = gather_animation_channel(obj_uuid, fcurves, export_settings, None, None, bake_range_start, bake_range_end, force_range, blender_action.name, obj_driver_uuid, True) if channel is not None: channels.append(channel) else: done_paths = [] for channel_group in __get_channel_groups(blender_action, blender_object, export_settings): channel_group_sorted = __get_channel_group_sorted( channel_group, blender_object) if len(channel_group_sorted) == 0: # Only errors on channels, ignoring continue channel = gather_animation_channel(obj_uuid, channel_group_sorted, export_settings, None, None, bake_range_start, bake_range_end, force_range, blender_action.name, None, True) if channel is not None: channels.append(channel) # Store already done channel path target = [c for c in channel_group_sorted if c is not None][0].data_path.split('.')[-1] path = { "delta_location": "location", "delta_rotation_euler": "rotation_quaternion", "location": "location", "rotation_axis_angle": "rotation_quaternion", "rotation_euler": "rotation_quaternion", "rotation_quaternion": "rotation_quaternion", "scale": "scale", "value": "weights" }.get(target) if path is not None: done_paths.append(path) done_paths = list(set(done_paths)) if export_settings['gltf_selected'] is True and export_settings[ 'vtree'].tree_troncated is True: start_frame = min( [v[0] for v in [a.frame_range for a in bpy.data.actions]]) end_frame = max( [v[1] for v in [a.frame_range for a in bpy.data.actions]]) to_be_done = ['location', 'rotation_quaternion', 'scale'] to_be_done = [c for c in to_be_done if c not in done_paths] # In case of weight action, do nothing. # If there is only weight --> TRS is already managed at first if not (len(done_paths) == 1 and 'weights' in done_paths): for p in to_be_done: channel = gather_animation_channel( obj_uuid, (), export_settings, None, p, start_frame, end_frame, force_range, blender_action.name, None, False #If Object is not animated, don't keep animation for this channel ) if channel is not None: channels.append(channel) # resetting driver caches gltf2_blender_gather_drivers.get_sk_driver_values.reset_cache() gltf2_blender_gather_drivers.get_sk_drivers.reset_cache() # resetting bone caches gltf2_blender_gather_animation_sampler_keyframes.get_bone_matrix.reset_cache( ) return channels
def gather_keyframes(blender_obj_uuid: str, is_armature: bool, channels: typing.Tuple[bpy.types.FCurve], non_keyed_values: typing.Tuple[typing.Optional[float]], bake_bone: typing.Union[str, None], bake_channel: typing.Union[str, None], bake_range_start, bake_range_end, force_range: bool, action_name: str, driver_obj_uuid, node_channel_is_animated: bool, export_settings ) -> typing.Tuple[typing.List[Keyframe], bool]: """Convert the blender action groups' fcurves to keyframes for use in glTF.""" blender_object_if_armature = export_settings['vtree'].nodes[blender_obj_uuid].blender_object if is_armature is True is not None else None blender_obj_uuid_if_armature = blender_obj_uuid if is_armature is True else None if force_range is True: start_frame = bake_range_start end_frame = bake_range_end else: if bake_bone is None and driver_obj_uuid is None: # Find the start and end of the whole action group # Note: channels has some None items only for SK if some SK are not animated ranges = [channel.range() for channel in channels if channel is not None] if len(channels) != 0: start_frame = min([channel.range()[0] for channel in channels if channel is not None]) end_frame = max([channel.range()[1] for channel in channels if channel is not None]) else: start_frame = bake_range_start end_frame = bake_range_end else: start_frame = bake_range_start end_frame = bake_range_end keyframes = [] baking_is_needed = needs_baking(blender_object_if_armature, channels, export_settings) if baking_is_needed: # Bake the animation, by evaluating the animation for all frames if blender_object_if_armature is not None and driver_obj_uuid is None: if bake_bone is None: pose_bone_if_armature = gltf2_blender_get.get_object_from_datapath(blender_object_if_armature, channels[0].data_path) else: pose_bone_if_armature = blender_object_if_armature.pose.bones[bake_bone] else: pose_bone_if_armature = None # sample all frames frame = start_frame step = export_settings['gltf_frame_step'] while frame <= end_frame: key = Keyframe(channels, frame, bake_channel) if isinstance(pose_bone_if_armature, bpy.types.PoseBone): mat = get_bone_matrix( blender_obj_uuid_if_armature, channels, bake_bone, bake_channel, bake_range_start, bake_range_end, action_name, frame, step, export_settings ) trans, rot, scale = mat.decompose() if bake_channel is None: target_property = channels[0].data_path.split('.')[-1] else: target_property = bake_channel key.value = { "location": trans, "rotation_axis_angle": rot, "rotation_euler": rot, "rotation_quaternion": rot, "scale": scale }[target_property] else: if driver_obj_uuid is None: # If channel is TRS, we bake from world matrix, else this is SK if len(channels) != 0: target = [c for c in channels if c is not None][0].data_path.split('.')[-1] else: target = bake_channel if target == "value": #SK # Note: channels has some None items only for SK if some SK are not animated key.value = [c.evaluate(frame) for c in channels if c is not None] complete_key(key, non_keyed_values) else: mat = get_object_matrix(blender_obj_uuid, action_name, bake_range_start, bake_range_end, frame, step, export_settings) trans, rot, sca = mat.decompose() key.value_total = { "location": trans, "rotation_axis_angle": [rot.to_axis_angle()[1], rot.to_axis_angle()[0][0], rot.to_axis_angle()[0][1], rot.to_axis_angle()[0][2]], "rotation_euler": rot.to_euler(), "rotation_quaternion": rot, "scale": sca }[target] else: key.value = get_sk_driver_values(driver_obj_uuid, frame, channels, export_settings) complete_key(key, non_keyed_values) keyframes.append(key) frame += step else: # Just use the keyframes as they are specified in blender # Note: channels has some None items only for SK if some SK are not animated frames = [keyframe.co[0] for keyframe in [c for c in channels if c is not None][0].keyframe_points] # some weird files have duplicate frame at same time, removed them frames = sorted(set(frames)) if force_range is True: frames = [f for f in frames if f >= bake_range_start and f <= bake_range_end] for i, frame in enumerate(frames): key = Keyframe(channels, frame, bake_channel) # key.value = [c.keyframe_points[i].co[0] for c in action_group.channels] key.value = [c.evaluate(frame) for c in channels if c is not None] # Complete key with non keyed values, if needed if len([c for c in channels if c is not None]) != key.get_target_len(): complete_key(key, non_keyed_values) # compute tangents for cubic spline interpolation if [c for c in channels if c is not None][0].keyframe_points[0].interpolation == "BEZIER": # Construct the in tangent if frame == frames[0]: # start in-tangent should become all zero key.set_first_tangent() else: # otherwise construct an in tangent coordinate from the keyframes control points. We intermediately # use a point at t-1 to define the tangent. This allows the tangent control point to be transformed # normally key.in_tangent = [ c.keyframe_points[i].co[1] + ((c.keyframe_points[i].co[1] - c.keyframe_points[i].handle_left[1] ) / (frame - frames[i - 1])) for c in channels if c is not None ] # Construct the out tangent if frame == frames[-1]: # end out-tangent should become all zero key.set_last_tangent() else: # otherwise construct an in tangent coordinate from the keyframes control points. We intermediately # use a point at t+1 to define the tangent. This allows the tangent control point to be transformed # normally key.out_tangent = [ c.keyframe_points[i].co[1] + ((c.keyframe_points[i].handle_right[1] - c.keyframe_points[i].co[1] ) / (frames[i + 1] - frame)) for c in channels if c is not None ] complete_key_tangents(key, non_keyed_values) keyframes.append(key) if not export_settings[gltf2_blender_export_keys.OPTIMIZE_ANIMS]: return (keyframes, baking_is_needed) # For armature only # Check if all values are the same # In that case, if there is no real keyframe on this channel for this given bone, # We can ignore this keyframes # if there are some fcurve, we can keep only 2 keyframes, first and last if blender_object_if_armature is not None: cst = fcurve_is_constant(keyframes) if node_channel_is_animated is True: # fcurve on this bone for this property # Keep animation, but keep only 2 keyframes if data are not changing return ([keyframes[0], keyframes[-1]], baking_is_needed) if cst is True and len(keyframes) >= 2 else (keyframes, baking_is_needed) else: # bone is not animated (no fcurve) # Not keeping if not changing property return (None, baking_is_needed) if cst is True else (keyframes, baking_is_needed) else: # For objects, if all values are the same, we keep only first and last cst = fcurve_is_constant(keyframes) if node_channel_is_animated is True: return ([keyframes[0], keyframes[-1]], baking_is_needed) if cst is True and len(keyframes) >= 2 else (keyframes, baking_is_needed) else: # baked object (selected but not animated) return (None, baking_is_needed) if cst is True else (keyframes, baking_is_needed) return (keyframes, baking_is_needed)
def gather_keyframes(blender_object_if_armature: typing.Optional[bpy.types.Object], channels: typing.Tuple[bpy.types.FCurve], export_settings) -> typing.List[Keyframe]: """Convert the blender action groups' fcurves to keyframes for use in glTF.""" # Find the start and end of the whole action group ranges = [channel.range() for channel in channels] start_frame = min([channel.range()[0] for channel in channels]) end_frame = max([channel.range()[1] for channel in channels]) keyframes = [] if needs_baking(blender_object_if_armature, channels, export_settings): # Bake the animation, by evaluating the animation for all frames # TODO: maybe baking can also be done with FCurve.convert_to_samples if blender_object_if_armature is not None: pose_bone_if_armature = gltf2_blender_get.get_object_from_datapath(blender_object_if_armature, channels[0].data_path) else: pose_bone_if_armature = None # sample all frames frame = start_frame step = export_settings['gltf_frame_step'] while frame <= end_frame: key = Keyframe(channels, frame) if isinstance(pose_bone_if_armature, bpy.types.PoseBone): # we need to bake in the constraints bpy.context.scene.frame_set(frame) trans, rot, scale = pose_bone_if_armature.matrix_basis.decompose() target_property = channels[0].data_path.split('.')[-1] key.value = { "location": trans, "rotation_axis_angle": rot, "rotation_euler": rot, "rotation_quaternion": rot, "scale": scale }[target_property] else: key.value = [c.evaluate(frame) for c in channels] keyframes.append(key) frame += step else: # Just use the keyframes as they are specified in blender frames = [keyframe.co[0] for keyframe in channels[0].keyframe_points] for i, frame in enumerate(frames): key = Keyframe(channels, frame) # key.value = [c.keyframe_points[i].co[0] for c in action_group.channels] key.value = [c.evaluate(frame) for c in channels] # compute tangents for cubic spline interpolation if channels[0].keyframe_points[0].interpolation == "BEZIER": # Construct the in tangent if frame == frames[0]: # start in-tangent should become all zero key.in_tangent = key.value else: # otherwise construct an in tangent coordinate from the keyframes control points. We intermediately # use a point at t-1 to define the tangent. This allows the tangent control point to be transformed # normally key.in_tangent = [ c.keyframe_points[i].co[1] + ((c.keyframe_points[i].co[1] - c.keyframe_points[i].handle_left[1] ) / (frame - frames[i - 1])) for c in channels ] # Construct the out tangent if frame == frames[-1]: # end out-tangent should become all zero key.out_tangent = key.value else: # otherwise construct an in tangent coordinate from the keyframes control points. We intermediately # use a point at t+1 to define the tangent. This allows the tangent control point to be transformed # normally key.out_tangent = [ c.keyframe_points[i].co[1] + ((c.keyframe_points[i].handle_right[1] - c.keyframe_points[i].co[1] ) / (frames[i + 1] - frame)) for c in channels ] keyframes.append(key) return keyframes
def gather_animation_channels( blender_action: bpy.types.Action, blender_object: bpy.types.Object, export_settings) -> typing.List[gltf2_io.AnimationChannel]: channels = [] # First calculate range of animation for baking # This is need if user set 'Force sampling' and in case we need to bake bake_range_start = None bake_range_end = None groups = __get_channel_groups(blender_action, blender_object, export_settings) # Note: channels has some None items only for SK if some SK are not animated for chans in groups: ranges = [channel.range() for channel in chans if channel is not None] if bake_range_start is None: bake_range_start = min([ channel.range()[0] for channel in chans if channel is not None ]) else: bake_range_start = min( bake_range_start, min([ channel.range()[0] for channel in chans if channel is not None ])) if bake_range_end is None: bake_range_end = max([ channel.range()[1] for channel in chans if channel is not None ]) else: bake_range_end = max( bake_range_end, max([ channel.range()[1] for channel in chans if channel is not None ])) if blender_object.type == "ARMATURE" and export_settings[ 'gltf_force_sampling'] is True: # We have to store sampled animation data for every deformation bones # Check that there are some anim in this action if bake_range_start is None: return [] # Then bake all bones bones_to_be_animated = [] if export_settings["gltf_def_bones"] is False: bones_to_be_animated = blender_object.data.bones else: bones_to_be_animated, _, _ = gltf2_blender_gather_skins.get_bone_tree( None, blender_object) bones_to_be_animated = [ blender_object.pose.bones[b.name] for b in bones_to_be_animated ] list_of_animated_bone_channels = [] for channel_group in __get_channel_groups(blender_action, blender_object, export_settings): channel_group_sorted = __get_channel_group_sorted( channel_group, blender_object) list_of_animated_bone_channels.extend([ (gltf2_blender_get.get_object_from_datapath( blender_object, get_target_object_path(i.data_path)).name, get_target_property_name(i.data_path)) for i in channel_group ]) for bone in bones_to_be_animated: for p in ["location", "rotation_quaternion", "scale"]: channel = __gather_animation_channel( (), blender_object, export_settings, bone.name, p, bake_range_start, bake_range_end, blender_action.name, None, (bone.name, p) in list_of_animated_bone_channels) if channel is not None: channels.append(channel) # Retrieve animation on armature object itself, if any fcurves_armature = __gather_armature_object_channel_groups( blender_action, blender_object, export_settings) for channel_group in fcurves_armature: # No need to sort on armature, that can't have SK if len(channel_group) == 0: # Only errors on channels, ignoring continue channel = __gather_animation_channel(channel_group, blender_object, export_settings, None, None, bake_range_start, bake_range_end, blender_action.name, None, True) if channel is not None: channels.append(channel) # Retrieve channels for drivers, if needed obj_driver = blender_object.proxy if blender_object.proxy else blender_object drivers_to_manage = gltf2_blender_gather_drivers.get_sk_drivers( obj_driver) for obj, fcurves in drivers_to_manage: channel = __gather_animation_channel(fcurves, blender_object, export_settings, None, None, bake_range_start, bake_range_end, blender_action.name, obj, False) if channel is not None: channels.append(channel) else: for channel_group in __get_channel_groups(blender_action, blender_object, export_settings): channel_group_sorted = __get_channel_group_sorted( channel_group, blender_object) if len(channel_group_sorted) == 0: # Only errors on channels, ignoring continue channel = __gather_animation_channel( channel_group_sorted, blender_object, export_settings, None, None, bake_range_start, bake_range_end, blender_action.name, None, False) if channel is not None: channels.append(channel) # resetting driver caches gltf2_blender_gather_drivers.get_sk_driver_values.reset_cache() gltf2_blender_gather_drivers.get_sk_drivers.reset_cache() # resetting bone caches gltf2_blender_gather_animation_sampler_keyframes.get_bone_matrix.reset_cache( ) return channels
def __gather_output(channels: typing.Tuple[bpy.types.FCurve], parent_inverse, blender_object_if_armature: typing.Optional[bpy.types.Object], export_settings ) -> gltf2_io.Accessor: """Gather the data of the keyframes.""" keyframes = gltf2_blender_gather_animation_sampler_keyframes.gather_keyframes(blender_object_if_armature, channels, export_settings) target_datapath = channels[0].data_path transform = mathutils.Matrix.Identity(4) is_yup = export_settings[gltf2_blender_export_keys.YUP] # bone animations need to be handled differently as they are in a different coordinate system object_path = get_target_object_path(target_datapath) is_armature_animation = blender_object_if_armature is not None and object_path != "" if is_armature_animation: bone = gltf2_blender_get.get_object_from_datapath(blender_object_if_armature, object_path) if isinstance(bone, bpy.types.PoseBone): if bone.parent is None: axis_basis_change = mathutils.Matrix.Identity(4) if export_settings[gltf2_blender_export_keys.YUP]: axis_basis_change = mathutils.Matrix( ((1.0, 0.0, 0.0, 0.0), (0.0, 0.0, 1.0, 0.0), (0.0, -1.0, 0.0, 0.0), (0.0, 0.0, 0.0, 1.0))) correction_matrix_local = gltf2_blender_math.multiply(axis_basis_change, bone.bone.matrix_local) else: correction_matrix_local = gltf2_blender_math.multiply( bone.parent.bone.matrix_local.inverted(), bone.bone.matrix_local) transform = correction_matrix_local values = [] for keyframe in keyframes: # Transform the data and build gltf control points value = gltf2_blender_math.transform(keyframe.value, target_datapath, transform) if is_yup and not is_armature_animation: value = gltf2_blender_math.swizzle_yup(value, target_datapath) keyframe_value = gltf2_blender_math.mathutils_to_gltf(value) if keyframe.in_tangent is not None: # we can directly transform the tangent as it currently is represented by a control point in_tangent = gltf2_blender_math.transform(keyframe.in_tangent, target_datapath, transform) if is_yup and blender_object_if_armature is None: in_tangent = gltf2_blender_math.swizzle_yup(in_tangent, target_datapath) # the tangent in glTF is relative to the keyframe value in_tangent = value - in_tangent if not isinstance(value, list) else [value[0] - in_tangent[0]] keyframe_value = gltf2_blender_math.mathutils_to_gltf(in_tangent) + keyframe_value # append if keyframe.out_tangent is not None: # we can directly transform the tangent as it currently is represented by a control point out_tangent = gltf2_blender_math.transform(keyframe.out_tangent, target_datapath, transform) if is_yup and blender_object_if_armature is None: out_tangent = gltf2_blender_math.swizzle_yup(out_tangent, target_datapath) # the tangent in glTF is relative to the keyframe value out_tangent = value - out_tangent if not isinstance(value, list) else [value[0] - out_tangent[0]] keyframe_value = keyframe_value + gltf2_blender_math.mathutils_to_gltf(out_tangent) # append values += keyframe_value # store the keyframe data in a binary buffer component_type = gltf2_io_constants.ComponentType.Float if get_target_property_name(target_datapath) == "value": # channels with 'weight' targets must have scalar accessors data_type = gltf2_io_constants.DataType.Scalar else: data_type = gltf2_io_constants.DataType.vec_type_from_num(len(keyframes[0].value)) return gltf2_io.Accessor( buffer_view=gltf2_io_binary_data.BinaryData.from_list(values, component_type), byte_offset=None, component_type=component_type, count=len(values) // gltf2_io_constants.DataType.num_elements(data_type), extensions=None, extras=None, max=None, min=None, name=None, normalized=None, sparse=None, type=data_type )