import bpy from mathutils import Matrix from mathutils import Vector # 사용 방법 # 1. 대략 T포즈로 만들고 싶은 Armature를 활성 선택한다. # 2. Pose Mode에서 이 스크립트를 실행한다. # 3. 결과가 뒤집히면 FORWARD_AXIS 부호를 바꿔 다시 실행한다. # 4. 팔 방향이 반대로 가면 LEFT_AXIS / RIGHT_AXIS를 서로 바꿔 다시 실행한다. # 축 설정 # Blender FBX는 전방이 -Y인 경우가 많다. UP_AXIS = Vector((0.0, 0.0, 1.0)) FORWARD_AXIS = Vector((0.0, -1.0, 0.0)) LEFT_AXIS = Vector((1.0, 0.0, 0.0)) RIGHT_AXIS = Vector((-1.0, 0.0, 0.0)) DOWN_AXIS = Vector((0.0, 0.0, -1.0)) # 현재 포즈를 최대한 유지한 채 방향만 맞추려면 False # 먼저 완전히 Rest Pose로 돌려놓고 시작하려면 True RESET_TO_REST_BEFORE_ALIGN = False # 현재 프레임에 키를 찍고 싶으면 True INSERT_KEYFRAME = False BONE_PATTERNS = { "hips": ["hips", "pelvis", "root", "cog"], "spine": ["spine", "spine_01", "spine1", "abdomen"], "chest": ["chest", "spine_02", "spine2", "upperchest", "upper_chest"], "neck": ["neck", "neck_01"], "head": ["head"], "shoulder_l": ["shoulder.l", "clavicle_l", "clavicle.l", "leftshoulder", "shoulder_l"], "upperarm_l": ["upperarm_l", "upperarm.l", "arm_l", "leftarm", "upper_arm_l"], "forearm_l": ["lowerarm_l", "forearm_l", "forearm.l", "leftforearm", "lower_arm_l"], "hand_l": ["hand_l", "hand.l", "lefthand"], "shoulder_r": ["shoulder.r", "clavicle_r", "clavicle.r", "rightshoulder", "shoulder_r"], "upperarm_r": ["upperarm_r", "upperarm.r", "arm_r", "rightarm", "upper_arm_r"], "forearm_r": ["lowerarm_r", "forearm_r", "forearm.r", "rightforearm", "lower_arm_r"], "hand_r": ["hand_r", "hand.r", "righthand"], "thigh_l": ["thigh_l", "upleg_l", "upleg.l", "leftupleg", "upperleg_l"], "shin_l": ["calf_l", "lowerleg_l", "shin_l", "leg_l", "lowerleg.l"], "foot_l": ["foot_l", "foot.l", "leftfoot"], "toe_l": ["toe_l", "toes_l", "ball_l", "toe.l", "toes.l", "ball.l"], "thigh_r": ["thigh_r", "upleg_r", "upleg.r", "rightupleg", "upperleg_r"], "shin_r": ["calf_r", "lowerleg_r", "shin_r", "leg_r", "lowerleg.r"], "foot_r": ["foot_r", "foot.r", "rightfoot"], "toe_r": ["toe_r", "toes_r", "ball_r", "toe.r", "toes.r", "ball.r"], } def normalize_name(name: str) -> str: return name.lower().replace(" ", "").replace("-", "").replace(":", "").replace("_", "") def find_pose_bone(armature_obj: bpy.types.Object, key: str): patterns = [normalize_name(item) for item in BONE_PATTERNS[key]] for pose_bone in armature_obj.pose.bones: normalized = normalize_name(pose_bone.name) if normalized in patterns: return pose_bone for pose_bone in armature_obj.pose.bones: normalized = normalize_name(pose_bone.name) for pattern in patterns: if pattern in normalized: return pose_bone return None def get_bone_vector(pose_bone: bpy.types.PoseBone) -> Vector: children = [child for child in pose_bone.children if child.bone.use_deform] if children: child = children[0] vector = child.head - pose_bone.head else: vector = pose_bone.tail - pose_bone.head if vector.length < 1e-6: vector = pose_bone.tail - pose_bone.head return vector.normalized() def rotate_pose_bone_towards(pose_bone: bpy.types.PoseBone, target_direction: Vector): current_direction = get_bone_vector(pose_bone) target_direction = target_direction.normalized() if current_direction.length < 1e-6 or target_direction.length < 1e-6: return rotation = current_direction.rotation_difference(target_direction) pivot = pose_bone.head.copy() transform = ( Matrix.Translation(pivot) @ rotation.to_matrix().to_4x4() @ Matrix.Translation(-pivot) ) pose_bone.matrix = transform @ pose_bone.matrix def insert_rotation_keyframe(pose_bone: bpy.types.PoseBone): if pose_bone.rotation_mode == "QUATERNION": pose_bone.keyframe_insert(data_path="rotation_quaternion") elif pose_bone.rotation_mode == "AXIS_ANGLE": pose_bone.keyframe_insert(data_path="rotation_axis_angle") else: pose_bone.keyframe_insert(data_path="rotation_euler") def align_bone(armature_obj: bpy.types.Object, key: str, target_direction: Vector, label: str, report: list[str]): pose_bone = find_pose_bone(armature_obj, key) if pose_bone is None: report.append(f"[누락] {label}") return rotate_pose_bone_towards(pose_bone, target_direction) bpy.context.view_layer.update() if INSERT_KEYFRAME: insert_rotation_keyframe(pose_bone) report.append(f"[정렬] {label}: {pose_bone.name}") def main(): armature_obj = bpy.context.object if armature_obj is None or armature_obj.type != "ARMATURE": raise RuntimeError("활성 오브젝트가 Armature가 아닙니다.") if bpy.context.mode != "POSE": raise RuntimeError("Pose Mode에서 실행해야 합니다.") if RESET_TO_REST_BEFORE_ALIGN: bpy.ops.pose.select_all(action="SELECT") bpy.ops.pose.transforms_clear() bpy.context.view_layer.update() report = [] align_bone(armature_obj, "spine", UP_AXIS, "Spine", report) align_bone(armature_obj, "chest", UP_AXIS, "Chest", report) align_bone(armature_obj, "neck", UP_AXIS, "Neck", report) align_bone(armature_obj, "head", UP_AXIS, "Head", report) align_bone(armature_obj, "shoulder_l", LEFT_AXIS, "Shoulder_L", report) align_bone(armature_obj, "upperarm_l", LEFT_AXIS, "UpperArm_L", report) align_bone(armature_obj, "forearm_l", LEFT_AXIS, "ForeArm_L", report) align_bone(armature_obj, "hand_l", LEFT_AXIS, "Hand_L", report) align_bone(armature_obj, "shoulder_r", RIGHT_AXIS, "Shoulder_R", report) align_bone(armature_obj, "upperarm_r", RIGHT_AXIS, "UpperArm_R", report) align_bone(armature_obj, "forearm_r", RIGHT_AXIS, "ForeArm_R", report) align_bone(armature_obj, "hand_r", RIGHT_AXIS, "Hand_R", report) align_bone(armature_obj, "thigh_l", DOWN_AXIS, "Thigh_L", report) align_bone(armature_obj, "shin_l", DOWN_AXIS, "Shin_L", report) align_bone(armature_obj, "foot_l", FORWARD_AXIS, "Foot_L", report) align_bone(armature_obj, "toe_l", FORWARD_AXIS, "Toe_L", report) align_bone(armature_obj, "thigh_r", DOWN_AXIS, "Thigh_R", report) align_bone(armature_obj, "shin_r", DOWN_AXIS, "Shin_R", report) align_bone(armature_obj, "foot_r", FORWARD_AXIS, "Foot_R", report) align_bone(armature_obj, "toe_r", FORWARD_AXIS, "Toe_R", report) print("\n".join(report)) print("대략 T포즈 정렬이 끝났습니다. 결과가 뒤집히면 상단 축 설정을 바꿔 다시 실행하세요.") main()