Generate clean pole vectors

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()
3 Likes

Hey ! I didn’t tested it but it sounds like a nice gem ! I moved it to a more appropriate category !

Thanks for sharing !

1 Like