Created
January 12, 2026 19:01
-
-
Save fangzhangmnm/a73d7d3f1c7c3c848ad2a488a9b0a215 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # ------------------------------------------------------------ | |
| # Blender Armature Animation Rescale Utility | |
| # | |
| # Safely applies a UNIFORM scale to an Armature object while | |
| # preserving animation by rescaling all relevant location | |
| # FCurves across actions that affect the armature. | |
| # | |
| # - Asserts uniform armature scale | |
| # - Applies object scale (Ctrl+A equivalent) | |
| # - Rescales object & pose-bone location keyframes | |
| # - Prints every modified curve for debugging | |
| # | |
| # Usage: | |
| # Select the Armature object | |
| # Run this script in Object Mode | |
| # | |
| # Warning: | |
| # AI generated code. Use at your own risk! | |
| # | |
| # Author: chatgpt.com, Dec.25 2025 | |
| # Supervisor: fangzhangmnm | |
| # ------------------------------------------------------------ | |
| import bpy | |
| import math | |
| def rescale_armature_apply_scale_then_fix_actions(): | |
| arm = bpy.context.view_layer.objects.active | |
| if not arm or arm.type != "ARMATURE": | |
| raise RuntimeError("Select an Armature object as the active object.") | |
| # --- Read scale and assert uniform --- | |
| sx, sy, sz = arm.scale | |
| eps = 1e-6 | |
| if not (math.isclose(sx, sy, rel_tol=0.0, abs_tol=eps) and math.isclose(sx, sz, rel_tol=0.0, abs_tol=eps)): | |
| raise AssertionError(f"Armature scale is not uniform: {arm.scale}") | |
| s = float(sx) | |
| if math.isclose(s, 1.0, rel_tol=0.0, abs_tol=eps): | |
| print(f'Armature "{arm.name}" scale is already 1.0 — nothing to do.') | |
| return | |
| # --- Apply scaling to the armature object --- | |
| # Ensure correct context: object mode + selected | |
| bpy.ops.object.mode_set(mode='OBJECT') | |
| for o in bpy.context.selected_objects: | |
| o.select_set(False) | |
| arm.select_set(True) | |
| bpy.context.view_layer.objects.active = arm | |
| bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) | |
| print(f'Rescaled (applied scale) on armature: "{arm.name}" (baked factor was {s})') | |
| # --- Rescale location curves in all actions that affect this armature --- | |
| def action_affects_armature(action): | |
| for fc in action.fcurves: | |
| p = fc.data_path | |
| if p.startswith('pose.bones["'): | |
| return True | |
| if p == "location": | |
| return True | |
| return False | |
| def rescale_fcurve(fc, factor): | |
| for kp in fc.keyframe_points: | |
| kp.co[1] *= factor # 0 is t, 1 is value | |
| kp.handle_left[1] *= factor | |
| kp.handle_right[1] *= factor | |
| fc.update() | |
| for act in bpy.data.actions: | |
| if not action_affects_armature(act): | |
| continue | |
| for fc in act.fcurves: | |
| p = fc.data_path | |
| # Armature object location | |
| if p == "location": | |
| rescale_fcurve(fc, s) | |
| print(f'Rescaled curve: Action="{act.name}" FCurve="{p}[{fc.array_index}]"') | |
| continue | |
| # Pose bone location curves | |
| if p.startswith('pose.bones["') and p.endswith('"].location'): | |
| rescale_fcurve(fc, s) | |
| print(f'Rescaled curve: Action="{act.name}" FCurve="{p}[{fc.array_index}]"') | |
| def transform_current_action(tx, ty, tz, frame_start, frame_end): | |
| # frame_end is exclusive | |
| arm = bpy.context.view_layer.objects.active | |
| if not arm or arm.type != "ARMATURE": | |
| raise RuntimeError("Select an Armature object as the active object.") | |
| action = arm.animation_data.action | |
| if not action: | |
| raise RuntimeError("The active armature has no action assigned.") | |
| # Ensure correct context: object mode + selected | |
| bpy.ops.object.mode_set(mode='OBJECT') | |
| for o in bpy.context.selected_objects: | |
| o.select_set(False) | |
| arm.select_set(True) | |
| bpy.context.view_layer.objects.active = arm | |
| print(f'Transforming action "{action.name}" location keyframes by ({tx}, {ty}, {tz}) from frame {frame_start} to {frame_end} (exclusive)') | |
| def transform_fcurve(fc, delta, frame_start, frame_end): | |
| for kp in fc.keyframe_points: | |
| if frame_start <= kp.co[0] < frame_end: | |
| kp.co[1] += delta | |
| kp.handle_left[1] += delta | |
| kp.handle_right[1] += delta | |
| fc.update() | |
| def rotate_basis_for_bone(bone, tx, ty, tz): | |
| import mathutils | |
| mat = bone.bone.matrix_local.inverted() | |
| print("matrix",mat) | |
| delta_vec = mathutils.Vector((tx, ty, tz, 0)) | |
| rotated_delta = mat @ delta_vec | |
| return rotated_delta.x, rotated_delta.y, rotated_delta.z | |
| for fc in action.fcurves: | |
| p = fc.data_path | |
| # Pose bone location curves | |
| # only transform bones with no parent | |
| if p.startswith('pose.bones["') and p.endswith('"].location'): | |
| bone_name = p[len('pose.bones["'):-len('"].location')] | |
| bone = arm.pose.bones.get(bone_name) | |
| if bone and bone.parent is None: | |
| ttx, tty, ttz = rotate_basis_for_bone(bone, tx, ty, tz) | |
| if fc.array_index == 0: | |
| transform_fcurve(fc, ttx, frame_start, frame_end) | |
| elif fc.array_index == 1: | |
| transform_fcurve(fc, tty, frame_start, frame_end) | |
| elif fc.array_index == 2: | |
| transform_fcurve(fc, ttz, frame_start, frame_end) | |
| print(f'Transformed curve: Action="{action.name}" FCurve="{p}[{fc.array_index}]"') | |
| #rescale_armature_apply_scale_then_fix_actions() | |
| transform_current_action(0,-.2,0,5,32) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment