def get_bone_matrix(blender_obj_uuid_if_armature: typing.Optional[str], channels: typing.Tuple[bpy.types.FCurve], bake_bone: typing.Union[str, None], bake_channel: typing.Union[str, None], bake_range_start, bake_range_end, action_name: str, current_frame: int, step: int, export_settings ): blender_object_if_armature = export_settings['vtree'].nodes[blender_obj_uuid_if_armature].blender_object if blender_obj_uuid_if_armature is not None else None data = {} # Always using bake_range, because some bones may need to be baked, # even if user didn't request it start_frame = bake_range_start end_frame = bake_range_end frame = start_frame while frame <= end_frame: data[frame] = {} bpy.context.scene.frame_set(int(frame)) bones = export_settings['vtree'].get_all_bones(blender_obj_uuid_if_armature) for bone_uuid in bones: blender_bone = export_settings['vtree'].nodes[bone_uuid].blender_bone if export_settings['vtree'].nodes[bone_uuid].parent_uuid is not None and export_settings['vtree'].nodes[export_settings['vtree'].nodes[bone_uuid].parent_uuid].blender_type == VExportNode.BONE: blender_bone_parent = export_settings['vtree'].nodes[export_settings['vtree'].nodes[bone_uuid].parent_uuid].blender_bone rest_mat = blender_bone_parent.bone.matrix_local.inverted_safe() @ blender_bone.bone.matrix_local matrix = rest_mat.inverted_safe() @ blender_bone_parent.matrix.inverted_safe() @ blender_bone.matrix else: if blender_bone.parent is None: matrix = blender_bone.bone.matrix_local.inverted_safe() @ blender_bone.matrix else: # Bone has a parent, but in export, after filter, is at root of armature matrix = blender_bone.matrix.copy() data[frame][blender_bone.name] = matrix # If some drivers must be evaluated, do it here, to avoid to have to change frame by frame later drivers_to_manage = get_sk_drivers(blender_obj_uuid_if_armature, export_settings) for dr_obj_uuid, dr_fcurves in drivers_to_manage: vals = get_sk_driver_values(dr_obj_uuid, frame, dr_fcurves, export_settings) frame += step return data
def get_bone_matrix( blender_object_if_armature: typing.Optional[bpy.types.Object], channels: typing.Tuple[bpy.types.FCurve], bake_bone: typing.Union[str, None], bake_channel: typing.Union[str, None], bake_range_start, bake_range_end, action_name: str, current_frame: int, step: int): data = {} # Always using bake_range, because some bones may need to be baked, # even if user didn't request it start_frame = bake_range_start end_frame = bake_range_end frame = start_frame while frame <= end_frame: data[frame] = {} # we need to bake in the constraints bpy.context.scene.frame_set(frame) for pbone in blender_object_if_armature.pose.bones: if bake_bone is None: matrix = pbone.matrix_basis.copy() else: if (pbone.bone.use_inherit_rotation == False or pbone.bone.inherit_scale != "FULL" ) and pbone.parent != None: rest_mat = (pbone.parent.bone.matrix_local.inverted_safe() @ pbone.bone.matrix_local) matrix = ( rest_mat.inverted_safe() @ pbone.parent.matrix.inverted_safe() @ pbone.matrix) else: matrix = pbone.matrix matrix = blender_object_if_armature.convert_space( pose_bone=pbone, matrix=matrix, from_space='POSE', to_space='LOCAL') data[frame][pbone.name] = matrix # If some drivers must be evaluated, do it here, to avoid to have to change frame by frame later obj_driver = blender_object_if_armature.proxy if blender_object_if_armature.proxy else blender_object_if_armature drivers_to_manage = get_sk_drivers(obj_driver) for dr_obj, dr_fcurves in drivers_to_manage: vals = get_sk_driver_values(dr_obj, frame, dr_fcurves) frame += step return data
def get_bone_matrix( blender_object_if_armature: typing.Optional[bpy.types.Object], channels: typing.Tuple[bpy.types.FCurve], bake_bone: typing.Union[str, None], bake_channel: typing.Union[str, None], bake_range_start, bake_range_end, action_name: str, current_frame: int, step: int): data = {} # Always using bake_range, because some bones may need to be baked, # even if user didn't request it start_frame = bake_range_start end_frame = bake_range_end frame = start_frame while frame <= end_frame: data[frame] = {} # we need to bake in the constraints bpy.context.scene.frame_set(frame) for pbone in blender_object_if_armature.pose.bones: if bake_bone is None: matrix = pbone.matrix_basis else: matrix = pbone.matrix if bpy.app.version < (2, 80, 0): matrix = blender_object_if_armature.convert_space( pbone, matrix, 'POSE', 'LOCAL') else: matrix = blender_object_if_armature.convert_space( pose_bone=pbone, matrix=matrix, from_space='POSE', to_space='LOCAL') data[frame][pbone.name] = matrix # If some drivers must be evaluated, do it here, to avoid to have to change frame by frame later drivers_to_manage = get_sk_drivers(blender_object_if_armature) for dr_obj, dr_fcurves in drivers_to_manage: vals = get_sk_driver_values(dr_obj, frame, dr_fcurves) frame += step return data
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_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)