Ok, so I got feed up with generating clean pole vector for inverse kinematics controls and to have to align them properly (so that we don’t break the holy bindpose). I wrote a script to do this, it works really well so I wanted to share it with the community. You only have to select the constrained bone in pose mode and then run the script, it will generate a pole vector joint and fill everything in, try it and let me know what you think.
import bpy
from mathutils import Vector
from math import pi
# -------------------
# USER PARAMETERS
# -------------------
POLE_BONE_NAME = "PV_Auto"
DISTANCE = 0.4 # slide pole away/toward elbow (Blender units)
POLE_BONE_LENGTH = 0.15 # visual length of PV bone
DEFORM = False
PARENT_TO = None # e.g. "upper_arm.L" or None
# Error weights (tweak if needed)
W_HEAD_POS = 1.0 # head position match
W_ROT = 0.25 # rotation match
def ensure_mode(mode: str):
if bpy.context.mode != mode:
bpy.ops.object.mode_set(mode=mode)
def depsgraph_update():
bpy.context.view_layer.update()
def find_ik_constraint(pbone):
for c in pbone.constraints:
if c.type == 'IK':
return c
return None
def get_chain_bones(end_pb, ik_con):
"""Returns chain pose bones from start->...->end (end is constrained bone)."""
chain = [end_pb]
cur = end_pb
for _ in range(max(0, ik_con.chain_count - 1)):
if cur.parent is None:
break
cur = cur.parent
chain.append(cur)
chain.reverse()
return chain
def get_ik_target_world(ik_con):
"""
World position of IK target.
If subtarget is set, uses that pose bone head; else target object origin.
"""
tgt = ik_con.target
if tgt is None:
raise RuntimeError("IK constraint has no Target set (needed for 2-bone IK).")
if ik_con.subtarget and tgt.type == "ARMATURE":
pb = tgt.pose.bones.get(ik_con.subtarget)
if pb is None:
raise RuntimeError(f"IK subtarget bone not found: {ik_con.subtarget}")
return tgt.matrix_world @ pb.head
return tgt.matrix_world.translation
def compute_pole_dir(A: Vector, B: Vector, C: Vector):
"""Returns normalized pole direction in world space."""
AC = C - A
if AC.length < 1e-10:
raise ValueError("Start and end are too close; can't compute pole plane.")
t = (B - A).dot(AC) / AC.dot(AC)
proj = A + t * AC
pole_vec = (B - proj)
if pole_vec.length < 1e-10:
# chain straight; choose stable perpendicular fallback
fallback = Vector((1.0, 0.0, 0.0))
pole_vec = fallback - fallback.project(AC)
if pole_vec.length < 1e-10:
fallback = Vector((0.0, 1.0, 0.0))
pole_vec = fallback - fallback.project(AC)
return pole_vec.normalized()
def create_or_move_pole_bone(arm_obj, world_head: Vector, world_dir: Vector):
"""Create/move an EDIT bone at world_head, pointing along world_dir."""
M_inv = arm_obj.matrix_world.inverted()
head_local = M_inv @ world_head
dir_local = (M_inv.to_3x3() @ world_dir).normalized()
tail_local = head_local + dir_local * POLE_BONE_LENGTH
ensure_mode("EDIT")
eb = arm_obj.data.edit_bones.get(POLE_BONE_NAME)
if eb is None:
eb = arm_obj.data.edit_bones.new(POLE_BONE_NAME)
eb.head = head_local
eb.tail = tail_local
if PARENT_TO:
parent = arm_obj.data.edit_bones.get(PARENT_TO)
if parent:
eb.parent = parent
eb.use_connect = False
ensure_mode("POSE")
pb = arm_obj.pose.bones.get(POLE_BONE_NAME)
if pb:
pb.bone.use_deform = DEFORM
return POLE_BONE_NAME
def snapshot_chain(chain):
"""
Snapshot important pose info for each bone in chain:
- head position in armature object space (pb.head)
- rotation quaternion from pb.matrix (object space)
"""
snap = {}
for pb in chain:
snap[pb.name] = {
"head": pb.head.copy(),
"rot": pb.matrix.to_quaternion().copy(),
}
return snap
def pose_error(chain, snap):
"""
Sum error over chain:
- head position delta
- rotation delta
"""
err = 0.0
for pb in chain:
s = snap[pb.name]
dh = (pb.head - s["head"]).length
dq = pb.matrix.to_quaternion().rotation_difference(s["rot"]).angle
err += W_HEAD_POS * dh + W_ROT * dq
return err
def best_pole_angle_global(chain, ik_con, snap):
"""
Brute force pole_angle that minimizes overall pose change across chain.
Coarse sweep then refinement.
"""
depsgraph_update()
best_a = ik_con.pole_angle
best_e = 1e18
def eval_a(a):
ik_con.pole_angle = a
depsgraph_update()
return pose_error(chain, snap)
# coarse sweep
steps = 90
for i in range(steps):
a = -pi + (2 * pi) * (i / (steps - 1))
e = eval_a(a)
if e < best_e:
best_e = e
best_a = a
# refine around best
span = (2 * pi) / steps
for _ in range(7):
left, right = best_a - span, best_a + span
for i in range(31):
a = left + (right - left) * (i / 30)
e = eval_a(a)
if e < best_e:
best_e = e
best_a = a
span *= 0.35
ik_con.pole_angle = best_a
depsgraph_update()
return best_a, best_e
def run():
arm = bpy.context.object
if not arm or arm.type != "ARMATURE":
raise RuntimeError("Select an Armature object.")
ensure_mode("POSE")
end_pb = bpy.context.active_pose_bone
if not end_pb:
raise RuntimeError("Select the pose bone that has the IK constraint.")
ik = find_ik_constraint(end_pb)
if not ik:
raise RuntimeError("Active bone has no IK constraint.")
chain = get_chain_bones(end_pb, ik)
if len(chain) < 2:
raise RuntimeError("IK chain is too short (check parenting / chain_count).")
# Determine points A(start), B(elbow/mid), C(end)
start_pb = chain[0]
mid_pb = chain[1] if len(chain) >= 2 else chain[0]
A_world = arm.matrix_world @ start_pb.head
B_world = arm.matrix_world @ mid_pb.head
if len(chain) >= 3:
C_world = arm.matrix_world @ chain[-1].head
else:
# 2-bone IK: use IK target as end point
C_world = get_ik_target_world(ik)
# Create PV bone
pole_dir_w = compute_pole_dir(A_world, B_world, C_world)
pole_head_w = B_world + pole_dir_w * DISTANCE
pole_bone = create_or_move_pole_bone(arm, pole_head_w, pole_dir_w)
# Snapshot pose BEFORE enabling pole (this is what we want to preserve)
depsgraph_update()
snap = snapshot_chain(chain)
# Enable pole target
ik.pole_target = arm
ik.pole_subtarget = pole_bone
depsgraph_update()
# Solve pole_angle to preserve the snapped pose as closely as possible
angle, err = best_pole_angle_global(chain, ik, snap)
print(f"PV bone: {pole_bone}")
print(f"Pole angle set: {angle:.6f} rad | error: {err:.6e}")
print(f"Chain: {[b.name for b in chain]}")
run()