Skip to content

Instantly share code, notes, and snippets.

@fangzhangmnm
Created January 12, 2026 19:01
Show Gist options
  • Select an option

  • Save fangzhangmnm/a73d7d3f1c7c3c848ad2a488a9b0a215 to your computer and use it in GitHub Desktop.

Select an option

Save fangzhangmnm/a73d7d3f1c7c3c848ad2a488a9b0a215 to your computer and use it in GitHub Desktop.
# ------------------------------------------------------------
# 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