Initial commit
This commit is contained in:
91
skills/addon/commands/__init__.py
Normal file
91
skills/addon/commands/__init__.py
Normal file
@@ -0,0 +1,91 @@
|
||||
"""
|
||||
Command Handlers
|
||||
WebSocket 명령 핸들러 모듈
|
||||
"""
|
||||
|
||||
from .armature import list_armatures, get_bones
|
||||
from .retargeting import auto_map_bones, retarget_animation, get_preset_bone_mapping
|
||||
from .animation import list_animations, play_animation, stop_animation, add_to_nla
|
||||
from .import_ import import_fbx, import_dae
|
||||
from .bone_mapping import store_bone_mapping, load_bone_mapping
|
||||
from .geometry import (
|
||||
# Primitive creation
|
||||
create_cube, create_sphere, create_cylinder, create_plane,
|
||||
create_cone, create_torus,
|
||||
# Object operations
|
||||
delete_object, transform_object, duplicate_object, list_objects,
|
||||
# Vertex operations
|
||||
get_vertices, move_vertex, subdivide_mesh, extrude_face
|
||||
)
|
||||
from .modifier import (
|
||||
# Modifier operations
|
||||
add_modifier, apply_modifier, list_modifiers, remove_modifier,
|
||||
toggle_modifier, modify_modifier_properties, get_modifier_info, reorder_modifier
|
||||
)
|
||||
from .material import (
|
||||
# Material creation
|
||||
create_material, list_materials, delete_material,
|
||||
# Material assignment
|
||||
assign_material, list_object_materials,
|
||||
# Material properties
|
||||
set_material_base_color, set_material_metallic, set_material_roughness,
|
||||
set_material_emission, get_material_properties
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Armature commands
|
||||
'list_armatures',
|
||||
'get_bones',
|
||||
# Retargeting commands
|
||||
'auto_map_bones',
|
||||
'retarget_animation',
|
||||
'get_preset_bone_mapping',
|
||||
# Animation commands
|
||||
'list_animations',
|
||||
'play_animation',
|
||||
'stop_animation',
|
||||
'add_to_nla',
|
||||
# Import commands
|
||||
'import_fbx',
|
||||
'import_dae',
|
||||
# Bone mapping commands
|
||||
'store_bone_mapping',
|
||||
'load_bone_mapping',
|
||||
# Geometry - Primitive creation
|
||||
'create_cube',
|
||||
'create_sphere',
|
||||
'create_cylinder',
|
||||
'create_plane',
|
||||
'create_cone',
|
||||
'create_torus',
|
||||
# Geometry - Object operations
|
||||
'delete_object',
|
||||
'transform_object',
|
||||
'duplicate_object',
|
||||
'list_objects',
|
||||
# Geometry - Vertex operations
|
||||
'get_vertices',
|
||||
'move_vertex',
|
||||
'subdivide_mesh',
|
||||
'extrude_face',
|
||||
# Modifier operations
|
||||
'add_modifier',
|
||||
'apply_modifier',
|
||||
'list_modifiers',
|
||||
'remove_modifier',
|
||||
'toggle_modifier',
|
||||
'modify_modifier_properties',
|
||||
'get_modifier_info',
|
||||
'reorder_modifier',
|
||||
# Material operations
|
||||
'create_material',
|
||||
'list_materials',
|
||||
'delete_material',
|
||||
'assign_material',
|
||||
'list_object_materials',
|
||||
'set_material_base_color',
|
||||
'set_material_metallic',
|
||||
'set_material_roughness',
|
||||
'set_material_emission',
|
||||
'get_material_properties',
|
||||
]
|
||||
136
skills/addon/commands/animation.py
Normal file
136
skills/addon/commands/animation.py
Normal file
@@ -0,0 +1,136 @@
|
||||
"""
|
||||
Animation 관련 명령 핸들러
|
||||
애니메이션 재생, NLA 트랙 관리
|
||||
"""
|
||||
|
||||
import bpy
|
||||
from typing import List
|
||||
from ..utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def list_animations(armature_name: str) -> List[str]:
|
||||
"""
|
||||
아마추어의 애니메이션 액션 목록
|
||||
|
||||
Args:
|
||||
armature_name: 아마추어 이름
|
||||
|
||||
Returns:
|
||||
액션 이름 리스트
|
||||
|
||||
Raises:
|
||||
ValueError: 아마추어를 찾을 수 없는 경우
|
||||
"""
|
||||
logger.debug(f"Listing animations for armature: {armature_name}")
|
||||
|
||||
armature = bpy.data.objects.get(armature_name)
|
||||
if not armature:
|
||||
logger.error(f"Armature '{armature_name}' not found")
|
||||
raise ValueError(f"Armature '{armature_name}' not found")
|
||||
|
||||
actions = []
|
||||
if armature.animation_data:
|
||||
for action in bpy.data.actions:
|
||||
if action.id_root == 'OBJECT':
|
||||
actions.append(action.name)
|
||||
|
||||
logger.info(f"Found {len(actions)} animations for {armature_name}")
|
||||
return actions
|
||||
|
||||
|
||||
def play_animation(armature_name: str, action_name: str, loop: bool = True) -> str:
|
||||
"""
|
||||
애니메이션 재생
|
||||
|
||||
Args:
|
||||
armature_name: 아마추어 이름
|
||||
action_name: 액션 이름
|
||||
loop: 루프 재생 여부
|
||||
|
||||
Returns:
|
||||
결과 메시지
|
||||
|
||||
Raises:
|
||||
ValueError: 아마추어 또는 액션을 찾을 수 없는 경우
|
||||
"""
|
||||
logger.info(f"Playing animation: {action_name} on {armature_name}")
|
||||
|
||||
armature = bpy.data.objects.get(armature_name)
|
||||
if not armature:
|
||||
logger.error(f"Armature '{armature_name}' not found")
|
||||
raise ValueError(f"Armature '{armature_name}' not found")
|
||||
|
||||
action = bpy.data.actions.get(action_name)
|
||||
if not action:
|
||||
logger.error(f"Action '{action_name}' not found")
|
||||
raise ValueError(f"Action '{action_name}' not found")
|
||||
|
||||
if not armature.animation_data:
|
||||
armature.animation_data_create()
|
||||
|
||||
armature.animation_data.action = action
|
||||
bpy.context.scene.frame_set(int(action.frame_range[0]))
|
||||
bpy.ops.screen.animation_play()
|
||||
|
||||
logger.info(f"Started playing {action_name}")
|
||||
return f"Playing {action_name}"
|
||||
|
||||
|
||||
def stop_animation() -> str:
|
||||
"""
|
||||
애니메이션 중지
|
||||
|
||||
Returns:
|
||||
결과 메시지
|
||||
"""
|
||||
logger.info("Stopping animation playback")
|
||||
bpy.ops.screen.animation_cancel()
|
||||
return "Animation stopped"
|
||||
|
||||
|
||||
def add_to_nla(armature_name: str, action_name: str, track_name: str) -> str:
|
||||
"""
|
||||
NLA 트랙에 애니메이션 추가
|
||||
|
||||
Args:
|
||||
armature_name: 아마추어 이름
|
||||
action_name: 액션 이름
|
||||
track_name: 트랙 이름
|
||||
|
||||
Returns:
|
||||
결과 메시지
|
||||
|
||||
Raises:
|
||||
ValueError: 아마추어 또는 액션을 찾을 수 없는 경우
|
||||
"""
|
||||
logger.info(f"Adding {action_name} to NLA track {track_name} on {armature_name}")
|
||||
|
||||
armature = bpy.data.objects.get(armature_name)
|
||||
if not armature:
|
||||
logger.error(f"Armature '{armature_name}' not found")
|
||||
raise ValueError(f"Armature '{armature_name}' not found")
|
||||
|
||||
action = bpy.data.actions.get(action_name)
|
||||
if not action:
|
||||
logger.error(f"Action '{action_name}' not found")
|
||||
raise ValueError(f"Action '{action_name}' not found")
|
||||
|
||||
if not armature.animation_data:
|
||||
armature.animation_data_create()
|
||||
|
||||
# NLA 트랙 생성 또는 찾기
|
||||
nla_tracks = armature.animation_data.nla_tracks
|
||||
track = nla_tracks.get(track_name)
|
||||
|
||||
if not track:
|
||||
track = nla_tracks.new()
|
||||
track.name = track_name
|
||||
logger.debug(f"Created new NLA track: {track_name}")
|
||||
|
||||
# 액션을 스트립으로 추가
|
||||
strip = track.strips.new(action.name, int(action.frame_range[0]), action)
|
||||
logger.info(f"Added strip {strip.name} to track {track_name}")
|
||||
|
||||
return f"Added {action_name} to NLA track {track_name}"
|
||||
55
skills/addon/commands/armature.py
Normal file
55
skills/addon/commands/armature.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""
|
||||
Armature 관련 명령 핸들러
|
||||
아마추어 정보 조회 및 본 구조 분석
|
||||
"""
|
||||
|
||||
import bpy
|
||||
from typing import List, Dict
|
||||
from ..utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def list_armatures() -> List[str]:
|
||||
"""
|
||||
모든 아마추어 오브젝트 목록 반환
|
||||
|
||||
Returns:
|
||||
아마추어 이름 리스트
|
||||
"""
|
||||
logger.debug("Listing all armatures")
|
||||
armatures = [obj.name for obj in bpy.data.objects if obj.type == 'ARMATURE']
|
||||
logger.info(f"Found {len(armatures)} armatures")
|
||||
return armatures
|
||||
|
||||
|
||||
def get_bones(armature_name: str) -> List[Dict]:
|
||||
"""
|
||||
아마추어의 본 정보 가져오기
|
||||
|
||||
Args:
|
||||
armature_name: 아마추어 이름
|
||||
|
||||
Returns:
|
||||
본 정보 리스트 (name, parent, children)
|
||||
|
||||
Raises:
|
||||
ValueError: 아마추어를 찾을 수 없거나 타입이 잘못된 경우
|
||||
"""
|
||||
logger.debug(f"Getting bones for armature: {armature_name}")
|
||||
|
||||
armature = bpy.data.objects.get(armature_name)
|
||||
if not armature or armature.type != 'ARMATURE':
|
||||
logger.error(f"Armature '{armature_name}' not found or invalid type")
|
||||
raise ValueError(f"Armature '{armature_name}' not found")
|
||||
|
||||
bones = []
|
||||
for bone in armature.data.bones:
|
||||
bones.append({
|
||||
"name": bone.name,
|
||||
"parent": bone.parent.name if bone.parent else None,
|
||||
"children": [child.name for child in bone.children]
|
||||
})
|
||||
|
||||
logger.info(f"Retrieved {len(bones)} bones from {armature_name}")
|
||||
return bones
|
||||
90
skills/addon/commands/bone_mapping.py
Normal file
90
skills/addon/commands/bone_mapping.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""
|
||||
Bone Mapping 관련 명령 핸들러
|
||||
본 매핑 저장/로드, UI 표시
|
||||
"""
|
||||
|
||||
import bpy
|
||||
from typing import Dict
|
||||
from ..utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def store_bone_mapping(source_armature: str, target_armature: str, bone_mapping: Dict[str, str]) -> str:
|
||||
"""
|
||||
본 매핑을 Scene 속성에 저장
|
||||
|
||||
Args:
|
||||
source_armature: 소스 아마추어 이름
|
||||
target_armature: 타겟 아마추어 이름
|
||||
bone_mapping: 본 매핑 딕셔너리
|
||||
|
||||
Returns:
|
||||
결과 메시지
|
||||
"""
|
||||
logger.info(f"Storing bone mapping: {source_armature} -> {target_armature} ({len(bone_mapping)} bones)")
|
||||
|
||||
scene = bpy.context.scene
|
||||
|
||||
# 기존 매핑 클리어
|
||||
scene.bone_mapping_items.clear()
|
||||
|
||||
# 새 매핑 저장
|
||||
for source_bone, target_bone in bone_mapping.items():
|
||||
item = scene.bone_mapping_items.add()
|
||||
item.source_bone = source_bone
|
||||
item.target_bone = target_bone
|
||||
|
||||
# 아마추어 정보 저장
|
||||
scene.bone_mapping_source_armature = source_armature
|
||||
scene.bone_mapping_target_armature = target_armature
|
||||
|
||||
logger.info(f"Stored {len(bone_mapping)} bone mappings")
|
||||
print(f"✅ Stored bone mapping: {len(bone_mapping)} bones")
|
||||
return f"Bone mapping stored ({len(bone_mapping)} bones)"
|
||||
|
||||
|
||||
def load_bone_mapping(source_armature: str, target_armature: str) -> Dict[str, str]:
|
||||
"""
|
||||
Scene 속성에서 본 매핑 로드
|
||||
|
||||
Args:
|
||||
source_armature: 소스 아마추어 이름
|
||||
target_armature: 타겟 아마추어 이름
|
||||
|
||||
Returns:
|
||||
본 매핑 딕셔너리
|
||||
|
||||
Raises:
|
||||
ValueError: 저장된 매핑이 없거나 불일치하는 경우
|
||||
"""
|
||||
logger.info(f"Loading bone mapping: {source_armature} -> {target_armature}")
|
||||
|
||||
scene = bpy.context.scene
|
||||
|
||||
# 아마추어 검증
|
||||
if not scene.bone_mapping_source_armature:
|
||||
logger.error("No bone mapping stored")
|
||||
raise ValueError("No bone mapping stored. Please generate mapping first using BoneMapping.show command.")
|
||||
|
||||
if (scene.bone_mapping_source_armature != source_armature or
|
||||
scene.bone_mapping_target_armature != target_armature):
|
||||
logger.error("Stored mapping doesn't match requested armatures")
|
||||
raise ValueError(
|
||||
f"Stored mapping for ({scene.bone_mapping_source_armature} → "
|
||||
f"{scene.bone_mapping_target_armature}) doesn't match requested "
|
||||
f"({source_armature} → {target_armature})"
|
||||
)
|
||||
|
||||
# 매핑 로드
|
||||
bone_mapping = {}
|
||||
for item in scene.bone_mapping_items:
|
||||
bone_mapping[item.source_bone] = item.target_bone
|
||||
|
||||
if not bone_mapping:
|
||||
logger.error("Bone mapping is empty")
|
||||
raise ValueError("Bone mapping is empty. Please generate mapping first.")
|
||||
|
||||
logger.info(f"Loaded {len(bone_mapping)} bone mappings")
|
||||
print(f"✅ Loaded bone mapping: {len(bone_mapping)} bones")
|
||||
return bone_mapping
|
||||
88
skills/addon/commands/collection.py
Normal file
88
skills/addon/commands/collection.py
Normal file
@@ -0,0 +1,88 @@
|
||||
"""
|
||||
Collection Operations
|
||||
컬렉션 관리 명령 핸들러
|
||||
"""
|
||||
|
||||
import bpy
|
||||
from typing import Dict, List, Any
|
||||
from ..utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def create_collection(name: str) -> Dict[str, Any]:
|
||||
"""컬렉션 생성"""
|
||||
logger.info(f"Creating collection: {name}")
|
||||
|
||||
if name in bpy.data.collections:
|
||||
logger.warn(f"Collection '{name}' already exists")
|
||||
coll = bpy.data.collections[name]
|
||||
else:
|
||||
coll = bpy.data.collections.new(name)
|
||||
bpy.context.scene.collection.children.link(coll)
|
||||
|
||||
return {'name': coll.name, 'objects': len(coll.objects)}
|
||||
|
||||
|
||||
def list_collections() -> List[Dict[str, Any]]:
|
||||
"""모든 컬렉션 목록 조회"""
|
||||
logger.info("Listing all collections")
|
||||
|
||||
collections = []
|
||||
for coll in bpy.data.collections:
|
||||
collections.append({
|
||||
'name': coll.name,
|
||||
'objects': len(coll.objects),
|
||||
'children': len(coll.children)
|
||||
})
|
||||
|
||||
return collections
|
||||
|
||||
|
||||
def add_to_collection(object_name: str, collection_name: str) -> Dict[str, str]:
|
||||
"""오브젝트를 컬렉션에 추가"""
|
||||
logger.info(f"Adding '{object_name}' to collection '{collection_name}'")
|
||||
|
||||
obj = bpy.data.objects.get(object_name)
|
||||
if not obj:
|
||||
raise ValueError(f"Object '{object_name}' not found")
|
||||
|
||||
coll = bpy.data.collections.get(collection_name)
|
||||
if not coll:
|
||||
raise ValueError(f"Collection '{collection_name}' not found")
|
||||
|
||||
if obj.name not in coll.objects:
|
||||
coll.objects.link(obj)
|
||||
|
||||
return {'status': 'success', 'message': f"Added '{object_name}' to '{collection_name}'"}
|
||||
|
||||
|
||||
def remove_from_collection(object_name: str, collection_name: str) -> Dict[str, str]:
|
||||
"""오브젝트를 컬렉션에서 제거"""
|
||||
logger.info(f"Removing '{object_name}' from collection '{collection_name}'")
|
||||
|
||||
obj = bpy.data.objects.get(object_name)
|
||||
if not obj:
|
||||
raise ValueError(f"Object '{object_name}' not found")
|
||||
|
||||
coll = bpy.data.collections.get(collection_name)
|
||||
if not coll:
|
||||
raise ValueError(f"Collection '{collection_name}' not found")
|
||||
|
||||
if obj.name in coll.objects:
|
||||
coll.objects.unlink(obj)
|
||||
|
||||
return {'status': 'success', 'message': f"Removed '{object_name}' from '{collection_name}'"}
|
||||
|
||||
|
||||
def delete_collection(name: str) -> Dict[str, str]:
|
||||
"""컬렉션 삭제"""
|
||||
logger.info(f"Deleting collection: {name}")
|
||||
|
||||
coll = bpy.data.collections.get(name)
|
||||
if not coll:
|
||||
raise ValueError(f"Collection '{name}' not found")
|
||||
|
||||
bpy.data.collections.remove(coll)
|
||||
|
||||
return {'status': 'success', 'message': f"Collection '{name}' deleted"}
|
||||
552
skills/addon/commands/geometry.py
Normal file
552
skills/addon/commands/geometry.py
Normal file
@@ -0,0 +1,552 @@
|
||||
"""
|
||||
Geometry Operations
|
||||
도형 생성, 수정, 삭제 등 기하학적 작업을 처리하는 명령 핸들러
|
||||
"""
|
||||
|
||||
import bpy
|
||||
import bmesh
|
||||
from typing import Dict, List, Tuple, Optional, Any
|
||||
from ..utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Primitive Creation (기본 도형 생성)
|
||||
# ============================================================================
|
||||
|
||||
def create_cube(
|
||||
location: Tuple[float, float, float] = (0, 0, 0),
|
||||
size: float = 2.0,
|
||||
name: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
큐브 생성
|
||||
|
||||
Args:
|
||||
location: 위치 (x, y, z)
|
||||
size: 크기
|
||||
name: 오브젝트 이름 (None이면 자동 생성)
|
||||
|
||||
Returns:
|
||||
생성된 오브젝트 정보
|
||||
"""
|
||||
logger.info(f"Creating cube at {location} with size {size}")
|
||||
|
||||
bpy.ops.mesh.primitive_cube_add(size=size, location=location)
|
||||
obj = bpy.context.active_object
|
||||
|
||||
if name:
|
||||
obj.name = name
|
||||
|
||||
return {
|
||||
'name': obj.name,
|
||||
'type': obj.type,
|
||||
'location': list(obj.location),
|
||||
'vertices': len(obj.data.vertices),
|
||||
'faces': len(obj.data.polygons)
|
||||
}
|
||||
|
||||
|
||||
def create_sphere(
|
||||
location: Tuple[float, float, float] = (0, 0, 0),
|
||||
radius: float = 1.0,
|
||||
segments: int = 32,
|
||||
ring_count: int = 16,
|
||||
name: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
구(Sphere) 생성
|
||||
|
||||
Args:
|
||||
location: 위치 (x, y, z)
|
||||
radius: 반지름
|
||||
segments: 세그먼트 수 (수평)
|
||||
ring_count: 링 수 (수직)
|
||||
name: 오브젝트 이름
|
||||
|
||||
Returns:
|
||||
생성된 오브젝트 정보
|
||||
"""
|
||||
logger.info(f"Creating sphere at {location} with radius {radius}")
|
||||
|
||||
bpy.ops.mesh.primitive_uv_sphere_add(
|
||||
radius=radius,
|
||||
segments=segments,
|
||||
ring_count=ring_count,
|
||||
location=location
|
||||
)
|
||||
obj = bpy.context.active_object
|
||||
|
||||
if name:
|
||||
obj.name = name
|
||||
|
||||
return {
|
||||
'name': obj.name,
|
||||
'type': obj.type,
|
||||
'location': list(obj.location),
|
||||
'vertices': len(obj.data.vertices),
|
||||
'faces': len(obj.data.polygons)
|
||||
}
|
||||
|
||||
|
||||
def create_cylinder(
|
||||
location: Tuple[float, float, float] = (0, 0, 0),
|
||||
radius: float = 1.0,
|
||||
depth: float = 2.0,
|
||||
vertices: int = 32,
|
||||
name: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
실린더 생성
|
||||
|
||||
Args:
|
||||
location: 위치 (x, y, z)
|
||||
radius: 반지름
|
||||
depth: 높이
|
||||
vertices: 버텍스 수
|
||||
name: 오브젝트 이름
|
||||
|
||||
Returns:
|
||||
생성된 오브젝트 정보
|
||||
"""
|
||||
logger.info(f"Creating cylinder at {location}")
|
||||
|
||||
bpy.ops.mesh.primitive_cylinder_add(
|
||||
radius=radius,
|
||||
depth=depth,
|
||||
vertices=vertices,
|
||||
location=location
|
||||
)
|
||||
obj = bpy.context.active_object
|
||||
|
||||
if name:
|
||||
obj.name = name
|
||||
|
||||
return {
|
||||
'name': obj.name,
|
||||
'type': obj.type,
|
||||
'location': list(obj.location),
|
||||
'vertices': len(obj.data.vertices),
|
||||
'faces': len(obj.data.polygons)
|
||||
}
|
||||
|
||||
|
||||
def create_plane(
|
||||
location: Tuple[float, float, float] = (0, 0, 0),
|
||||
size: float = 2.0,
|
||||
name: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
평면(Plane) 생성
|
||||
|
||||
Args:
|
||||
location: 위치 (x, y, z)
|
||||
size: 크기
|
||||
name: 오브젝트 이름
|
||||
|
||||
Returns:
|
||||
생성된 오브젝트 정보
|
||||
"""
|
||||
logger.info(f"Creating plane at {location}")
|
||||
|
||||
bpy.ops.mesh.primitive_plane_add(size=size, location=location)
|
||||
obj = bpy.context.active_object
|
||||
|
||||
if name:
|
||||
obj.name = name
|
||||
|
||||
return {
|
||||
'name': obj.name,
|
||||
'type': obj.type,
|
||||
'location': list(obj.location),
|
||||
'vertices': len(obj.data.vertices),
|
||||
'faces': len(obj.data.polygons)
|
||||
}
|
||||
|
||||
|
||||
def create_cone(
|
||||
location: Tuple[float, float, float] = (0, 0, 0),
|
||||
radius1: float = 1.0,
|
||||
depth: float = 2.0,
|
||||
vertices: int = 32,
|
||||
name: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
원뿔(Cone) 생성
|
||||
|
||||
Args:
|
||||
location: 위치 (x, y, z)
|
||||
radius1: 아래 반지름
|
||||
depth: 높이
|
||||
vertices: 버텍스 수
|
||||
name: 오브젝트 이름
|
||||
|
||||
Returns:
|
||||
생성된 오브젝트 정보
|
||||
"""
|
||||
logger.info(f"Creating cone at {location}")
|
||||
|
||||
bpy.ops.mesh.primitive_cone_add(
|
||||
radius1=radius1,
|
||||
depth=depth,
|
||||
vertices=vertices,
|
||||
location=location
|
||||
)
|
||||
obj = bpy.context.active_object
|
||||
|
||||
if name:
|
||||
obj.name = name
|
||||
|
||||
return {
|
||||
'name': obj.name,
|
||||
'type': obj.type,
|
||||
'location': list(obj.location),
|
||||
'vertices': len(obj.data.vertices),
|
||||
'faces': len(obj.data.polygons)
|
||||
}
|
||||
|
||||
|
||||
def create_torus(
|
||||
location: Tuple[float, float, float] = (0, 0, 0),
|
||||
major_radius: float = 1.0,
|
||||
minor_radius: float = 0.25,
|
||||
major_segments: int = 48,
|
||||
minor_segments: int = 12,
|
||||
name: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
토러스(Torus) 생성
|
||||
|
||||
Args:
|
||||
location: 위치 (x, y, z)
|
||||
major_radius: 주 반지름
|
||||
minor_radius: 부 반지름
|
||||
major_segments: 주 세그먼트 수
|
||||
minor_segments: 부 세그먼트 수
|
||||
name: 오브젝트 이름
|
||||
|
||||
Returns:
|
||||
생성된 오브젝트 정보
|
||||
"""
|
||||
logger.info(f"Creating torus at {location}")
|
||||
|
||||
bpy.ops.mesh.primitive_torus_add(
|
||||
major_radius=major_radius,
|
||||
minor_radius=minor_radius,
|
||||
major_segments=major_segments,
|
||||
minor_segments=minor_segments,
|
||||
location=location
|
||||
)
|
||||
obj = bpy.context.active_object
|
||||
|
||||
if name:
|
||||
obj.name = name
|
||||
|
||||
return {
|
||||
'name': obj.name,
|
||||
'type': obj.type,
|
||||
'location': list(obj.location),
|
||||
'vertices': len(obj.data.vertices),
|
||||
'faces': len(obj.data.polygons)
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Object Operations (오브젝트 작업)
|
||||
# ============================================================================
|
||||
|
||||
def delete_object(name: str) -> Dict[str, str]:
|
||||
"""
|
||||
오브젝트 삭제
|
||||
|
||||
Args:
|
||||
name: 오브젝트 이름
|
||||
|
||||
Returns:
|
||||
삭제 결과
|
||||
"""
|
||||
logger.info(f"Deleting object: {name}")
|
||||
|
||||
obj = bpy.data.objects.get(name)
|
||||
if not obj:
|
||||
raise ValueError(f"Object '{name}' not found")
|
||||
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
|
||||
return {'status': 'success', 'message': f"Object '{name}' deleted"}
|
||||
|
||||
|
||||
def transform_object(
|
||||
name: str,
|
||||
location: Optional[Tuple[float, float, float]] = None,
|
||||
rotation: Optional[Tuple[float, float, float]] = None,
|
||||
scale: Optional[Tuple[float, float, float]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
오브젝트 변형 (이동, 회전, 스케일)
|
||||
|
||||
Args:
|
||||
name: 오브젝트 이름
|
||||
location: 위치 (x, y, z)
|
||||
rotation: 회전 (x, y, z) in radians
|
||||
scale: 스케일 (x, y, z)
|
||||
|
||||
Returns:
|
||||
변형된 오브젝트 정보
|
||||
"""
|
||||
logger.info(f"Transforming object: {name}")
|
||||
|
||||
obj = bpy.data.objects.get(name)
|
||||
if not obj:
|
||||
raise ValueError(f"Object '{name}' not found")
|
||||
|
||||
if location:
|
||||
obj.location = location
|
||||
|
||||
if rotation:
|
||||
obj.rotation_euler = rotation
|
||||
|
||||
if scale:
|
||||
obj.scale = scale
|
||||
|
||||
return {
|
||||
'name': obj.name,
|
||||
'location': list(obj.location),
|
||||
'rotation': list(obj.rotation_euler),
|
||||
'scale': list(obj.scale)
|
||||
}
|
||||
|
||||
|
||||
def duplicate_object(
|
||||
name: str,
|
||||
new_name: Optional[str] = None,
|
||||
location: Optional[Tuple[float, float, float]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
오브젝트 복제
|
||||
|
||||
Args:
|
||||
name: 원본 오브젝트 이름
|
||||
new_name: 새 오브젝트 이름 (None이면 자동 생성)
|
||||
location: 새 위치 (None이면 원본 위치)
|
||||
|
||||
Returns:
|
||||
복제된 오브젝트 정보
|
||||
"""
|
||||
logger.info(f"Duplicating object: {name}")
|
||||
|
||||
obj = bpy.data.objects.get(name)
|
||||
if not obj:
|
||||
raise ValueError(f"Object '{name}' not found")
|
||||
|
||||
# 복제
|
||||
new_obj = obj.copy()
|
||||
new_obj.data = obj.data.copy()
|
||||
|
||||
if new_name:
|
||||
new_obj.name = new_name
|
||||
|
||||
if location:
|
||||
new_obj.location = location
|
||||
|
||||
# 씬에 추가
|
||||
bpy.context.collection.objects.link(new_obj)
|
||||
|
||||
return {
|
||||
'name': new_obj.name,
|
||||
'type': new_obj.type,
|
||||
'location': list(new_obj.location)
|
||||
}
|
||||
|
||||
|
||||
def list_objects(object_type: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
씬의 오브젝트 목록 조회
|
||||
|
||||
Args:
|
||||
object_type: 오브젝트 타입 필터 (None이면 전체)
|
||||
예: 'MESH', 'ARMATURE', 'CAMERA', 'LIGHT'
|
||||
|
||||
Returns:
|
||||
오브젝트 목록
|
||||
"""
|
||||
logger.info(f"Listing objects (type: {object_type or 'ALL'})")
|
||||
|
||||
objects = []
|
||||
for obj in bpy.data.objects:
|
||||
if object_type and obj.type != object_type:
|
||||
continue
|
||||
|
||||
objects.append({
|
||||
'name': obj.name,
|
||||
'type': obj.type,
|
||||
'location': list(obj.location),
|
||||
'rotation': list(obj.rotation_euler),
|
||||
'scale': list(obj.scale)
|
||||
})
|
||||
|
||||
return objects
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Vertex Operations (버텍스 작업)
|
||||
# ============================================================================
|
||||
|
||||
def get_vertices(name: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
오브젝트의 버텍스 정보 조회
|
||||
|
||||
Args:
|
||||
name: 오브젝트 이름
|
||||
|
||||
Returns:
|
||||
버텍스 목록
|
||||
"""
|
||||
logger.info(f"Getting vertices for object: {name}")
|
||||
|
||||
obj = bpy.data.objects.get(name)
|
||||
if not obj or obj.type != 'MESH':
|
||||
raise ValueError(f"Mesh object '{name}' not found")
|
||||
|
||||
vertices = []
|
||||
for i, vert in enumerate(obj.data.vertices):
|
||||
vertices.append({
|
||||
'index': i,
|
||||
'co': list(vert.co),
|
||||
'normal': list(vert.normal)
|
||||
})
|
||||
|
||||
return vertices
|
||||
|
||||
|
||||
def move_vertex(
|
||||
object_name: str,
|
||||
vertex_index: int,
|
||||
new_position: Tuple[float, float, float]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
버텍스 이동
|
||||
|
||||
Args:
|
||||
object_name: 오브젝트 이름
|
||||
vertex_index: 버텍스 인덱스
|
||||
new_position: 새 위치 (x, y, z)
|
||||
|
||||
Returns:
|
||||
수정된 버텍스 정보
|
||||
"""
|
||||
logger.info(f"Moving vertex {vertex_index} in object {object_name}")
|
||||
|
||||
obj = bpy.data.objects.get(object_name)
|
||||
if not obj or obj.type != 'MESH':
|
||||
raise ValueError(f"Mesh object '{object_name}' not found")
|
||||
|
||||
mesh = obj.data
|
||||
if vertex_index >= len(mesh.vertices):
|
||||
raise ValueError(f"Vertex index {vertex_index} out of range")
|
||||
|
||||
mesh.vertices[vertex_index].co = new_position
|
||||
mesh.update()
|
||||
|
||||
return {
|
||||
'object': object_name,
|
||||
'vertex_index': vertex_index,
|
||||
'position': list(mesh.vertices[vertex_index].co)
|
||||
}
|
||||
|
||||
|
||||
def subdivide_mesh(
|
||||
name: str,
|
||||
cuts: int = 1
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
메쉬 세분화 (Subdivide)
|
||||
|
||||
Args:
|
||||
name: 오브젝트 이름
|
||||
cuts: 세분화 횟수
|
||||
|
||||
Returns:
|
||||
세분화된 메쉬 정보
|
||||
"""
|
||||
logger.info(f"Subdividing mesh: {name} (cuts: {cuts})")
|
||||
|
||||
obj = bpy.data.objects.get(name)
|
||||
if not obj or obj.type != 'MESH':
|
||||
raise ValueError(f"Mesh object '{name}' not found")
|
||||
|
||||
# Edit 모드로 전환
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
|
||||
# 모든 에지 선택
|
||||
bpy.ops.mesh.select_all(action='SELECT')
|
||||
|
||||
# 세분화
|
||||
bpy.ops.mesh.subdivide(number_cuts=cuts)
|
||||
|
||||
# Object 모드로 복귀
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
return {
|
||||
'name': obj.name,
|
||||
'vertices': len(obj.data.vertices),
|
||||
'edges': len(obj.data.edges),
|
||||
'faces': len(obj.data.polygons)
|
||||
}
|
||||
|
||||
|
||||
def extrude_face(
|
||||
object_name: str,
|
||||
face_index: int,
|
||||
offset: float = 1.0
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
페이스 돌출 (Extrude)
|
||||
|
||||
Args:
|
||||
object_name: 오브젝트 이름
|
||||
face_index: 페이스 인덱스
|
||||
offset: 돌출 거리
|
||||
|
||||
Returns:
|
||||
돌출 결과 정보
|
||||
"""
|
||||
logger.info(f"Extruding face {face_index} in object {object_name}")
|
||||
|
||||
obj = bpy.data.objects.get(object_name)
|
||||
if not obj or obj.type != 'MESH':
|
||||
raise ValueError(f"Mesh object '{object_name}' not found")
|
||||
|
||||
# BMesh를 사용한 extrude
|
||||
mesh = obj.data
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
|
||||
# 페이스 선택
|
||||
if face_index >= len(bm.faces):
|
||||
bm.free()
|
||||
raise ValueError(f"Face index {face_index} out of range")
|
||||
|
||||
face = bm.faces[face_index]
|
||||
|
||||
# Extrude
|
||||
ret = bmesh.ops.extrude_face_region(bm, geom=[face])
|
||||
extruded_verts = [v for v in ret['geom'] if isinstance(v, bmesh.types.BMVert)]
|
||||
|
||||
# 오프셋 적용
|
||||
for v in extruded_verts:
|
||||
v.co += face.normal * offset
|
||||
|
||||
# 메쉬 업데이트
|
||||
bm.to_mesh(mesh)
|
||||
bm.free()
|
||||
mesh.update()
|
||||
|
||||
return {
|
||||
'object': object_name,
|
||||
'face_index': face_index,
|
||||
'vertices': len(mesh.vertices),
|
||||
'faces': len(mesh.polygons)
|
||||
}
|
||||
79
skills/addon/commands/import_.py
Normal file
79
skills/addon/commands/import_.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""
|
||||
Import 관련 명령 핸들러
|
||||
FBX, DAE 파일 임포트
|
||||
"""
|
||||
|
||||
import bpy
|
||||
import os
|
||||
from ..utils.logger import get_logger
|
||||
from ..utils.security import validate_file_path
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def import_fbx(filepath: str) -> str:
|
||||
"""
|
||||
FBX 파일 임포트
|
||||
|
||||
Args:
|
||||
filepath: FBX 파일 경로
|
||||
|
||||
Returns:
|
||||
결과 메시지
|
||||
|
||||
Raises:
|
||||
RuntimeError: 임포트 실패
|
||||
ValueError: 잘못된 파일 경로
|
||||
"""
|
||||
logger.info(f"Importing FBX file: {filepath}")
|
||||
|
||||
# 경로 보안 검증 (path traversal 방지)
|
||||
try:
|
||||
# 사용자 홈 디렉토리 또는 현재 작업 디렉토리 내로 제한
|
||||
allowed_root = os.path.expanduser("~")
|
||||
validated_path = validate_file_path(filepath, allowed_root)
|
||||
except ValueError as e:
|
||||
logger.error(f"Invalid file path: {e}")
|
||||
raise ValueError(f"Invalid file path: {e}")
|
||||
|
||||
try:
|
||||
bpy.ops.import_scene.fbx(filepath=validated_path)
|
||||
logger.info(f"FBX import successful: {validated_path}")
|
||||
return f"Imported {validated_path}"
|
||||
except Exception as e:
|
||||
logger.error(f"FBX import failed: {e}", exc_info=True)
|
||||
raise RuntimeError(f"Failed to import FBX: {str(e)}")
|
||||
|
||||
|
||||
def import_dae(filepath: str) -> str:
|
||||
"""
|
||||
DAE (Collada) 파일 임포트
|
||||
|
||||
Args:
|
||||
filepath: DAE 파일 경로
|
||||
|
||||
Returns:
|
||||
결과 메시지
|
||||
|
||||
Raises:
|
||||
RuntimeError: 임포트 실패
|
||||
ValueError: 잘못된 파일 경로
|
||||
"""
|
||||
logger.info(f"Importing DAE file: {filepath}")
|
||||
|
||||
# 경로 보안 검증 (path traversal 방지)
|
||||
try:
|
||||
# 사용자 홈 디렉토리 또는 현재 작업 디렉토리 내로 제한
|
||||
allowed_root = os.path.expanduser("~")
|
||||
validated_path = validate_file_path(filepath, allowed_root)
|
||||
except ValueError as e:
|
||||
logger.error(f"Invalid file path: {e}")
|
||||
raise ValueError(f"Invalid file path: {e}")
|
||||
|
||||
try:
|
||||
bpy.ops.wm.collada_import(filepath=validated_path)
|
||||
logger.info(f"DAE import successful: {validated_path}")
|
||||
return f"Imported {validated_path}"
|
||||
except Exception as e:
|
||||
logger.error(f"DAE import failed: {e}", exc_info=True)
|
||||
raise RuntimeError(f"Failed to import DAE: {str(e)}")
|
||||
361
skills/addon/commands/material.py
Normal file
361
skills/addon/commands/material.py
Normal file
@@ -0,0 +1,361 @@
|
||||
"""
|
||||
Material Operations
|
||||
머티리얼 및 셰이더 관련 작업을 처리하는 명령 핸들러
|
||||
"""
|
||||
|
||||
import bpy
|
||||
from typing import Dict, List, Tuple, Optional, Any
|
||||
from ..utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Material Creation (머티리얼 생성)
|
||||
# ============================================================================
|
||||
|
||||
def create_material(
|
||||
name: str,
|
||||
use_nodes: bool = True
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
머티리얼 생성
|
||||
|
||||
Args:
|
||||
name: 머티리얼 이름
|
||||
use_nodes: 노드 시스템 사용 여부 (기본값: True)
|
||||
|
||||
Returns:
|
||||
생성된 머티리얼 정보
|
||||
"""
|
||||
logger.info(f"Creating material: {name}")
|
||||
|
||||
# 기존 머티리얼 확인
|
||||
if name in bpy.data.materials:
|
||||
logger.warn(f"Material '{name}' already exists, returning existing")
|
||||
mat = bpy.data.materials[name]
|
||||
else:
|
||||
mat = bpy.data.materials.new(name=name)
|
||||
mat.use_nodes = use_nodes
|
||||
|
||||
return {
|
||||
'name': mat.name,
|
||||
'use_nodes': mat.use_nodes
|
||||
}
|
||||
|
||||
|
||||
def list_materials() -> List[Dict[str, Any]]:
|
||||
"""
|
||||
모든 머티리얼 목록 조회
|
||||
|
||||
Returns:
|
||||
머티리얼 목록
|
||||
"""
|
||||
logger.info("Listing all materials")
|
||||
|
||||
materials = []
|
||||
for mat in bpy.data.materials:
|
||||
materials.append({
|
||||
'name': mat.name,
|
||||
'use_nodes': mat.use_nodes,
|
||||
'users': mat.users # 사용 중인 오브젝트 수
|
||||
})
|
||||
|
||||
return materials
|
||||
|
||||
|
||||
def delete_material(name: str) -> Dict[str, str]:
|
||||
"""
|
||||
머티리얼 삭제
|
||||
|
||||
Args:
|
||||
name: 머티리얼 이름
|
||||
|
||||
Returns:
|
||||
삭제 결과
|
||||
"""
|
||||
logger.info(f"Deleting material: {name}")
|
||||
|
||||
mat = bpy.data.materials.get(name)
|
||||
if not mat:
|
||||
raise ValueError(f"Material '{name}' not found")
|
||||
|
||||
bpy.data.materials.remove(mat)
|
||||
|
||||
return {'status': 'success', 'message': f"Material '{name}' deleted"}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Material Assignment (머티리얼 할당)
|
||||
# ============================================================================
|
||||
|
||||
def assign_material(
|
||||
object_name: str,
|
||||
material_name: str,
|
||||
slot_index: int = 0
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
오브젝트에 머티리얼 할당
|
||||
|
||||
Args:
|
||||
object_name: 오브젝트 이름
|
||||
material_name: 머티리얼 이름
|
||||
slot_index: 머티리얼 슬롯 인덱스 (기본값: 0)
|
||||
|
||||
Returns:
|
||||
할당 결과
|
||||
"""
|
||||
logger.info(f"Assigning material '{material_name}' to object '{object_name}'")
|
||||
|
||||
obj = bpy.data.objects.get(object_name)
|
||||
if not obj:
|
||||
raise ValueError(f"Object '{object_name}' not found")
|
||||
|
||||
mat = bpy.data.materials.get(material_name)
|
||||
if not mat:
|
||||
raise ValueError(f"Material '{material_name}' not found")
|
||||
|
||||
# 머티리얼 슬롯이 없으면 생성
|
||||
if len(obj.data.materials) == 0:
|
||||
obj.data.materials.append(mat)
|
||||
else:
|
||||
# 기존 슬롯에 할당
|
||||
if slot_index < len(obj.data.materials):
|
||||
obj.data.materials[slot_index] = mat
|
||||
else:
|
||||
obj.data.materials.append(mat)
|
||||
|
||||
return {
|
||||
'object': object_name,
|
||||
'material': material_name,
|
||||
'slot_index': slot_index
|
||||
}
|
||||
|
||||
|
||||
def list_object_materials(object_name: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
오브젝트의 머티리얼 슬롯 목록 조회
|
||||
|
||||
Args:
|
||||
object_name: 오브젝트 이름
|
||||
|
||||
Returns:
|
||||
머티리얼 슬롯 목록
|
||||
"""
|
||||
logger.info(f"Listing materials for object: {object_name}")
|
||||
|
||||
obj = bpy.data.objects.get(object_name)
|
||||
if not obj:
|
||||
raise ValueError(f"Object '{object_name}' not found")
|
||||
|
||||
materials = []
|
||||
for i, mat_slot in enumerate(obj.material_slots):
|
||||
materials.append({
|
||||
'slot_index': i,
|
||||
'material': mat_slot.material.name if mat_slot.material else None
|
||||
})
|
||||
|
||||
return materials
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Material Properties (머티리얼 속성)
|
||||
# ============================================================================
|
||||
|
||||
def set_material_base_color(
|
||||
material_name: str,
|
||||
color: Tuple[float, float, float, float]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
머티리얼 기본 색상 설정 (Principled BSDF)
|
||||
|
||||
Args:
|
||||
material_name: 머티리얼 이름
|
||||
color: RGBA 색상 (0.0 ~ 1.0)
|
||||
|
||||
Returns:
|
||||
설정 결과
|
||||
"""
|
||||
logger.info(f"Setting base color for material: {material_name}")
|
||||
|
||||
mat = bpy.data.materials.get(material_name)
|
||||
if not mat:
|
||||
raise ValueError(f"Material '{material_name}' not found")
|
||||
|
||||
if not mat.use_nodes:
|
||||
raise ValueError(f"Material '{material_name}' does not use nodes")
|
||||
|
||||
# Principled BSDF 노드 찾기
|
||||
principled = None
|
||||
for node in mat.node_tree.nodes:
|
||||
if node.type == 'BSDF_PRINCIPLED':
|
||||
principled = node
|
||||
break
|
||||
|
||||
if not principled:
|
||||
raise ValueError(f"Principled BSDF node not found in material '{material_name}'")
|
||||
|
||||
# Base Color 설정
|
||||
principled.inputs['Base Color'].default_value = color
|
||||
|
||||
return {
|
||||
'material': material_name,
|
||||
'base_color': list(color)
|
||||
}
|
||||
|
||||
|
||||
def set_material_metallic(
|
||||
material_name: str,
|
||||
metallic: float
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
머티리얼 Metallic 값 설정
|
||||
|
||||
Args:
|
||||
material_name: 머티리얼 이름
|
||||
metallic: Metallic 값 (0.0 ~ 1.0)
|
||||
|
||||
Returns:
|
||||
설정 결과
|
||||
"""
|
||||
logger.info(f"Setting metallic for material: {material_name}")
|
||||
|
||||
mat = bpy.data.materials.get(material_name)
|
||||
if not mat or not mat.use_nodes:
|
||||
raise ValueError(f"Material '{material_name}' not found or does not use nodes")
|
||||
|
||||
# Principled BSDF 노드 찾기
|
||||
principled = None
|
||||
for node in mat.node_tree.nodes:
|
||||
if node.type == 'BSDF_PRINCIPLED':
|
||||
principled = node
|
||||
break
|
||||
|
||||
if not principled:
|
||||
raise ValueError(f"Principled BSDF node not found")
|
||||
|
||||
principled.inputs['Metallic'].default_value = metallic
|
||||
|
||||
return {
|
||||
'material': material_name,
|
||||
'metallic': metallic
|
||||
}
|
||||
|
||||
|
||||
def set_material_roughness(
|
||||
material_name: str,
|
||||
roughness: float
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
머티리얼 Roughness 값 설정
|
||||
|
||||
Args:
|
||||
material_name: 머티리얼 이름
|
||||
roughness: Roughness 값 (0.0 ~ 1.0)
|
||||
|
||||
Returns:
|
||||
설정 결과
|
||||
"""
|
||||
logger.info(f"Setting roughness for material: {material_name}")
|
||||
|
||||
mat = bpy.data.materials.get(material_name)
|
||||
if not mat or not mat.use_nodes:
|
||||
raise ValueError(f"Material '{material_name}' not found or does not use nodes")
|
||||
|
||||
# Principled BSDF 노드 찾기
|
||||
principled = None
|
||||
for node in mat.node_tree.nodes:
|
||||
if node.type == 'BSDF_PRINCIPLED':
|
||||
principled = node
|
||||
break
|
||||
|
||||
if not principled:
|
||||
raise ValueError(f"Principled BSDF node not found")
|
||||
|
||||
principled.inputs['Roughness'].default_value = roughness
|
||||
|
||||
return {
|
||||
'material': material_name,
|
||||
'roughness': roughness
|
||||
}
|
||||
|
||||
|
||||
def set_material_emission(
|
||||
material_name: str,
|
||||
color: Tuple[float, float, float, float],
|
||||
strength: float = 1.0
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
머티리얼 Emission 설정
|
||||
|
||||
Args:
|
||||
material_name: 머티리얼 이름
|
||||
color: Emission 색상 RGBA (0.0 ~ 1.0)
|
||||
strength: Emission 강도 (기본값: 1.0)
|
||||
|
||||
Returns:
|
||||
설정 결과
|
||||
"""
|
||||
logger.info(f"Setting emission for material: {material_name}")
|
||||
|
||||
mat = bpy.data.materials.get(material_name)
|
||||
if not mat or not mat.use_nodes:
|
||||
raise ValueError(f"Material '{material_name}' not found or does not use nodes")
|
||||
|
||||
# Principled BSDF 노드 찾기
|
||||
principled = None
|
||||
for node in mat.node_tree.nodes:
|
||||
if node.type == 'BSDF_PRINCIPLED':
|
||||
principled = node
|
||||
break
|
||||
|
||||
if not principled:
|
||||
raise ValueError(f"Principled BSDF node not found")
|
||||
|
||||
principled.inputs['Emission'].default_value = color
|
||||
principled.inputs['Emission Strength'].default_value = strength
|
||||
|
||||
return {
|
||||
'material': material_name,
|
||||
'emission_color': list(color),
|
||||
'emission_strength': strength
|
||||
}
|
||||
|
||||
|
||||
def get_material_properties(material_name: str) -> Dict[str, Any]:
|
||||
"""
|
||||
머티리얼 속성 조회
|
||||
|
||||
Args:
|
||||
material_name: 머티리얼 이름
|
||||
|
||||
Returns:
|
||||
머티리얼 속성
|
||||
"""
|
||||
logger.info(f"Getting properties for material: {material_name}")
|
||||
|
||||
mat = bpy.data.materials.get(material_name)
|
||||
if not mat:
|
||||
raise ValueError(f"Material '{material_name}' not found")
|
||||
|
||||
props = {
|
||||
'name': mat.name,
|
||||
'use_nodes': mat.use_nodes
|
||||
}
|
||||
|
||||
if mat.use_nodes:
|
||||
# Principled BSDF 속성 가져오기
|
||||
principled = None
|
||||
for node in mat.node_tree.nodes:
|
||||
if node.type == 'BSDF_PRINCIPLED':
|
||||
principled = node
|
||||
break
|
||||
|
||||
if principled:
|
||||
props['base_color'] = list(principled.inputs['Base Color'].default_value)
|
||||
props['metallic'] = principled.inputs['Metallic'].default_value
|
||||
props['roughness'] = principled.inputs['Roughness'].default_value
|
||||
props['emission'] = list(principled.inputs['Emission'].default_value)
|
||||
props['emission_strength'] = principled.inputs['Emission Strength'].default_value
|
||||
|
||||
return props
|
||||
388
skills/addon/commands/modifier.py
Normal file
388
skills/addon/commands/modifier.py
Normal file
@@ -0,0 +1,388 @@
|
||||
"""
|
||||
Modifier Operations
|
||||
모디파이어 관리 명령 핸들러
|
||||
"""
|
||||
|
||||
import bpy
|
||||
from typing import Dict, List, Any, Optional
|
||||
from ..utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def add_modifier(object_name: str, modifier_type: str, name: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""오브젝트에 모디파이어 추가
|
||||
|
||||
Args:
|
||||
object_name: 대상 오브젝트 이름
|
||||
modifier_type: 모디파이어 타입 (SUBSURF, MIRROR, ARRAY, BEVEL, etc.)
|
||||
name: 모디파이어 이름 (optional)
|
||||
|
||||
Supported modifier types:
|
||||
- SUBSURF: Subdivision Surface
|
||||
- MIRROR: Mirror
|
||||
- ARRAY: Array
|
||||
- BEVEL: Bevel
|
||||
- BOOLEAN: Boolean
|
||||
- SOLIDIFY: Solidify
|
||||
- WIREFRAME: Wireframe
|
||||
- SKIN: Skin
|
||||
- ARMATURE: Armature
|
||||
- LATTICE: Lattice
|
||||
- CURVE: Curve
|
||||
- SIMPLE_DEFORM: Simple Deform
|
||||
- CAST: Cast
|
||||
- DISPLACE: Displace
|
||||
- HOOK: Hook
|
||||
- LAPLACIANDEFORM: Laplacian Deform
|
||||
- MESH_DEFORM: Mesh Deform
|
||||
- SHRINKWRAP: Shrinkwrap
|
||||
- WAVE: Wave
|
||||
- OCEAN: Ocean
|
||||
- PARTICLE_SYSTEM: Particle System
|
||||
- CLOTH: Cloth
|
||||
- COLLISION: Collision
|
||||
- DYNAMIC_PAINT: Dynamic Paint
|
||||
- EXPLODE: Explode
|
||||
- FLUID: Fluid
|
||||
- SOFT_BODY: Soft Body
|
||||
"""
|
||||
logger.info(f"Adding modifier '{modifier_type}' to '{object_name}'")
|
||||
|
||||
obj = bpy.data.objects.get(object_name)
|
||||
if not obj:
|
||||
raise ValueError(f"Object '{object_name}' not found")
|
||||
|
||||
mod = obj.modifiers.new(name or modifier_type, modifier_type)
|
||||
|
||||
return {
|
||||
'name': mod.name,
|
||||
'type': mod.type,
|
||||
'show_viewport': mod.show_viewport,
|
||||
'show_render': mod.show_render
|
||||
}
|
||||
|
||||
|
||||
def apply_modifier(object_name: str, modifier_name: str) -> Dict[str, str]:
|
||||
"""모디파이어 적용"""
|
||||
logger.info(f"Applying modifier '{modifier_name}' on '{object_name}'")
|
||||
|
||||
obj = bpy.data.objects.get(object_name)
|
||||
if not obj:
|
||||
raise ValueError(f"Object '{object_name}' not found")
|
||||
|
||||
mod = obj.modifiers.get(modifier_name)
|
||||
if not mod:
|
||||
raise ValueError(f"Modifier '{modifier_name}' not found on '{object_name}'")
|
||||
|
||||
# 모디파이어 적용은 Edit 모드에서는 할 수 없음
|
||||
if bpy.context.mode != 'OBJECT':
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
# 오브젝트 선택 및 활성화
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
obj.select_set(True)
|
||||
|
||||
# 모디파이어 적용
|
||||
bpy.ops.object.modifier_apply(modifier=modifier_name)
|
||||
|
||||
return {'status': 'success', 'message': f"Applied modifier '{modifier_name}' to '{object_name}'"}
|
||||
|
||||
|
||||
def list_modifiers(object_name: str) -> List[Dict[str, Any]]:
|
||||
"""오브젝트의 모디파이어 목록 조회"""
|
||||
logger.info(f"Listing modifiers for '{object_name}'")
|
||||
|
||||
obj = bpy.data.objects.get(object_name)
|
||||
if not obj:
|
||||
raise ValueError(f"Object '{object_name}' not found")
|
||||
|
||||
modifiers = []
|
||||
for mod in obj.modifiers:
|
||||
mod_info = {
|
||||
'name': mod.name,
|
||||
'type': mod.type,
|
||||
'show_viewport': mod.show_viewport,
|
||||
'show_render': mod.show_render,
|
||||
}
|
||||
|
||||
# 타입별 특화 속성 추가
|
||||
if mod.type == 'SUBSURF':
|
||||
mod_info['levels'] = mod.levels
|
||||
mod_info['render_levels'] = mod.render_levels
|
||||
elif mod.type == 'MIRROR':
|
||||
mod_info['use_axis'] = [mod.use_axis[0], mod.use_axis[1], mod.use_axis[2]]
|
||||
mod_info['use_bisect_axis'] = [mod.use_bisect_axis[0], mod.use_bisect_axis[1], mod.use_bisect_axis[2]]
|
||||
elif mod.type == 'ARRAY':
|
||||
mod_info['count'] = mod.count
|
||||
mod_info['use_relative_offset'] = mod.use_relative_offset
|
||||
mod_info['relative_offset_displace'] = list(mod.relative_offset_displace)
|
||||
elif mod.type == 'BEVEL':
|
||||
mod_info['width'] = mod.width
|
||||
mod_info['segments'] = mod.segments
|
||||
mod_info['limit_method'] = mod.limit_method
|
||||
elif mod.type == 'BOOLEAN':
|
||||
mod_info['operation'] = mod.operation
|
||||
mod_info['object'] = mod.object.name if mod.object else None
|
||||
elif mod.type == 'SOLIDIFY':
|
||||
mod_info['thickness'] = mod.thickness
|
||||
mod_info['offset'] = mod.offset
|
||||
elif mod.type == 'ARMATURE':
|
||||
mod_info['object'] = mod.object.name if mod.object else None
|
||||
mod_info['use_vertex_groups'] = mod.use_vertex_groups
|
||||
elif mod.type == 'LATTICE':
|
||||
mod_info['object'] = mod.object.name if mod.object else None
|
||||
elif mod.type == 'CURVE':
|
||||
mod_info['object'] = mod.object.name if mod.object else None
|
||||
elif mod.type == 'SIMPLE_DEFORM':
|
||||
mod_info['deform_method'] = mod.deform_method
|
||||
mod_info['factor'] = mod.factor
|
||||
elif mod.type == 'CAST':
|
||||
mod_info['cast_type'] = mod.cast_type
|
||||
mod_info['factor'] = mod.factor
|
||||
elif mod.type == 'DISPLACE':
|
||||
mod_info['strength'] = mod.strength
|
||||
mod_info['direction'] = mod.direction
|
||||
elif mod.type == 'WAVE':
|
||||
mod_info['time_offset'] = mod.time_offset
|
||||
mod_info['height'] = mod.height
|
||||
|
||||
modifiers.append(mod_info)
|
||||
|
||||
return modifiers
|
||||
|
||||
|
||||
def remove_modifier(object_name: str, modifier_name: str) -> Dict[str, str]:
|
||||
"""모디파이어 제거"""
|
||||
logger.info(f"Removing modifier '{modifier_name}' from '{object_name}'")
|
||||
|
||||
obj = bpy.data.objects.get(object_name)
|
||||
if not obj:
|
||||
raise ValueError(f"Object '{object_name}' not found")
|
||||
|
||||
mod = obj.modifiers.get(modifier_name)
|
||||
if not mod:
|
||||
raise ValueError(f"Modifier '{modifier_name}' not found on '{object_name}'")
|
||||
|
||||
obj.modifiers.remove(mod)
|
||||
|
||||
return {'status': 'success', 'message': f"Removed modifier '{modifier_name}' from '{object_name}'"}
|
||||
|
||||
|
||||
def toggle_modifier(object_name: str, modifier_name: str,
|
||||
viewport: Optional[bool] = None,
|
||||
render: Optional[bool] = None) -> Dict[str, Any]:
|
||||
"""모디파이어 활성화/비활성화
|
||||
|
||||
Args:
|
||||
object_name: 대상 오브젝트 이름
|
||||
modifier_name: 모디파이어 이름
|
||||
viewport: 뷰포트 표시 on/off (None이면 토글)
|
||||
render: 렌더 표시 on/off (None이면 토글)
|
||||
"""
|
||||
logger.info(f"Toggling modifier '{modifier_name}' on '{object_name}'")
|
||||
|
||||
obj = bpy.data.objects.get(object_name)
|
||||
if not obj:
|
||||
raise ValueError(f"Object '{object_name}' not found")
|
||||
|
||||
mod = obj.modifiers.get(modifier_name)
|
||||
if not mod:
|
||||
raise ValueError(f"Modifier '{modifier_name}' not found on '{object_name}'")
|
||||
|
||||
if viewport is not None:
|
||||
mod.show_viewport = viewport
|
||||
else:
|
||||
mod.show_viewport = not mod.show_viewport
|
||||
|
||||
if render is not None:
|
||||
mod.show_render = render
|
||||
else:
|
||||
mod.show_render = not mod.show_render
|
||||
|
||||
return {
|
||||
'name': mod.name,
|
||||
'show_viewport': mod.show_viewport,
|
||||
'show_render': mod.show_render
|
||||
}
|
||||
|
||||
|
||||
def modify_modifier_properties(object_name: str, modifier_name: str, **properties) -> Dict[str, Any]:
|
||||
"""모디파이어 속성 수정
|
||||
|
||||
Args:
|
||||
object_name: 대상 오브젝트 이름
|
||||
modifier_name: 모디파이어 이름
|
||||
**properties: 수정할 속성들 (key=value 형태)
|
||||
|
||||
Example properties by modifier type:
|
||||
SUBSURF: levels, render_levels
|
||||
MIRROR: use_axis, use_bisect_axis, mirror_object
|
||||
ARRAY: count, relative_offset_displace
|
||||
BEVEL: width, segments, limit_method
|
||||
BOOLEAN: operation, object
|
||||
SOLIDIFY: thickness, offset
|
||||
ARMATURE: object, use_vertex_groups
|
||||
SIMPLE_DEFORM: deform_method, factor, angle
|
||||
CAST: cast_type, factor, radius
|
||||
DISPLACE: strength, direction
|
||||
"""
|
||||
logger.info(f"Modifying properties of '{modifier_name}' on '{object_name}'")
|
||||
|
||||
obj = bpy.data.objects.get(object_name)
|
||||
if not obj:
|
||||
raise ValueError(f"Object '{object_name}' not found")
|
||||
|
||||
mod = obj.modifiers.get(modifier_name)
|
||||
if not mod:
|
||||
raise ValueError(f"Modifier '{modifier_name}' not found on '{object_name}'")
|
||||
|
||||
updated_properties = {}
|
||||
for key, value in properties.items():
|
||||
if hasattr(mod, key):
|
||||
# 특수 처리가 필요한 속성들
|
||||
if key in ['use_axis', 'use_bisect_axis'] and isinstance(value, list):
|
||||
# Mirror 모디파이어의 axis는 boolean 배열
|
||||
for i, v in enumerate(value):
|
||||
if i < 3:
|
||||
getattr(mod, key)[i] = v
|
||||
elif key == 'relative_offset_displace' and isinstance(value, list):
|
||||
# Array 모디파이어의 offset은 Vector
|
||||
for i, v in enumerate(value):
|
||||
if i < 3:
|
||||
mod.relative_offset_displace[i] = v
|
||||
elif key == 'object' and isinstance(value, str):
|
||||
# 오브젝트 참조는 문자열로 받아서 변환
|
||||
target_obj = bpy.data.objects.get(value)
|
||||
if target_obj:
|
||||
setattr(mod, key, target_obj)
|
||||
else:
|
||||
logger.warn(f"Target object '{value}' not found for property '{key}'")
|
||||
continue
|
||||
else:
|
||||
# 일반 속성
|
||||
setattr(mod, key, value)
|
||||
|
||||
updated_properties[key] = value
|
||||
else:
|
||||
logger.warn(f"Property '{key}' not found on modifier '{modifier_name}'")
|
||||
|
||||
return {
|
||||
'name': mod.name,
|
||||
'type': mod.type,
|
||||
'updated_properties': updated_properties
|
||||
}
|
||||
|
||||
|
||||
def get_modifier_info(object_name: str, modifier_name: str) -> Dict[str, Any]:
|
||||
"""특정 모디파이어의 상세 정보 조회"""
|
||||
logger.info(f"Getting info for modifier '{modifier_name}' on '{object_name}'")
|
||||
|
||||
obj = bpy.data.objects.get(object_name)
|
||||
if not obj:
|
||||
raise ValueError(f"Object '{object_name}' not found")
|
||||
|
||||
mod = obj.modifiers.get(modifier_name)
|
||||
if not mod:
|
||||
raise ValueError(f"Modifier '{modifier_name}' not found on '{object_name}'")
|
||||
|
||||
# 모든 읽기 가능한 속성을 추출
|
||||
info = {
|
||||
'name': mod.name,
|
||||
'type': mod.type,
|
||||
'show_viewport': mod.show_viewport,
|
||||
'show_render': mod.show_render,
|
||||
}
|
||||
|
||||
# 타입별 모든 관련 속성 추가
|
||||
if mod.type == 'SUBSURF':
|
||||
info.update({
|
||||
'levels': mod.levels,
|
||||
'render_levels': mod.render_levels,
|
||||
'subdivision_type': mod.subdivision_type,
|
||||
'use_limit_surface': mod.use_limit_surface
|
||||
})
|
||||
elif mod.type == 'MIRROR':
|
||||
info.update({
|
||||
'use_axis': [mod.use_axis[0], mod.use_axis[1], mod.use_axis[2]],
|
||||
'use_bisect_axis': [mod.use_bisect_axis[0], mod.use_bisect_axis[1], mod.use_bisect_axis[2]],
|
||||
'use_bisect_flip_axis': [mod.use_bisect_flip_axis[0], mod.use_bisect_flip_axis[1], mod.use_bisect_flip_axis[2]],
|
||||
'mirror_object': mod.mirror_object.name if mod.mirror_object else None,
|
||||
'use_clip': mod.use_clip,
|
||||
'use_mirror_merge': mod.use_mirror_merge,
|
||||
'merge_threshold': mod.merge_threshold
|
||||
})
|
||||
elif mod.type == 'ARRAY':
|
||||
info.update({
|
||||
'count': mod.count,
|
||||
'use_constant_offset': mod.use_constant_offset,
|
||||
'use_relative_offset': mod.use_relative_offset,
|
||||
'use_object_offset': mod.use_object_offset,
|
||||
'constant_offset_displace': list(mod.constant_offset_displace),
|
||||
'relative_offset_displace': list(mod.relative_offset_displace),
|
||||
'offset_object': mod.offset_object.name if mod.offset_object else None
|
||||
})
|
||||
elif mod.type == 'BEVEL':
|
||||
info.update({
|
||||
'width': mod.width,
|
||||
'segments': mod.segments,
|
||||
'limit_method': mod.limit_method,
|
||||
'offset_type': mod.offset_type,
|
||||
'profile': mod.profile,
|
||||
'material': mod.material
|
||||
})
|
||||
elif mod.type == 'BOOLEAN':
|
||||
info.update({
|
||||
'operation': mod.operation,
|
||||
'object': mod.object.name if mod.object else None,
|
||||
'solver': mod.solver
|
||||
})
|
||||
elif mod.type == 'SOLIDIFY':
|
||||
info.update({
|
||||
'thickness': mod.thickness,
|
||||
'offset': mod.offset,
|
||||
'use_rim': mod.use_rim,
|
||||
'use_even_offset': mod.use_even_offset,
|
||||
'material_offset': mod.material_offset
|
||||
})
|
||||
|
||||
return info
|
||||
|
||||
|
||||
def reorder_modifier(object_name: str, modifier_name: str, direction: str) -> Dict[str, Any]:
|
||||
"""모디파이어 순서 변경
|
||||
|
||||
Args:
|
||||
object_name: 대상 오브젝트 이름
|
||||
modifier_name: 모디파이어 이름
|
||||
direction: 'UP' 또는 'DOWN'
|
||||
"""
|
||||
logger.info(f"Moving modifier '{modifier_name}' {direction} on '{object_name}'")
|
||||
|
||||
obj = bpy.data.objects.get(object_name)
|
||||
if not obj:
|
||||
raise ValueError(f"Object '{object_name}' not found")
|
||||
|
||||
mod = obj.modifiers.get(modifier_name)
|
||||
if not mod:
|
||||
raise ValueError(f"Modifier '{modifier_name}' not found on '{object_name}'")
|
||||
|
||||
# 오브젝트 선택 및 활성화
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
obj.select_set(True)
|
||||
|
||||
if direction.upper() == 'UP':
|
||||
bpy.ops.object.modifier_move_up(modifier=modifier_name)
|
||||
elif direction.upper() == 'DOWN':
|
||||
bpy.ops.object.modifier_move_down(modifier=modifier_name)
|
||||
else:
|
||||
raise ValueError(f"Invalid direction '{direction}'. Use 'UP' or 'DOWN'")
|
||||
|
||||
# 현재 순서 반환
|
||||
modifiers = [m.name for m in obj.modifiers]
|
||||
|
||||
return {
|
||||
'status': 'success',
|
||||
'modifier': modifier_name,
|
||||
'new_order': modifiers
|
||||
}
|
||||
260
skills/addon/commands/retargeting.py
Normal file
260
skills/addon/commands/retargeting.py
Normal file
@@ -0,0 +1,260 @@
|
||||
"""
|
||||
Animation Retargeting 관련 명령 핸들러
|
||||
본 매핑, 애니메이션 리타게팅 실행
|
||||
"""
|
||||
|
||||
import bpy
|
||||
from typing import Dict
|
||||
from ..utils.logger import get_logger
|
||||
from ..utils.bone_matching import fuzzy_match_bones, get_match_quality_report
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def auto_map_bones(source_armature: str, target_armature: str) -> Dict[str, str]:
|
||||
"""
|
||||
자동 본 매핑 (Mixamo -> 사용자 캐릭터)
|
||||
Fuzzy matching 알고리즘 사용으로 정확도 개선
|
||||
|
||||
Args:
|
||||
source_armature: 소스 아마추어 이름 (Mixamo)
|
||||
target_armature: 타겟 아마추어 이름 (사용자 캐릭터)
|
||||
|
||||
Returns:
|
||||
본 매핑 딕셔너리 {source_bone: target_bone}
|
||||
|
||||
Raises:
|
||||
ValueError: 아마추어를 찾을 수 없는 경우
|
||||
"""
|
||||
logger.info(f"Auto-mapping bones: {source_armature} -> {target_armature}")
|
||||
|
||||
source = bpy.data.objects.get(source_armature)
|
||||
target = bpy.data.objects.get(target_armature)
|
||||
|
||||
if not source or not target:
|
||||
logger.error("Source or target armature not found")
|
||||
raise ValueError("Source or target armature not found")
|
||||
|
||||
# Mixamo 표준 본 이름과 알려진 별칭 (확장: 손가락, 발가락 포함)
|
||||
mixamo_bone_aliases = {
|
||||
# 몸통 (6개)
|
||||
"Hips": ["hips", "pelvis", "root"],
|
||||
"Spine": ["spine", "spine1"],
|
||||
"Spine1": ["spine1", "spine2"],
|
||||
"Spine2": ["spine2", "spine3", "chest"],
|
||||
"Neck": ["neck"],
|
||||
"Head": ["head"],
|
||||
|
||||
# 왼쪽 팔 (4개)
|
||||
"LeftShoulder": ["shoulder.l", "clavicle.l", "leftshoulder"],
|
||||
"LeftArm": ["upper_arm.l", "leftarm", "upperarm.l"],
|
||||
"LeftForeArm": ["forearm.l", "leftforearm", "lowerarm.l"],
|
||||
"LeftHand": ["hand.l", "lefthand"],
|
||||
|
||||
# 오른쪽 팔 (4개)
|
||||
"RightShoulder": ["shoulder.r", "clavicle.r", "rightshoulder"],
|
||||
"RightArm": ["upper_arm.r", "rightarm", "upperarm.r"],
|
||||
"RightForeArm": ["forearm.r", "rightforearm", "lowerarm.r"],
|
||||
"RightHand": ["hand.r", "righthand"],
|
||||
|
||||
# 왼쪽 다리 (4개)
|
||||
"LeftUpLeg": ["thigh.l", "leftupleg", "upperleg.l"],
|
||||
"LeftLeg": ["shin.l", "leftleg", "lowerleg.l"],
|
||||
"LeftFoot": ["foot.l", "leftfoot"],
|
||||
"LeftToeBase": ["toe.l", "lefttoebase", "foot.l.001"],
|
||||
|
||||
# 오른쪽 다리 (4개)
|
||||
"RightUpLeg": ["thigh.r", "rightupleg", "upperleg.r"],
|
||||
"RightLeg": ["shin.r", "rightleg", "lowerleg.r"],
|
||||
"RightFoot": ["foot.r", "rightfoot"],
|
||||
"RightToeBase": ["toe.r", "righttoebase", "foot.r.001"],
|
||||
|
||||
# 왼쪽 손가락 (15개)
|
||||
"LeftHandThumb1": ["thumb.01.l", "lefthandthumb1", "thumb_01.l"],
|
||||
"LeftHandThumb2": ["thumb.02.l", "lefthandthumb2", "thumb_02.l"],
|
||||
"LeftHandThumb3": ["thumb.03.l", "lefthandthumb3", "thumb_03.l"],
|
||||
"LeftHandIndex1": ["f_index.01.l", "lefthandindex1", "index_01.l"],
|
||||
"LeftHandIndex2": ["f_index.02.l", "lefthandindex2", "index_02.l"],
|
||||
"LeftHandIndex3": ["f_index.03.l", "lefthandindex3", "index_03.l"],
|
||||
"LeftHandMiddle1": ["f_middle.01.l", "lefthandmiddle1", "middle_01.l"],
|
||||
"LeftHandMiddle2": ["f_middle.02.l", "lefthandmiddle2", "middle_02.l"],
|
||||
"LeftHandMiddle3": ["f_middle.03.l", "lefthandmiddle3", "middle_03.l"],
|
||||
"LeftHandRing1": ["f_ring.01.l", "lefthandring1", "ring_01.l"],
|
||||
"LeftHandRing2": ["f_ring.02.l", "lefthandring2", "ring_02.l"],
|
||||
"LeftHandRing3": ["f_ring.03.l", "lefthandring3", "ring_03.l"],
|
||||
"LeftHandPinky1": ["f_pinky.01.l", "lefthandpinky1", "pinky_01.l"],
|
||||
"LeftHandPinky2": ["f_pinky.02.l", "lefthandpinky2", "pinky_02.l"],
|
||||
"LeftHandPinky3": ["f_pinky.03.l", "lefthandpinky3", "pinky_03.l"],
|
||||
|
||||
# 오른쪽 손가락 (15개)
|
||||
"RightHandThumb1": ["thumb.01.r", "righthandthumb1", "thumb_01.r"],
|
||||
"RightHandThumb2": ["thumb.02.r", "righthandthumb2", "thumb_02.r"],
|
||||
"RightHandThumb3": ["thumb.03.r", "righthandthumb3", "thumb_03.r"],
|
||||
"RightHandIndex1": ["f_index.01.r", "righthandindex1", "index_01.r"],
|
||||
"RightHandIndex2": ["f_index.02.r", "righthandindex2", "index_02.r"],
|
||||
"RightHandIndex3": ["f_index.03.r", "righthandindex3", "index_03.r"],
|
||||
"RightHandMiddle1": ["f_middle.01.r", "righthandmiddle1", "middle_01.r"],
|
||||
"RightHandMiddle2": ["f_middle.02.r", "righthandmiddle2", "middle_02.r"],
|
||||
"RightHandMiddle3": ["f_middle.03.r", "righthandmiddle3", "middle_03.r"],
|
||||
"RightHandRing1": ["f_ring.01.r", "righthandring1", "ring_01.r"],
|
||||
"RightHandRing2": ["f_ring.02.r", "righthandring2", "ring_02.r"],
|
||||
"RightHandRing3": ["f_ring.03.r", "righthandring3", "ring_03.r"],
|
||||
"RightHandPinky1": ["f_pinky.01.r", "righthandpinky1", "pinky_01.r"],
|
||||
"RightHandPinky2": ["f_pinky.02.r", "righthandpinky2", "pinky_02.r"],
|
||||
"RightHandPinky3": ["f_pinky.03.r", "righthandpinky3", "pinky_03.r"],
|
||||
}
|
||||
|
||||
# 소스 본 리스트 (실제로 존재하는 본만)
|
||||
source_bones = [bone.name for bone in source.data.bones
|
||||
if bone.name in mixamo_bone_aliases]
|
||||
|
||||
# 타겟 본 리스트
|
||||
target_bones = [bone.name for bone in target.data.bones]
|
||||
|
||||
# Fuzzy matching 실행 (정확한 매칭 우선, 그 다음 유사도 매칭)
|
||||
logger.info("Running fuzzy bone matching algorithm...")
|
||||
logger.debug(f"Source bones: {len(source_bones)}, Target bones: {len(target_bones)}")
|
||||
|
||||
bone_map = fuzzy_match_bones(
|
||||
source_bones=source_bones,
|
||||
target_bones=target_bones,
|
||||
known_aliases=mixamo_bone_aliases,
|
||||
threshold=0.6, # 60% 이상 유사도
|
||||
prefer_exact=True # 정확한 매칭 우선
|
||||
)
|
||||
|
||||
# 매칭 품질 보고서
|
||||
quality_report = get_match_quality_report(bone_map)
|
||||
logger.info(f"Auto-mapped {quality_report['total_mappings']} bones")
|
||||
logger.info(f"Quality: {quality_report['quality'].upper()}")
|
||||
logger.info(f"Critical bones: {quality_report['critical_bones_mapped']}")
|
||||
logger.debug(f"Bone mapping: {bone_map}")
|
||||
|
||||
# 콘솔 출력 (사용자에게 피드백)
|
||||
print(f"✅ Auto-mapped {quality_report['total_mappings']} bones")
|
||||
print(f" Quality: {quality_report['quality'].upper()}")
|
||||
print(f" Critical bones: {quality_report['critical_bones_mapped']}")
|
||||
|
||||
return bone_map
|
||||
|
||||
|
||||
def retarget_animation(
|
||||
source_armature: str,
|
||||
target_armature: str,
|
||||
bone_map: Dict[str, str],
|
||||
preserve_rotation: bool = True,
|
||||
preserve_location: bool = False
|
||||
) -> str:
|
||||
"""
|
||||
애니메이션 리타게팅 실행
|
||||
|
||||
Args:
|
||||
source_armature: 소스 아마추어 이름
|
||||
target_armature: 타겟 아마추어 이름
|
||||
bone_map: 본 매핑 딕셔너리
|
||||
preserve_rotation: 회전 보존 여부
|
||||
preserve_location: 위치 보존 여부 (보통 루트 본만)
|
||||
|
||||
Returns:
|
||||
결과 메시지
|
||||
|
||||
Raises:
|
||||
ValueError: 아마추어를 찾을 수 없거나 애니메이션이 없는 경우
|
||||
"""
|
||||
logger.info(f"Retargeting animation: {source_armature} -> {target_armature}")
|
||||
logger.debug(f"Bone mappings: {len(bone_map)}, Rotation: {preserve_rotation}, Location: {preserve_location}")
|
||||
|
||||
source = bpy.data.objects.get(source_armature)
|
||||
target = bpy.data.objects.get(target_armature)
|
||||
|
||||
if not source or not target:
|
||||
logger.error("Source or target armature not found")
|
||||
raise ValueError("Source or target armature not found")
|
||||
|
||||
if not source.animation_data or not source.animation_data.action:
|
||||
logger.error("Source armature has no animation")
|
||||
raise ValueError("Source armature has no animation")
|
||||
|
||||
# 타겟 아마추어 선택
|
||||
bpy.context.view_layer.objects.active = target
|
||||
target.select_set(True)
|
||||
|
||||
# Pose 모드로 전환
|
||||
bpy.ops.object.mode_set(mode='POSE')
|
||||
|
||||
# 각 본에 대해 컨스트레인트 생성
|
||||
constraints_added = 0
|
||||
for source_bone_name, target_bone_name in bone_map.items():
|
||||
if source_bone_name not in source.pose.bones:
|
||||
logger.debug(f"Source bone not found: {source_bone_name}")
|
||||
continue
|
||||
if target_bone_name not in target.pose.bones:
|
||||
logger.debug(f"Target bone not found: {target_bone_name}")
|
||||
continue
|
||||
|
||||
target_bone = target.pose.bones[target_bone_name]
|
||||
|
||||
# Rotation constraint
|
||||
if preserve_rotation:
|
||||
constraint = target_bone.constraints.new('COPY_ROTATION')
|
||||
constraint.target = source
|
||||
constraint.subtarget = source_bone_name
|
||||
constraints_added += 1
|
||||
|
||||
# Location constraint (일반적으로 루트 본만)
|
||||
if preserve_location and source_bone_name == "Hips":
|
||||
constraint = target_bone.constraints.new('COPY_LOCATION')
|
||||
constraint.target = source
|
||||
constraint.subtarget = source_bone_name
|
||||
constraints_added += 1
|
||||
|
||||
logger.info(f"Added {constraints_added} constraints")
|
||||
|
||||
# 컨스트레인트를 키프레임으로 베이크
|
||||
logger.info("Baking constraints to keyframes...")
|
||||
bpy.ops.nla.bake(
|
||||
frame_start=bpy.context.scene.frame_start,
|
||||
frame_end=bpy.context.scene.frame_end,
|
||||
only_selected=False,
|
||||
visual_keying=True,
|
||||
clear_constraints=True,
|
||||
bake_types={'POSE'}
|
||||
)
|
||||
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
logger.info("Animation retargeting completed successfully")
|
||||
return f"Animation retargeted to {target_armature}"
|
||||
|
||||
|
||||
def get_preset_bone_mapping(preset: str) -> Dict[str, str]:
|
||||
"""
|
||||
미리 정의된 본 매핑 프리셋
|
||||
|
||||
Args:
|
||||
preset: 프리셋 이름 (예: "mixamo_to_rigify")
|
||||
|
||||
Returns:
|
||||
본 매핑 딕셔너리
|
||||
"""
|
||||
logger.debug(f"Getting bone mapping preset: {preset}")
|
||||
|
||||
presets = {
|
||||
"mixamo_to_rigify": {
|
||||
"Hips": "torso",
|
||||
"Spine": "spine",
|
||||
"Spine1": "spine.001",
|
||||
"Spine2": "spine.002",
|
||||
"Neck": "neck",
|
||||
"Head": "head",
|
||||
"LeftShoulder": "shoulder.L",
|
||||
"LeftArm": "upper_arm.L",
|
||||
"LeftForeArm": "forearm.L",
|
||||
"LeftHand": "hand.L",
|
||||
# ... 더 많은 매핑 추가 가능
|
||||
}
|
||||
}
|
||||
|
||||
bone_map = presets.get(preset, {})
|
||||
logger.info(f"Preset '{preset}' loaded with {len(bone_map)} mappings")
|
||||
return bone_map
|
||||
Reference in New Issue
Block a user