import bpy
import random
import math
import os
# -------------------------
# 설정
# -------------------------
NUM_IMAGES = 200
ARMATURE_NAME = "metarig" # 랜덤 포즈를 적용할 아마추어 이름
MAX_ANGLE_DEG = 25.0 # 본 하나가 회전할 수 있는 최대 각도(도)
EXCLUDE_NAME_KEYWORDS = [ # 이 문자열이 이름에 포함된 본은 건드리지 않음
"root", "pelvis", "spine", "metarig",
]
# IK/Copy Rotation/Child Of 등 다른 소스가 회전을 덮어쓰는 컨스트레인트 타입.
# 이런 컨스트레인트가 걸린 본은 rotation_euler를 바꿔도 무시되므로 건너뜁니다.
SKIP_CONSTRAINT_TYPES = {"IK", "COPY_ROTATION", "COPY_TRANSFORMS", "CHILD_OF"}
RESPECT_ROTATION_LIMITS = True
RANDOM_SEED = None
DEBUG = True
armature = bpy.data.objects[ARMATURE_NAME]
scene = bpy.context.scene
if DEBUG:
print(f"[DEBUG] 현재 씬: {scene.name} / 전체 씬 목록: {list(bpy.data.scenes.keys())}")
print(f"[DEBUG] 아마추어: {armature.name}, 모드: {armature.mode}")
print(f"[DEBUG] pose_position (변경 전): {armature.data.pose_position}")
print(f"[DEBUG] animation_data: {armature.animation_data}")
if armature.animation_data and armature.animation_data.action:
print(f"[DEBUG] action: {armature.animation_data.action.name}")
# 1) Rest Position으로 설정되어 있으면 본 회전이 렌더에 반영되지 않으므로 강제로 Pose Position 설정
if armature.data.pose_position != 'POSE':
armature.data.pose_position = 'POSE'
if DEBUG:
print(f"[DEBUG] pose_position을 'POSE'로 변경함")
# 2) 액션(F-curve)이 물려있으면 매 프레임 평가 시 수동으로 넣은 rotation_euler를 덮어쓰므로 해제
if armature.animation_data and armature.animation_data.action is not None:
if DEBUG:
print(f"[DEBUG] 기존 액션 '{armature.animation_data.action.name}' 을 해제함 (수동 포즈와 충돌 방지)")
armature.animation_data.action = None
scene.render.engine = 'BLENDER_EEVEE'
scene.render.resolution_x = 640
scene.render.resolution_y = 480
scene.render.resolution_percentage = 100
# 저장 폴더 (blend 파일 기준)
output_dir = bpy.path.abspath("//render_output")
os.makedirs(output_dir, exist_ok=True)
# PNG 저장
scene.render.image_settings.file_format = 'PNG'
def should_skip(bone_name: str) -> bool:
lower = bone_name.lower()
return any(kw.lower() in lower for kw in EXCLUDE_NAME_KEYWORDS)
def has_blocking_constraint(pbone) -> bool:
"""IK 등 다른 소스가 회전을 덮어쓰는 컨스트레인트가 있으면 True."""
for c in pbone.constraints:
if c.mute:
continue
if c.type in SKIP_CONSTRAINT_TYPES:
return True
return False
def get_rotation_limits(pbone):
"""본에 Limit Rotation 컨스트레인트가 있으면 (min, max) 라디안 튜플 반환."""
for c in pbone.constraints:
if c.type == "LIMIT_ROTATION" and not c.mute:
return (
(c.min_x, c.max_x),
(c.min_y, c.max_y),
(c.min_z, c.max_z),
c.use_limit_x, c.use_limit_y, c.use_limit_z,
)
return None
def random_rotate_bone(pbone, max_angle_rad):
limits = get_rotation_limits(pbone) if RESPECT_ROTATION_LIMITS else None
prev_mode = pbone.rotation_mode
if prev_mode != "XYZ":
pbone.rotation_mode = "XYZ"
rx = random.uniform(-max_angle_rad, max_angle_rad)
ry = random.uniform(-max_angle_rad, max_angle_rad)
rz = random.uniform(-max_angle_rad, max_angle_rad)
if limits:
(minx, maxx), (miny, maxy), (minz, maxz), ux, uy, uz = limits
if ux:
rx = max(minx, min(maxx, rx))
if uy:
ry = max(miny, min(maxy, ry))
if uz:
rz = max(minz, min(maxz, rz))
pbone.rotation_euler = (rx, ry, rz)
def randomize_pose():
max_angle_rad = math.radians(MAX_ANGLE_DEG)
rotated = 0
skipped_name = 0
skipped_constraint = 0
for pbone in armature.pose.bones:
if should_skip(pbone.name):
skipped_name += 1
continue
if has_blocking_constraint(pbone):
skipped_constraint += 1
continue
random_rotate_bone(pbone, max_angle_rad)
rotated += 1
if DEBUG:
print(f"[DEBUG] 회전 적용: {rotated}개 / 이름 제외: {skipped_name}개 / 컨스트레인트 제외: {skipped_constraint}개")
if RANDOM_SEED is not None:
random.seed(RANDOM_SEED)
bpy.context.view_layer.objects.active = armature
prev_mode = armature.mode
if armature.mode != "POSE":
result = bpy.ops.object.mode_set(mode="POSE")
if DEBUG:
print(f"[DEBUG] mode_set(POSE) 결과: {result}, 현재 모드: {armature.mode}")
if DEBUG:
all_bones = [b.name for b in armature.pose.bones]
print(f"[DEBUG] 전체 본 개수: {len(all_bones)}")
print(f"[DEBUG] 본 목록: {all_bones}")
# -------------------------
# 렌더링 루프
# -------------------------
for i in range(NUM_IMAGES):
# 매 렌더마다 아마추어 랜덤 포즈 적용
randomize_pose()
# 씬/디펜던시 그래프 강제 갱신 (배치 렌더 시 이전 프레임 캐시 문제 방지)
bpy.context.view_layer.update()
depsgraph = bpy.context.evaluated_depsgraph_get()
depsgraph.update()
# 렌더 저장 경로
scene.render.filepath = os.path.join(output_dir, f"image_{i:03d}.png")
print(f"Rendering {scene.render.filepath}")
bpy.ops.render.render(write_still=True)
if prev_mode != armature.mode:
bpy.ops.object.mode_set(mode=prev_mode)
print("Done.")