pyjallib.max API documentation

# pyjallib.max

JalTools 3DS 패키지 3DS Max 작업을 위한 모듈 모음

View Source

     1#!/usr/bin/env python
     2# -*- coding: utf-8 -*-
     3
     4"""
     5JalTools 3DS 패키지
     63DS Max 작업을 위한 모듈 모음
     7"""
     8
     9# 모듈 임포트
    10from .header import Header
    11
    12from .name import Name
    13from .anim import Anim
    14
    15from .helper import Helper
    16from .constraint import Constraint
    17from .bone import Bone
    18
    19from .mirror import Mirror
    20from .layer import Layer
    21from .align import Align
    22from .select import Select
    23from .link import Link
    24
    25from .bip import Bip
    26from .skin import Skin
    27from .morph import Morph
    28
    29from .twistBone import TwistBone
    30from .twistBoneChain import TwistBoneChain
    31from .groinBone import GroinBone
    32from .groinBoneChain import GroinBoneChain
    33from .autoClavicle import AutoClavicle
    34from .autoClavicleChain import AutoClavicleChain
    35from .volumeBone import VolumeBone
    36from .volumeBoneChain import VolumeBoneChain
    37from .kneeBone import KneeBone
    38from .hip import Hip
    39
    40from .ui.Container import Container
    41
    42# 모듈 내보내기
    43__all__ = [
    44    'Header',
    45    'Name',
    46    'Anim',
    47    'Helper',
    48    'Constraint',
    49    'Bone',
    50    'Mirror',
    51    'Layer',
    52    'Align',
    53    'Select',
    54    'Link',
    55    'Bip',
    56    'Skin',
    57    'Morph',
    58    'TwistBone',
    59    'TwistBoneChain',
    60    'GroinBone',
    61    'GroinBoneChain',
    62    'AutoClavicle',
    63    'AutoClavicleChain',
    64    'VolumeBone',
    65    'VolumeBoneChain',
    66    'KneeBone',
    67    'Hip',
    68    'Container'
    69]

class Header: View Source

     37class Header:
     38    """
     39    JalLib.max 패키지의 헤더 모듈
     40    3DS Max에서 사용하는 다양한 기능을 제공하는 클래스들을 초기화하고 관리합니다.
     41    """
     42    _instance = None
     43
     44    @classmethod
     45    def get_instance(cls):
     46        """싱글톤 패턴을 구현한 인스턴스 접근 메소드"""
     47        if cls._instance is None:
     48            cls._instance = Header()
     49        return cls._instance
     50
     51    def __init__(self):
     52        """
     53        Header 클래스 초기화
     54        """
     55        self.configDir = os.path.join(os.path.dirname(__file__), "ConfigFiles")
     56        self.nameConfigDir = os.path.join(self.configDir, "3DSMaxNamingConfig.json")
     57
     58        self.name = Name(configPath=self.nameConfigDir)
     59        self.anim = Anim()
     60
     61        self.helper = Helper(nameService=self.name)
     62        self.constraint = Constraint(nameService=self.name, helperService=self.helper)
     63        self.bone = Bone(nameService=self.name, animService=self.anim, helperService=self.helper, constraintService=self.constraint)
     64
     65        self.mirror = Mirror(nameService=self.name, boneService=self.bone)
     66        self.layer = Layer()
     67        self.align = Align()
     68        self.sel = Select(nameService=self.name, boneService=self.bone)
     69        self.link = Link()
     70
     71        self.bip = Bip(animService=self.anim, nameService=self.name, boneService=self.bone)
     72        self.skin = Skin()
     73
     74        self.twistBone = TwistBone(nameService=self.name, animService=self.anim, constraintService=self.constraint, bipService=self.bip, boneService=self.bone)
     75        self.groinBone = GroinBone(nameService=self.name, animService=self.anim, constraintService=self.constraint, boneService=self.bone, helperService=self.helper)
     76        self.autoClavicle = AutoClavicle(nameService=self.name, animService=self.anim, helperService=self.helper, boneService=self.bone, constraintService=self.constraint, bipService=self.bip)
     77        self.volumeBone = VolumeBone(nameService=self.name, animService=self.anim, constraintService=self.constraint, boneService=self.bone, helperService=self.helper)
     78        self.kneeBone = KneeBone(nameService=self.name, animService=self.anim, constraintService=self.constraint, boneService=self.bone, helperService=self.helper, volumeBoneService=self.volumeBone)
     79        self.hip = Hip(nameService=self.name, animService=self.anim, helperService=self.helper, boneService=self.bone, constraintService=self.constraint)
     80
     81        self.morph = Morph()
     82
     83        self.tools = []
     84
     85    def update_nameConifg(self, configPath):
     86        """
     87        이름 설정을 업데이트합니다.
     88
     89        Args:
     90            configPath: ConfigPath 인스턴스
     91        """
     92        self.name.load_from_config_file(configPath)
     93
     94    def add_tool(self, tool):
     95        """
     96        도구를 추가합니다.
     97
     98        Args:
     99            tool: 추가할 도구
    100        """
    101        if tool in self.tools:
    102            self.tools.remove(tool)
    103
    104        self.tools.append(tool)

JalLib.max 패키지의 헤더 모듈 3DS Max에서 사용하는 다양한 기능을 제공하는 클래스들을 초기화하고 관리합니다.

Header() View Source

    51    def __init__(self):
    52        """
    53        Header 클래스 초기화
    54        """
    55        self.configDir = os.path.join(os.path.dirname(__file__), "ConfigFiles")
    56        self.nameConfigDir = os.path.join(self.configDir, "3DSMaxNamingConfig.json")
    57
    58        self.name = Name(configPath=self.nameConfigDir)
    59        self.anim = Anim()
    60
    61        self.helper = Helper(nameService=self.name)
    62        self.constraint = Constraint(nameService=self.name, helperService=self.helper)
    63        self.bone = Bone(nameService=self.name, animService=self.anim, helperService=self.helper, constraintService=self.constraint)
    64
    65        self.mirror = Mirror(nameService=self.name, boneService=self.bone)
    66        self.layer = Layer()
    67        self.align = Align()
    68        self.sel = Select(nameService=self.name, boneService=self.bone)
    69        self.link = Link()
    70
    71        self.bip = Bip(animService=self.anim, nameService=self.name, boneService=self.bone)
    72        self.skin = Skin()
    73
    74        self.twistBone = TwistBone(nameService=self.name, animService=self.anim, constraintService=self.constraint, bipService=self.bip, boneService=self.bone)
    75        self.groinBone = GroinBone(nameService=self.name, animService=self.anim, constraintService=self.constraint, boneService=self.bone, helperService=self.helper)
    76        self.autoClavicle = AutoClavicle(nameService=self.name, animService=self.anim, helperService=self.helper, boneService=self.bone, constraintService=self.constraint, bipService=self.bip)
    77        self.volumeBone = VolumeBone(nameService=self.name, animService=self.anim, constraintService=self.constraint, boneService=self.bone, helperService=self.helper)
    78        self.kneeBone = KneeBone(nameService=self.name, animService=self.anim, constraintService=self.constraint, boneService=self.bone, helperService=self.helper, volumeBoneService=self.volumeBone)
    79        self.hip = Hip(nameService=self.name, animService=self.anim, helperService=self.helper, boneService=self.bone, constraintService=self.constraint)
    80
    81        self.morph = Morph()
    82
    83        self.tools = []

Header 클래스 초기화

@classmethod
def get_instance(cls): View Source

    44    @classmethod
    45    def get_instance(cls):
    46        """싱글톤 패턴을 구현한 인스턴스 접근 메소드"""
    47        if cls._instance is None:
    48            cls._instance = Header()
    49        return cls._instance

싱글톤 패턴을 구현한 인스턴스 접근 메소드

configDir
nameConfigDir
name
anim
helper
constraint
bone
mirror
layer
align
sel
link
bip
skin
twistBone
groinBone
autoClavicle
volumeBone
kneeBone
hip
morph
tools
def update_nameConifg(self, configPath): View Source

    85    def update_nameConifg(self, configPath):
    86        """
    87        이름 설정을 업데이트합니다.
    88
    89        Args:
    90            configPath: ConfigPath 인스턴스
    91        """
    92        self.name.load_from_config_file(configPath)

이름 설정을 업데이트합니다.

Args: configPath: ConfigPath 인스턴스

def add_tool(self, tool): View Source

     94    def add_tool(self, tool):
     95        """
     96        도구를 추가합니다.
     97
     98        Args:
     99            tool: 추가할 도구
    100        """
    101        if tool in self.tools:
    102            self.tools.remove(tool)
    103
    104        self.tools.append(tool)

도구를 추가합니다.

Args: tool: 추가할 도구

class Name(pyjallib.naming.Naming): View Source

     16class Name(Naming):
     17    """
     18    3ds Max 노드 이름 관리를 위한 클래스
     19    Naming 클래스를 상속받으며, Max 특화 기능 제공
     20    """
     21
     22    def __init__(self, configPath=None):
     23        """
     24        클래스 초기화
     25
     26        Args:
     27            configPath: 설정 파일 경로 (기본값: None)
     28                        설정 파일이 제공되면 해당 파일에서 설정을 로드함
     29        """
     30        # 기본 설정값
     31        self._paddingNum = 2
     32        self._configPath = configPath
     33
     34        # 기본 namePart 초기화 (각 부분에 사전 정의 값 직접 설정)
     35        self._nameParts = []
     36
     37        if configPath:
     38            # 사용자가 지정한 설정 파일 사용
     39            self.load_from_config_file(configPath=configPath)
     40        else:
     41            configDir = os.path.join(os.path.dirname(__file__), "ConfigFiles")
     42            nameConfigDir = os.path.join(configDir, "3DSMaxNamingConfig.json")
     43            self.load_from_config_file(configPath=nameConfigDir)
     44
     45    # NamePart 직접 액세스 메소드들
     46    # get_<NamePart 이름>_values 메소드들
     47    def get_Base_values(self):
     48        """
     49        Base 부분의 사전 정의 값 목록 반환
     50
     51        Returns:
     52            Base 부분의 사전 정의 값 목록
     53        """
     54        return self.get_name_part_predefined_values("Base")
     55
     56    def get_Type_values(self):
     57        """
     58        Type 부분의 사전 정의 값 목록 반환
     59
     60        Returns:
     61            Type 부분의 사전 정의 값 목록
     62        """
     63        return self.get_name_part_predefined_values("Type")
     64
     65    def get_Side_values(self):
     66        """
     67        Side 부분의 사전 정의 값 목록 반환
     68
     69        Returns:
     70            Side 부분의 사전 정의 값 목록
     71        """
     72        return self.get_name_part_predefined_values("Side")
     73
     74    def get_FrontBack_values(self):
     75        """
     76        FrontBack 부분의 사전 정의 값 목록 반환
     77
     78        Returns:
     79            FrontBack 부분의 사전 정의 값 목록
     80        """
     81        return self.get_name_part_predefined_values("FrontBack")
     82
     83    def get_Nub_values(self):
     84        """
     85        Nub 부분의 사전 정의 값 목록 반환
     86
     87        Returns:
     88            Nub 부분의 사전 정의 값 목록
     89        """
     90        return self.get_name_part_predefined_values("Nub")
     91
     92    # is_<NamePart 이름> 메소드들
     93    def is_Base(self, inStr):
     94        """
     95        문자열이 Base 부분의 사전 정의 값인지 확인
     96
     97        Args:
     98            inStr: 확인할 문자열
     99
    100        Returns:
    101            Base 부분의 사전 정의 값이면 True, 아니면 False
    102        """
    103        return self.is_in_name_part_predefined_values("Base", inStr)
    104
    105    def is_Type(self, inStr):
    106        """
    107        문자열이 Type 부분의 사전 정의 값인지 확인
    108
    109        Args:
    110            inStr: 확인할 문자열
    111
    112        Returns:
    113            Type 부분의 사전 정의 값이면 True, 아니면 False
    114        """
    115        return self.is_in_name_part_predefined_values("Type", inStr)
    116
    117    def is_Side(self, inStr):
    118        """
    119        문자열이 Side 부분의 사전 정의 값인지 확인
    120
    121        Args:
    122            inStr: 확인할 문자열
    123
    124        Returns:
    125            Side 부분의 사전 정의 값이면 True, 아니면 False
    126        """
    127        return self.is_in_name_part_predefined_values("Side", inStr)
    128
    129    def is_FrontBack(self, inStr):
    130        """
    131        문자열이 FrontBack 부분의 사전 정의 값인지 확인
    132
    133        Args:
    134            inStr: 확인할 문자열
    135
    136        Returns:
    137            FrontBack 부분의 사전 정의 값이면 True, 아니면 False
    138        """
    139        return self.is_in_name_part_predefined_values("FrontBack", inStr)
    140
    141    def is_Nub(self, inStr):
    142        """
    143        문자열이 Nub 부분의 사전 정의 값인지 확인
    144
    145        Args:
    146            inStr: 확인할 문자열
    147
    148        Returns:
    149            Nub 부분의 사전 정의 값이면 True, 아니면 False
    150        """
    151        return self.is_in_name_part_predefined_values("Nub", inStr)
    152
    153    # has_<NamePart 이름> 메소드들
    154    def has_Base(self, inStr):
    155        """
    156        문자열에 Base 부분의 사전 정의 값이 포함되어 있는지 확인
    157
    158        Args:
    159            inStr: 확인할 문자열
    160
    161        Returns:
    162            Base 부분의 사전 정의 값이 포함되어 있으면 True, 아니면 False
    163        """
    164        return self.has_name_part("Base", inStr)
    165
    166    def has_Type(self, inStr):
    167        """
    168        문자열에 Type 부분의 사전 정의 값이 포함되어 있는지 확인
    169
    170        Args:
    171            inStr: 확인할 문자열
    172
    173        Returns:
    174            Type 부분의 사전 정의 값이 포함되어 있으면 True, 아니면 False
    175        """
    176        return self.has_name_part("Type", inStr)
    177
    178    def has_Side(self, inStr):
    179        """
    180        문자열에 Side 부분의 사전 정의 값이 포함되어 있는지 확인
    181
    182        Args:
    183            inStr: 확인할 문자열
    184
    185        Returns:
    186            Side 부분의 사전 정의 값이 포함되어 있으면 True, 아니면 False
    187        """
    188        return self.has_name_part("Side", inStr)
    189
    190    def has_FrontBack(self, inStr):
    191        """
    192        문자열에 FrontBack 부분의 사전 정의 값이 포함되어 있는지 확인
    193
    194        Args:
    195            inStr: 확인할 문자열
    196
    197        Returns:
    198            FrontBack 부분의 사전 정의 값이 포함되어 있으면 True, 아니면 False
    199        """
    200        return self.has_name_part("FrontBack", inStr)
    201
    202    def has_Nub(self, inStr):
    203        """
    204        문자열에 Nub 부분의 사전 정의 값이 포함되어 있는지 확인
    205
    206        Args:
    207            inStr: 확인할 문자열
    208
    209        Returns:
    210            Nub 부분의 사전 정의 값이 포함되어 있으면 True, 아니면 False
    211        """
    212        return self.has_name_part("Nub", inStr)
    213
    214    # replace_<NamePart 이름> 메소드들
    215    def replace_Base(self, inStr, inNewName):
    216        """
    217        문자열의 Base 부분을 새 이름으로 변경
    218
    219        Args:
    220            inStr: 처리할 문자열
    221            inNewName: 새 이름
    222
    223        Returns:
    224            변경된 문자열
    225        """
    226        return self.replace_name_part("Base", inStr, inNewName)
    227
    228    def replace_Type(self, inStr, inNewName):
    229        """
    230        문자열의 Type 부분을 새 이름으로 변경
    231
    232        Args:
    233            inStr: 처리할 문자열
    234            inNewName: 새 이름
    235
    236        Returns:
    237            변경된 문자열
    238        """
    239        return self.replace_name_part("Type", inStr, inNewName)
    240
    241    def replace_Side(self, inStr, inNewName):
    242        """
    243        문자열의 Side 부분을 새 이름으로 변경
    244
    245        Args:
    246            inStr: 처리할 문자열
    247            inNewName: 새 이름
    248
    249        Returns:
    250            변경된 문자열
    251        """
    252        return self.replace_name_part("Side", inStr, inNewName)
    253
    254    def replace_FrontBack(self, inStr, inNewName):
    255        """
    256        문자열의 FrontBack 부분을 새 이름으로 변경
    257
    258        Args:
    259            inStr: 처리할 문자열
    260            inNewName: 새 이름
    261
    262        Returns:
    263            변경된 문자열
    264        """
    265        return self.replace_name_part("FrontBack", inStr, inNewName)
    266
    267    def replace_RealName(self, inStr, inNewName):
    268        """
    269        문자열의 RealName 부분을 새 이름으로 변경
    270
    271        Args:
    272            inStr: 처리할 문자열
    273            inNewName: 새 이름
    274
    275        Returns:
    276            변경된 문자열
    277        """
    278        return self.replace_name_part("RealName", inStr, inNewName)
    279
    280    def replace_Index(self, inStr, inNewName):
    281        """
    282        문자열의 Index 부분을 새 이름으로 변경
    283
    284        Args:
    285            inStr: 처리할 문자열
    286            inNewName: 새 이름 (숫자 문자열)
    287
    288        Returns:
    289            변경된 문자열
    290        """
    291        return self.replace_name_part("Index", inStr, inNewName)
    292
    293    def replace_Nub(self, inStr, inNewName):
    294        """
    295        문자열의 Nub 부분을 새 이름으로 변경
    296
    297        Args:
    298            inStr: 처리할 문자열
    299            inNewName: 새 이름
    300
    301        Returns:
    302            변경된 문자열
    303        """
    304        return self.replace_name_part("Nub", inStr, inNewName)
    305
    306    # remove_<NamePart 이름> 메소드들
    307    def remove_Base(self, inStr):
    308        """
    309        문자열에서 Base 부분 제거
    310
    311        Args:
    312            inStr: 처리할 문자열
    313
    314        Returns:
    315            Base 부분이 제거된 문자열
    316        """
    317        return self.remove_name_part("Base", inStr)
    318
    319    def remove_Type(self, inStr):
    320        """
    321        문자열에서 Type 부분 제거
    322
    323        Args:
    324            inStr: 처리할 문자열
    325
    326        Returns:
    327            Type 부분이 제거된 문자열
    328        """
    329        return self.remove_name_part("Type", inStr)
    330
    331    def remove_Side(self, inStr):
    332        """
    333        문자열에서 Side 부분 제거
    334
    335        Args:
    336            inStr: 처리할 문자열
    337
    338        Returns:
    339            Side 부분이 제거된 문자열
    340        """
    341        return self.remove_name_part("Side", inStr)
    342
    343    def remove_FrontBack(self, inStr):
    344        """
    345        문자열에서 FrontBack 부분 제거
    346
    347        Args:
    348            inStr: 처리할 문자열
    349
    350        Returns:
    351            FrontBack 부분이 제거된 문자열
    352        """
    353        return self.remove_name_part("FrontBack", inStr)
    354
    355    def remove_Index(self, inStr):
    356        """
    357        문자열에서 Index 부분 제거
    358
    359        Args:
    360            inStr: 처리할 문자열
    361
    362        Returns:
    363            Index 부분이 제거된 문자열
    364        """
    365        return self.remove_name_part("Index", inStr)
    366
    367    def remove_Nub(self, inStr):
    368        """
    369        문자열에서 Nub 부분 제거
    370
    371        Args:
    372            inStr: 처리할 문자열
    373
    374        Returns:
    375            Nub 부분이 제거된 문자열
    376        """
    377        return self.remove_name_part("Nub", inStr)
    378
    379    # pymxs 의존적인 메소드 구현
    380
    381    def gen_unique_name(self, inStr):
    382        """
    383        고유한 이름 생성
    384
    385        Args:
    386            inStr: 기준 이름 문자열
    387
    388        Returns:
    389            고유한 이름 문자열
    390        """
    391        pattern_str = self.replace_Index(inStr, "*")
    392
    393        # pymxs를 사용하여 객체 이름을 패턴과 매칭하여 검색
    394        matched_objects = []
    395
    396        # 모든 객체 중에서 패턴과 일치하는 이름 찾기
    397        for obj in rt.objects:
    398            if rt.matchPattern(obj.name, pattern=pattern_str):
    399                matched_objects.append(obj)
    400
    401        return self.replace_Index(inStr, str(len(matched_objects) + 1))
    402
    403    def compare_name(self, inObjA, inObjB):
    404        """
    405        두 객체의 이름 비교 (정렬용)
    406
    407        Args:
    408            inObjA: 첫 번째 객체
    409            inObjB: 두 번째 객체
    410
    411        Returns:
    412            비교 결과 (inObjA.name < inObjB.name: 음수, inObjA.name == inObjB.name: 0, inObjA.name > inObjB.name: 양수)
    413        """
    414        # Python에서는 대소문자 구분 없는 비교를 위해 lower() 사용
    415        return 1 if inObjA.name.lower() > inObjB.name.lower() else -1 if inObjA.name.lower() < inObjB.name.lower() else 0
    416
    417    def sort_by_name(self, inArray):
    418        """
    419        객체 배열을 이름 기준으로 정렬
    420
    421        Args:
    422            inArray: 정렬할 객체 배열
    423
    424        Returns:
    425            이름 기준으로 정렬된 객체 배열
    426        """
    427        # Python의 sorted 함수와 key를 사용하여 이름 기준 정렬
    428        return sorted(inArray, key=lambda obj: obj.name.lower())
    429
    430    def gen_mirroring_name(self, inStr):
    431        """
    432        미러링된 이름 생성 (측면 또는 앞/뒤 변경)
    433
    434        이름에서 Side와 FrontBack namePart를 자동으로 검색하고,
    435        발견된 값의 semanticmapping weight와 가장 차이가 큰 값으로 교체합니다.
    436
    437        Args:
    438            inStr: 처리할 이름 문자열
    439
    440        Returns:
    441            미러링된 이름 문자열
    442        """
    443        return_name = super().gen_mirroring_name(inStr)
    444
    445        # 이름이 변경되지 않았다면 고유한 이름 생성
    446        if return_name == inStr:
    447            if self.has_Side(inStr) or self.has_FrontBack(inStr):
    448                return_name = self.gen_unique_name(inStr)
    449            else:
    450                return_name = self.add_suffix_to_real_name(inStr, "Mirrored")
    451
    452        return return_name
    453
    454    # Type name Part에서 Description으로 지정된 predefined value를 가져오는 메소드들
    455    def get_parent_value(self):
    456        """
    457        부모 이름 문자열 반환
    458
    459        Returns:
    460            부모 이름 문자열
    461        """
    462        return self.get_name_part_value_by_description("Type", "Parent")
    463
    464    def get_dummy_value(self):
    465        """
    466        더미 이름 문자열 반환
    467
    468        Returns:
    469            더미 이름 문자열
    470        """
    471        return self.get_name_part_value_by_description("Type", "Dummy")
    472
    473    def get_exposeTm_value(self):
    474        """
    475        ExposeTm 이름 문자열 반환
    476
    477        Returns:
    478            ExposeTm 이름 문자열
    479        """
    480        return self.get_name_part_value_by_description("Type", "ExposeTM")
    481
    482    def get_ik_value(self):
    483        """
    484        IK 이름 문자열 반환
    485
    486        Returns:
    487            IK 이름 문자열
    488        """
    489        return self.get_name_part_value_by_description("Type", "IK")
    490
    491    def get_target_value(self):
    492        """
    493        타겟 이름 문자열 반환
    494
    495        Returns:
    496            타겟 이름 문자열
    497        """
    498        return self.get_name_part_value_by_description("Type", "Target")

3ds Max 노드 이름 관리를 위한 클래스 Naming 클래스를 상속받으며, Max 특화 기능 제공

Name(configPath=None) View Source

    22    def __init__(self, configPath=None):
    23        """
    24        클래스 초기화
    25
    26        Args:
    27            configPath: 설정 파일 경로 (기본값: None)
    28                        설정 파일이 제공되면 해당 파일에서 설정을 로드함
    29        """
    30        # 기본 설정값
    31        self._paddingNum = 2
    32        self._configPath = configPath
    33
    34        # 기본 namePart 초기화 (각 부분에 사전 정의 값 직접 설정)
    35        self._nameParts = []
    36
    37        if configPath:
    38            # 사용자가 지정한 설정 파일 사용
    39            self.load_from_config_file(configPath=configPath)
    40        else:
    41            configDir = os.path.join(os.path.dirname(__file__), "ConfigFiles")
    42            nameConfigDir = os.path.join(configDir, "3DSMaxNamingConfig.json")
    43            self.load_from_config_file(configPath=nameConfigDir)

클래스 초기화

Args: configPath: 설정 파일 경로 (기본값: None) 설정 파일이 제공되면 해당 파일에서 설정을 로드함

def get_Base_values(self): View Source

    47    def get_Base_values(self):
    48        """
    49        Base 부분의 사전 정의 값 목록 반환
    50
    51        Returns:
    52            Base 부분의 사전 정의 값 목록
    53        """
    54        return self.get_name_part_predefined_values("Base")

Base 부분의 사전 정의 값 목록 반환

Returns: Base 부분의 사전 정의 값 목록

def get_Type_values(self): View Source

    56    def get_Type_values(self):
    57        """
    58        Type 부분의 사전 정의 값 목록 반환
    59
    60        Returns:
    61            Type 부분의 사전 정의 값 목록
    62        """
    63        return self.get_name_part_predefined_values("Type")

Type 부분의 사전 정의 값 목록 반환

Returns: Type 부분의 사전 정의 값 목록

def get_Side_values(self): View Source

    65    def get_Side_values(self):
    66        """
    67        Side 부분의 사전 정의 값 목록 반환
    68
    69        Returns:
    70            Side 부분의 사전 정의 값 목록
    71        """
    72        return self.get_name_part_predefined_values("Side")

Side 부분의 사전 정의 값 목록 반환

Returns: Side 부분의 사전 정의 값 목록

def get_FrontBack_values(self): View Source

    74    def get_FrontBack_values(self):
    75        """
    76        FrontBack 부분의 사전 정의 값 목록 반환
    77
    78        Returns:
    79            FrontBack 부분의 사전 정의 값 목록
    80        """
    81        return self.get_name_part_predefined_values("FrontBack")

FrontBack 부분의 사전 정의 값 목록 반환

Returns: FrontBack 부분의 사전 정의 값 목록

def get_Nub_values(self): View Source

    83    def get_Nub_values(self):
    84        """
    85        Nub 부분의 사전 정의 값 목록 반환
    86
    87        Returns:
    88            Nub 부분의 사전 정의 값 목록
    89        """
    90        return self.get_name_part_predefined_values("Nub")

Nub 부분의 사전 정의 값 목록 반환

Returns: Nub 부분의 사전 정의 값 목록

def is_Base(self, inStr): View Source

     93    def is_Base(self, inStr):
     94        """
     95        문자열이 Base 부분의 사전 정의 값인지 확인
     96
     97        Args:
     98            inStr: 확인할 문자열
     99
    100        Returns:
    101            Base 부분의 사전 정의 값이면 True, 아니면 False
    102        """
    103        return self.is_in_name_part_predefined_values("Base", inStr)

문자열이 Base 부분의 사전 정의 값인지 확인

Args: inStr: 확인할 문자열

Returns: Base 부분의 사전 정의 값이면 True, 아니면 False

def is_Type(self, inStr): View Source

    105    def is_Type(self, inStr):
    106        """
    107        문자열이 Type 부분의 사전 정의 값인지 확인
    108
    109        Args:
    110            inStr: 확인할 문자열
    111
    112        Returns:
    113            Type 부분의 사전 정의 값이면 True, 아니면 False
    114        """
    115        return self.is_in_name_part_predefined_values("Type", inStr)

문자열이 Type 부분의 사전 정의 값인지 확인

Args: inStr: 확인할 문자열

Returns: Type 부분의 사전 정의 값이면 True, 아니면 False

def is_Side(self, inStr): View Source

    117    def is_Side(self, inStr):
    118        """
    119        문자열이 Side 부분의 사전 정의 값인지 확인
    120
    121        Args:
    122            inStr: 확인할 문자열
    123
    124        Returns:
    125            Side 부분의 사전 정의 값이면 True, 아니면 False
    126        """
    127        return self.is_in_name_part_predefined_values("Side", inStr)

문자열이 Side 부분의 사전 정의 값인지 확인

Args: inStr: 확인할 문자열

Returns: Side 부분의 사전 정의 값이면 True, 아니면 False

def is_FrontBack(self, inStr): View Source

    129    def is_FrontBack(self, inStr):
    130        """
    131        문자열이 FrontBack 부분의 사전 정의 값인지 확인
    132
    133        Args:
    134            inStr: 확인할 문자열
    135
    136        Returns:
    137            FrontBack 부분의 사전 정의 값이면 True, 아니면 False
    138        """
    139        return self.is_in_name_part_predefined_values("FrontBack", inStr)

문자열이 FrontBack 부분의 사전 정의 값인지 확인

Args: inStr: 확인할 문자열

Returns: FrontBack 부분의 사전 정의 값이면 True, 아니면 False

def is_Nub(self, inStr): View Source

    141    def is_Nub(self, inStr):
    142        """
    143        문자열이 Nub 부분의 사전 정의 값인지 확인
    144
    145        Args:
    146            inStr: 확인할 문자열
    147
    148        Returns:
    149            Nub 부분의 사전 정의 값이면 True, 아니면 False
    150        """
    151        return self.is_in_name_part_predefined_values("Nub", inStr)

문자열이 Nub 부분의 사전 정의 값인지 확인

Args: inStr: 확인할 문자열

Returns: Nub 부분의 사전 정의 값이면 True, 아니면 False

def has_Base(self, inStr): View Source

    154    def has_Base(self, inStr):
    155        """
    156        문자열에 Base 부분의 사전 정의 값이 포함되어 있는지 확인
    157
    158        Args:
    159            inStr: 확인할 문자열
    160
    161        Returns:
    162            Base 부분의 사전 정의 값이 포함되어 있으면 True, 아니면 False
    163        """
    164        return self.has_name_part("Base", inStr)

문자열에 Base 부분의 사전 정의 값이 포함되어 있는지 확인

Args: inStr: 확인할 문자열

Returns: Base 부분의 사전 정의 값이 포함되어 있으면 True, 아니면 False

def has_Type(self, inStr): View Source

    166    def has_Type(self, inStr):
    167        """
    168        문자열에 Type 부분의 사전 정의 값이 포함되어 있는지 확인
    169
    170        Args:
    171            inStr: 확인할 문자열
    172
    173        Returns:
    174            Type 부분의 사전 정의 값이 포함되어 있으면 True, 아니면 False
    175        """
    176        return self.has_name_part("Type", inStr)

문자열에 Type 부분의 사전 정의 값이 포함되어 있는지 확인

Args: inStr: 확인할 문자열

Returns: Type 부분의 사전 정의 값이 포함되어 있으면 True, 아니면 False

def has_Side(self, inStr): View Source

    178    def has_Side(self, inStr):
    179        """
    180        문자열에 Side 부분의 사전 정의 값이 포함되어 있는지 확인
    181
    182        Args:
    183            inStr: 확인할 문자열
    184
    185        Returns:
    186            Side 부분의 사전 정의 값이 포함되어 있으면 True, 아니면 False
    187        """
    188        return self.has_name_part("Side", inStr)

문자열에 Side 부분의 사전 정의 값이 포함되어 있는지 확인

Args: inStr: 확인할 문자열

Returns: Side 부분의 사전 정의 값이 포함되어 있으면 True, 아니면 False

def has_FrontBack(self, inStr): View Source

    190    def has_FrontBack(self, inStr):
    191        """
    192        문자열에 FrontBack 부분의 사전 정의 값이 포함되어 있는지 확인
    193
    194        Args:
    195            inStr: 확인할 문자열
    196
    197        Returns:
    198            FrontBack 부분의 사전 정의 값이 포함되어 있으면 True, 아니면 False
    199        """
    200        return self.has_name_part("FrontBack", inStr)

문자열에 FrontBack 부분의 사전 정의 값이 포함되어 있는지 확인

Args: inStr: 확인할 문자열

Returns: FrontBack 부분의 사전 정의 값이 포함되어 있으면 True, 아니면 False

def has_Nub(self, inStr): View Source

    202    def has_Nub(self, inStr):
    203        """
    204        문자열에 Nub 부분의 사전 정의 값이 포함되어 있는지 확인
    205
    206        Args:
    207            inStr: 확인할 문자열
    208
    209        Returns:
    210            Nub 부분의 사전 정의 값이 포함되어 있으면 True, 아니면 False
    211        """
    212        return self.has_name_part("Nub", inStr)

문자열에 Nub 부분의 사전 정의 값이 포함되어 있는지 확인

Args: inStr: 확인할 문자열

Returns: Nub 부분의 사전 정의 값이 포함되어 있으면 True, 아니면 False

def replace_Base(self, inStr, inNewName): View Source

    215    def replace_Base(self, inStr, inNewName):
    216        """
    217        문자열의 Base 부분을 새 이름으로 변경
    218
    219        Args:
    220            inStr: 처리할 문자열
    221            inNewName: 새 이름
    222
    223        Returns:
    224            변경된 문자열
    225        """
    226        return self.replace_name_part("Base", inStr, inNewName)

문자열의 Base 부분을 새 이름으로 변경

Args: inStr: 처리할 문자열 inNewName: 새 이름

Returns: 변경된 문자열

def replace_Type(self, inStr, inNewName): View Source

    228    def replace_Type(self, inStr, inNewName):
    229        """
    230        문자열의 Type 부분을 새 이름으로 변경
    231
    232        Args:
    233            inStr: 처리할 문자열
    234            inNewName: 새 이름
    235
    236        Returns:
    237            변경된 문자열
    238        """
    239        return self.replace_name_part("Type", inStr, inNewName)

문자열의 Type 부분을 새 이름으로 변경

Args: inStr: 처리할 문자열 inNewName: 새 이름

Returns: 변경된 문자열

def replace_Side(self, inStr, inNewName): View Source

    241    def replace_Side(self, inStr, inNewName):
    242        """
    243        문자열의 Side 부분을 새 이름으로 변경
    244
    245        Args:
    246            inStr: 처리할 문자열
    247            inNewName: 새 이름
    248
    249        Returns:
    250            변경된 문자열
    251        """
    252        return self.replace_name_part("Side", inStr, inNewName)

문자열의 Side 부분을 새 이름으로 변경

Args: inStr: 처리할 문자열 inNewName: 새 이름

Returns: 변경된 문자열

def replace_FrontBack(self, inStr, inNewName): View Source

    254    def replace_FrontBack(self, inStr, inNewName):
    255        """
    256        문자열의 FrontBack 부분을 새 이름으로 변경
    257
    258        Args:
    259            inStr: 처리할 문자열
    260            inNewName: 새 이름
    261
    262        Returns:
    263            변경된 문자열
    264        """
    265        return self.replace_name_part("FrontBack", inStr, inNewName)

문자열의 FrontBack 부분을 새 이름으로 변경

Args: inStr: 처리할 문자열 inNewName: 새 이름

Returns: 변경된 문자열

def replace_RealName(self, inStr, inNewName): View Source

    267    def replace_RealName(self, inStr, inNewName):
    268        """
    269        문자열의 RealName 부분을 새 이름으로 변경
    270
    271        Args:
    272            inStr: 처리할 문자열
    273            inNewName: 새 이름
    274
    275        Returns:
    276            변경된 문자열
    277        """
    278        return self.replace_name_part("RealName", inStr, inNewName)

문자열의 RealName 부분을 새 이름으로 변경

Args: inStr: 처리할 문자열 inNewName: 새 이름

Returns: 변경된 문자열

def replace_Index(self, inStr, inNewName): View Source

    280    def replace_Index(self, inStr, inNewName):
    281        """
    282        문자열의 Index 부분을 새 이름으로 변경
    283
    284        Args:
    285            inStr: 처리할 문자열
    286            inNewName: 새 이름 (숫자 문자열)
    287
    288        Returns:
    289            변경된 문자열
    290        """
    291        return self.replace_name_part("Index", inStr, inNewName)

문자열의 Index 부분을 새 이름으로 변경

Args: inStr: 처리할 문자열 inNewName: 새 이름 (숫자 문자열)

Returns: 변경된 문자열

def replace_Nub(self, inStr, inNewName): View Source

    293    def replace_Nub(self, inStr, inNewName):
    294        """
    295        문자열의 Nub 부분을 새 이름으로 변경
    296
    297        Args:
    298            inStr: 처리할 문자열
    299            inNewName: 새 이름
    300
    301        Returns:
    302            변경된 문자열
    303        """
    304        return self.replace_name_part("Nub", inStr, inNewName)

문자열의 Nub 부분을 새 이름으로 변경

Args: inStr: 처리할 문자열 inNewName: 새 이름

Returns: 변경된 문자열

def remove_Base(self, inStr): View Source

    307    def remove_Base(self, inStr):
    308        """
    309        문자열에서 Base 부분 제거
    310
    311        Args:
    312            inStr: 처리할 문자열
    313
    314        Returns:
    315            Base 부분이 제거된 문자열
    316        """
    317        return self.remove_name_part("Base", inStr)

문자열에서 Base 부분 제거

Args: inStr: 처리할 문자열

Returns: Base 부분이 제거된 문자열

def remove_Type(self, inStr): View Source

    319    def remove_Type(self, inStr):
    320        """
    321        문자열에서 Type 부분 제거
    322
    323        Args:
    324            inStr: 처리할 문자열
    325
    326        Returns:
    327            Type 부분이 제거된 문자열
    328        """
    329        return self.remove_name_part("Type", inStr)

문자열에서 Type 부분 제거

Args: inStr: 처리할 문자열

Returns: Type 부분이 제거된 문자열

def remove_Side(self, inStr): View Source

    331    def remove_Side(self, inStr):
    332        """
    333        문자열에서 Side 부분 제거
    334
    335        Args:
    336            inStr: 처리할 문자열
    337
    338        Returns:
    339            Side 부분이 제거된 문자열
    340        """
    341        return self.remove_name_part("Side", inStr)

문자열에서 Side 부분 제거

Args: inStr: 처리할 문자열

Returns: Side 부분이 제거된 문자열

def remove_FrontBack(self, inStr): View Source

    343    def remove_FrontBack(self, inStr):
    344        """
    345        문자열에서 FrontBack 부분 제거
    346
    347        Args:
    348            inStr: 처리할 문자열
    349
    350        Returns:
    351            FrontBack 부분이 제거된 문자열
    352        """
    353        return self.remove_name_part("FrontBack", inStr)

문자열에서 FrontBack 부분 제거

Args: inStr: 처리할 문자열

Returns: FrontBack 부분이 제거된 문자열

def remove_Index(self, inStr): View Source

    355    def remove_Index(self, inStr):
    356        """
    357        문자열에서 Index 부분 제거
    358
    359        Args:
    360            inStr: 처리할 문자열
    361
    362        Returns:
    363            Index 부분이 제거된 문자열
    364        """
    365        return self.remove_name_part("Index", inStr)

문자열에서 Index 부분 제거

Args: inStr: 처리할 문자열

Returns: Index 부분이 제거된 문자열

def remove_Nub(self, inStr): View Source

    367    def remove_Nub(self, inStr):
    368        """
    369        문자열에서 Nub 부분 제거
    370
    371        Args:
    372            inStr: 처리할 문자열
    373
    374        Returns:
    375            Nub 부분이 제거된 문자열
    376        """
    377        return self.remove_name_part("Nub", inStr)

문자열에서 Nub 부분 제거

Args: inStr: 처리할 문자열

Returns: Nub 부분이 제거된 문자열

def gen_unique_name(self, inStr): View Source

    381    def gen_unique_name(self, inStr):
    382        """
    383        고유한 이름 생성
    384
    385        Args:
    386            inStr: 기준 이름 문자열
    387
    388        Returns:
    389            고유한 이름 문자열
    390        """
    391        pattern_str = self.replace_Index(inStr, "*")
    392
    393        # pymxs를 사용하여 객체 이름을 패턴과 매칭하여 검색
    394        matched_objects = []
    395
    396        # 모든 객체 중에서 패턴과 일치하는 이름 찾기
    397        for obj in rt.objects:
    398            if rt.matchPattern(obj.name, pattern=pattern_str):
    399                matched_objects.append(obj)
    400
    401        return self.replace_Index(inStr, str(len(matched_objects) + 1))

고유한 이름 생성

Args: inStr: 기준 이름 문자열

Returns: 고유한 이름 문자열

def compare_name(self, inObjA, inObjB): View Source

    403    def compare_name(self, inObjA, inObjB):
    404        """
    405        두 객체의 이름 비교 (정렬용)
    406
    407        Args:
    408            inObjA: 첫 번째 객체
    409            inObjB: 두 번째 객체
    410
    411        Returns:
    412            비교 결과 (inObjA.name < inObjB.name: 음수, inObjA.name == inObjB.name: 0, inObjA.name > inObjB.name: 양수)
    413        """
    414        # Python에서는 대소문자 구분 없는 비교를 위해 lower() 사용
    415        return 1 if inObjA.name.lower() > inObjB.name.lower() else -1 if inObjA.name.lower() < inObjB.name.lower() else 0

두 객체의 이름 비교 (정렬용)

Args: inObjA: 첫 번째 객체 inObjB: 두 번째 객체

Returns: 비교 결과 (inObjA.name < inObjB.name: 음수, inObjA.name == inObjB.name: 0, inObjA.name > inObjB.name: 양수)

def sort_by_name(self, inArray): View Source

    417    def sort_by_name(self, inArray):
    418        """
    419        객체 배열을 이름 기준으로 정렬
    420
    421        Args:
    422            inArray: 정렬할 객체 배열
    423
    424        Returns:
    425            이름 기준으로 정렬된 객체 배열
    426        """
    427        # Python의 sorted 함수와 key를 사용하여 이름 기준 정렬
    428        return sorted(inArray, key=lambda obj: obj.name.lower())

객체 배열을 이름 기준으로 정렬

Args: inArray: 정렬할 객체 배열

Returns: 이름 기준으로 정렬된 객체 배열

def gen_mirroring_name(self, inStr): View Source

    430    def gen_mirroring_name(self, inStr):
    431        """
    432        미러링된 이름 생성 (측면 또는 앞/뒤 변경)
    433
    434        이름에서 Side와 FrontBack namePart를 자동으로 검색하고,
    435        발견된 값의 semanticmapping weight와 가장 차이가 큰 값으로 교체합니다.
    436
    437        Args:
    438            inStr: 처리할 이름 문자열
    439
    440        Returns:
    441            미러링된 이름 문자열
    442        """
    443        return_name = super().gen_mirroring_name(inStr)
    444
    445        # 이름이 변경되지 않았다면 고유한 이름 생성
    446        if return_name == inStr:
    447            if self.has_Side(inStr) or self.has_FrontBack(inStr):
    448                return_name = self.gen_unique_name(inStr)
    449            else:
    450                return_name = self.add_suffix_to_real_name(inStr, "Mirrored")
    451
    452        return return_name

미러링된 이름 생성 (측면 또는 앞/뒤 변경)

이름에서 Side와 FrontBack namePart를 자동으로 검색하고, 발견된 값의 semanticmapping weight와 가장 차이가 큰 값으로 교체합니다.

Args: inStr: 처리할 이름 문자열

Returns: 미러링된 이름 문자열

def get_parent_value(self): View Source

    455    def get_parent_value(self):
    456        """
    457        부모 이름 문자열 반환
    458
    459        Returns:
    460            부모 이름 문자열
    461        """
    462        return self.get_name_part_value_by_description("Type", "Parent")

부모 이름 문자열 반환

Returns: 부모 이름 문자열

def get_dummy_value(self): View Source

    464    def get_dummy_value(self):
    465        """
    466        더미 이름 문자열 반환
    467
    468        Returns:
    469            더미 이름 문자열
    470        """
    471        return self.get_name_part_value_by_description("Type", "Dummy")

더미 이름 문자열 반환

Returns: 더미 이름 문자열

def get_exposeTm_value(self): View Source

    473    def get_exposeTm_value(self):
    474        """
    475        ExposeTm 이름 문자열 반환
    476
    477        Returns:
    478            ExposeTm 이름 문자열
    479        """
    480        return self.get_name_part_value_by_description("Type", "ExposeTM")

ExposeTm 이름 문자열 반환

Returns: ExposeTm 이름 문자열

def get_ik_value(self): View Source

    482    def get_ik_value(self):
    483        """
    484        IK 이름 문자열 반환
    485
    486        Returns:
    487            IK 이름 문자열
    488        """
    489        return self.get_name_part_value_by_description("Type", "IK")

IK 이름 문자열 반환

Returns: IK 이름 문자열

def get_target_value(self): View Source

    491    def get_target_value(self):
    492        """
    493        타겟 이름 문자열 반환
    494
    495        Returns:
    496            타겟 이름 문자열
    497        """
    498        return self.get_name_part_value_by_description("Type", "Target")

타겟 이름 문자열 반환

Returns: 타겟 이름 문자열

##### Inherited Members

pyjallib.naming.Naming
  get_padding_num
  get_name_part
  get_name_part_index
  get_name_part_predefined_values
  is_in_name_part_predefined_values
  get_name_part_value_by_description
  pick_name
  get_name
  combine
  get_RealName
  get_non_RealName
  convert_name_to_array
  convert_to_dictionary
  convert_to_description
  convert_to_korean_description
  has_name_part
  add_prefix_to_name_part
  add_suffix_to_name_part
  add_prefix_to_real_name
  add_suffix_to_real_name
  convert_digit_into_padding_string
  set_index_padding_num
  get_index_padding_num
  increase_index
  get_index_as_digit
  sort_by_index
  get_string
  replace_filtering_char
  replace_name_part
  remove_name_part
  load_from_config_file
  load_default_config
  get_config_path

class Anim: View Source

     16class Anim:
     17    """
     18    애니메이션 관련 기능을 제공하는 클래스.
     19    MAXScript의 _Anim 구조체 개념을 Python으로 재구현한 클래스이며, 3ds Max의 기능들을 pymxs API를 통해 제어합니다.
     20    """
     21
     22    def __init__(self):
     23        """클래스 초기화 (현재 특별한 초기화 동작은 없음)"""
     24        pass
     25
     26    def rotate_local(self, inObj, rx, ry, rz, dontAffectChildren=False):
     27        """
     28        객체를 로컬 좌표계에서 회전시킴.
     29
     30        매개변수:
     31            inObj : 회전할 객체
     32            rx    : X축 회전 각도 (도 단위)
     33            ry    : Y축 회전 각도 (도 단위)
     34            rz    : Z축 회전 각도 (도 단위)
     35        """
     36        tempParent = None
     37        tempChildren = []
     38        if dontAffectChildren:
     39            # 자식 객체에 영향을 주지 않도록 설정
     40            tempParent = inObj.parent
     41            for item in inObj.children:
     42                tempChildren.append(item)
     43            for item in tempChildren:
     44                item.parent = None
     45
     46        # 현재 객체의 변환 행렬을 가져옴
     47        currentMatrix = rt.getProperty(inObj, "transform")
     48        # 오일러 각도를 통해 회전 행렬(쿼터니언) 생성
     49        eulerAngles = rt.eulerAngles(rx, ry, rz)
     50        quatRotation = rt.eulertoquat(eulerAngles)
     51        # preRotate를 이용해 회전 적용
     52        rt.preRotate(currentMatrix, quatRotation)
     53        # 변경된 행렬을 객체에 설정
     54        rt.setProperty(inObj, "transform", currentMatrix)
     55
     56        if dontAffectChildren:
     57            # 자식 객체의 부모를 원래대로 복원
     58            for item in tempChildren:
     59                item.parent = inObj
     60            inObj.parent = tempParent
     61
     62    def move_local(self, inObj, mx, my, mz, dontAffectChildren=False):
     63        """
     64        객체를 로컬 좌표계에서 이동시킴.
     65
     66        매개변수:
     67            inObj : 이동할 객체
     68            mx    : X축 이동 거리
     69            my    : Y축 이동 거리
     70            mz    : Z축 이동 거리
     71        """
     72        tempParent = None
     73        tempChildren = []
     74        if dontAffectChildren:
     75            # 자식 객체에 영향을 주지 않도록 설정
     76            tempParent = inObj.parent
     77            for item in inObj.children:
     78                tempChildren.append(item)
     79            for item in tempChildren:
     80                item.parent = None
     81
     82        # 현재 변환 행렬 가져오기
     83        currentMatrix = rt.getProperty(inObj, "transform", dontAffectChildren=False)
     84        # 이동량을 Point3 형태로 생성
     85        translation = rt.Point3(mx, my, mz)
     86        # preTranslate를 이용해 행렬에 이동 적용
     87        rt.preTranslate(currentMatrix, translation)
     88        # 적용된 이동 변환 행렬을 객체에 설정
     89        rt.setProperty(inObj, "transform", currentMatrix)
     90
     91        if dontAffectChildren:
     92            # 자식 객체의 부모를 원래대로 복원
     93            for item in tempChildren:
     94                item.parent = inObj
     95            inObj.parent = tempParent
     96
     97    def reset_transform_controller(self, inObj):
     98        """
     99        객체의 트랜스폼 컨트롤러를 기본 상태로 재설정함.
    100
    101        매개변수:
    102            inObj : 초기화할 객체
    103        """
    104        # Biped_Object가 아닐 경우에만 실행
    105        if rt.classOf(inObj) != rt.Biped_Object:
    106            # 현재 변환 행렬 백업
    107            tempTransform = rt.getProperty(inObj, "transform")
    108            # 위치, 회전, 스케일 컨트롤러를 기본 컨트롤러로 재설정
    109            rt.setPropertyController(inObj.controller, "Position", rt.Position_XYZ())
    110            rt.setPropertyController(inObj.controller, "Rotation", rt.Euler_XYZ())
    111            rt.setPropertyController(inObj.controller, "Scale", rt.Bezier_Scale())
    112            # 백업한 행렬을 다시 객체에 할당
    113            inObj.transform = tempTransform
    114
    115    def freeze_transform(self, inObj):
    116        """
    117        객체의 변환(회전, 위치)을 키프레임에 의한 애니메이션 영향 없이 고정함.
    118
    119        매개변수:
    120            inObj : 변환을 고정할 객체
    121        """
    122        curObj = inObj
    123
    124        # 회전 컨트롤러 고정 (Rotation_list 사용)
    125        if rt.classOf(rt.getPropertyController(curObj.controller, "Rotation")) != rt.Rotation_list():
    126            rotList = rt.Rotation_list()
    127            rt.setPropertyController(curObj.controller, "Rotation", rotList)
    128            rt.setPropertyController(rotList, "Available", rt.Euler_xyz())
    129
    130            # 컨트롤러 이름 설정
    131            rotList.setname(1, "Frozen Rotation")
    132            rotList.setname(2, "Zero Euler XYZ")
    133
    134            # 활성 컨트롤러 설정
    135            rotList.setActive(2)
    136
    137        # 포지션 컨트롤러 고정 (Position_list 사용)
    138        if rt.classOf(rt.getPropertyController(curObj.controller, "position")) != rt.Position_list():
    139            posList = rt.Position_list()
    140            rt.setPropertyController(curObj.controller, "position", posList)
    141            rt.setPropertyController(posList, "Available", rt.Position_XYZ())
    142
    143            # 컨트롤러 이름 설정
    144            posList.setname(1, "Frozen Position")
    145            posList.setname(2, "Zero Position XYZ")
    146
    147            # 활성 컨트롤러 설정
    148            posList.setActive(2)
    149
    150            # 위치를 0으로 초기화
    151            zeroPosController = rt.getPropertyController(posList, "Zero Position XYZ")
    152            xPosController = rt.getPropertyController(zeroPosController, "X Position")
    153            yPosController = rt.getPropertyController(zeroPosController, "Y Position")
    154            zPosController = rt.getPropertyController(zeroPosController, "Z Position")
    155
    156            rt.setProperty(xPosController, "value", 0.0)
    157            rt.setProperty(yPosController, "value", 0.0)
    158            rt.setProperty(zPosController, "value", 0.0)
    159
    160    def collape_anim_transform(self, inObj, startFrame=None, endFrame=None):
    161        """
    162        객체의 애니메이션 변환을 병합하여 단일 트랜스폼으로 통합함.
    163
    164        매개변수:
    165            inObj      : 변환 병합 대상 객체
    166            startFrame : 시작 프레임 (기본값: 애니메이션 범위의 시작)
    167            endFrame   : 끝 프레임 (기본값: 애니메이션 범위의 끝)
    168        """
    169        # 시작과 끝 프레임이 지정되지 않은 경우 기본값 할당
    170        if startFrame is None:
    171            startFrame = int(rt.animationRange.start)
    172        if endFrame is None:
    173            endFrame = int(rt.animationRange.end)
    174
    175        # 씬 리드로우(화면 업데이트)를 중단하여 성능 최적화
    176        rt.disableSceneRedraw()
    177
    178        # 진행 상태 표시 시작
    179        progressMessage = f"Collapse transform {inObj.name}..."
    180        rt.progressStart(progressMessage, allowCancel=True)
    181
    182        # 임시 포인트 객체 생성 (중간 변환값 저장용)
    183        p = rt.Point()
    184
    185        # 각 프레임에서 대상 객체의 변환 정보를 임시 포인트에 저장
    186        for k in range(startFrame, endFrame+1):
    187            with attime(k):
    188                with animate(True):
    189                    rt.setProperty(p, "transform", rt.getProperty(inObj, "transform"))
    190
    191        # 트랜스폼 컨트롤러를 스크립트와 PRS 컨트롤러로 재설정
    192        rt.setPropertyController(inObj.controller, "Transform", rt.transform_Script())
    193        rt.setPropertyController(inObj.controller, "Transform", rt.prs())
    194
    195        # 각 프레임별로 임시 포인트와의 차이를 계산해서 최종 변환 적용
    196        for k in range(startFrame, endFrame+1):
    197            with attime(k):
    198                with animate(True):
    199                    tm = inObj.transform * rt.inverse(p.transform)
    200                    rt.setProperty(inObj, "rotation", tm.rotation)
    201                    rt.setProperty(inObj, "position", p.transform.position)
    202                    rt.setProperty(inObj, "scale", p.transform.scale)
    203
    204            # 진행 상황 업데이트 (백분율 계산)
    205            rt.progressUpdate(100 * k / endFrame)
    206
    207        # 임시 포인트 객체 삭제
    208        rt.delete(p)
    209
    210        # 진행 상태 종료 및 씬 업데이트 재활성화
    211        rt.progressEnd()
    212        rt.enableSceneRedraw()
    213
    214    def match_anim_transform(self, inObj, inTarget, startFrame=None, endFrame=None):
    215        """
    216        한 객체의 애니메이션 변환을 다른 객체의 변환과 일치시킴.
    217
    218        매개변수:
    219            inObj      : 변환을 적용할 객체
    220            inTarget   : 기준이 될 대상 객체
    221            startFrame : 시작 프레임 (기본값: 애니메이션 범위의 시작)
    222            endFrame   : 끝 프레임 (기본값: 애니메이션 범위의 끝)
    223        """
    224        # 시작/끝 프레임 기본값 설정
    225        if startFrame is None:
    226            startFrame = int(rt.animationRange.start)
    227        if endFrame is None:
    228            endFrame = int(rt.animationRange.end)
    229
    230        # 대상 객체와 기준 객체가 유효한지 확인
    231        if rt.isValidNode(inObj) and rt.isValidNode(inTarget):
    232            # 씬 업데이트 중단
    233            rt.disableSceneRedraw()
    234
    235            # 진행 상태 표시 시작
    236            progressMessage = f"Match transform {inObj.name} to {inTarget.name}"
    237            rt.progressStart(progressMessage, allowCancel=True)
    238            progressCounter = 0
    239
    240            # 임시 포인트 객체 생성 (타겟 변환 저장용)
    241            p = rt.Point()
    242
    243            # 각 프레임마다 inTarget의 변환을 저장하고 inObj의 기존 키 삭제
    244            for k in range(startFrame, endFrame + 1):
    245                with attime(k):
    246                    with animate(True):
    247                        rt.setProperty(p, "transform", rt.getProperty(inTarget, "transform"))
    248
    249                # inObj의 위치, 회전, 스케일 컨트롤러에서 기존 키 삭제
    250                inObjControllers = []
    251                inObjControllers.append(rt.getPropertyController(inObj.controller, "Position"))
    252                inObjControllers.append(rt.getPropertyController(inObj.controller, "Rotation"))
    253                inObjControllers.append(rt.getPropertyController(inObj.controller, "Scale"))
    254
    255                for controller in inObjControllers:
    256                    rt.deselectKeys(controller)
    257                    rt.selectKeys(controller, k)
    258                    rt.deleteKeys(controller, rt.Name("selection"))
    259                    rt.deselectKeys(controller)
    260
    261                progressCounter += 1
    262                if progressCounter >= 100:
    263                    progressCounter = 0
    264                rt.progressUpdate(progressCounter)
    265
    266            # 시작 프레임 이전의 불필요한 키 삭제
    267            if startFrame != rt.animationRange.start:
    268                dumPointControllers = []
    269                dumPointControllers.append(rt.getPropertyController(p.controller, "Position"))
    270                dumPointControllers.append(rt.getPropertyController(p.controller, "Rotation"))
    271                dumPointControllers.append(rt.getPropertyController(p.controller, "Scale"))
    272
    273                for controller in dumPointControllers:
    274                    rt.deselectKeys(controller)
    275                    rt.selectKeys(controller, startFrame)
    276                    rt.deleteKeys(controller, rt.Name("selection"))
    277                    rt.deselectKeys(controller)
    278
    279                progressCounter += 1
    280                if progressCounter >= 100:
    281                    progressCounter = 0
    282                rt.progressUpdate(progressCounter)
    283
    284            # inTarget의 각 컨트롤러에서 키 배열을 가져옴
    285            inTargetPosController = rt.getPropertyController(inTarget.controller, "Position")
    286            inTargetRotController = rt.getPropertyController(inTarget.controller, "Rotation")
    287            inTargetScaleController = rt.getPropertyController(inTarget.controller, "Scale")
    288
    289            posKeyArray = inTargetPosController.keys
    290            rotKeyArray = inTargetRotController.keys
    291            scaleKeyArray = inTargetScaleController.keys
    292
    293            # 시작 프레임 및 끝 프레임의 변환 적용
    294            with attime(startFrame):
    295                with animate(True):
    296                    rt.setProperty(inObj, "transform", rt.getProperty(p, "transform"))
    297            with attime(endFrame):
    298                with animate(True):
    299                    rt.setProperty(inObj, "transform", rt.getProperty(p, "transform"))
    300
    301            # 위치 키프레임 적용
    302            for key in posKeyArray:
    303                keyTime = int(rt.getProperty(key, "time"))
    304                if keyTime >= startFrame and keyTime <= endFrame:
    305                    with attime(keyTime):
    306                        with animate(True):
    307                            rt.setProperty(inObj, "transform", rt.getProperty(p, "transform"))
    308                progressCounter += 1
    309                if progressCounter >= 100:
    310                    progressCounter = 0
    311                rt.progressUpdate(progressCounter)
    312
    313            # 회전 키프레임 적용
    314            for key in rotKeyArray:
    315                keyTime = int(rt.getProperty(key, "time"))
    316                if keyTime >= startFrame and keyTime <= endFrame:
    317                    with attime(keyTime):
    318                        with animate(True):
    319                            rt.setProperty(inObj, "transform", rt.getProperty(p, "transform"))
    320                progressCounter += 1
    321                if progressCounter >= 100:
    322                    progressCounter = 0
    323                rt.progressUpdate(progressCounter)
    324
    325            # 스케일 키프레임 적용
    326            for key in scaleKeyArray:
    327                keyTime = int(rt.getProperty(key, "time"))
    328                if keyTime >= startFrame and keyTime <= endFrame:
    329                    with attime(keyTime):
    330                        with animate(True):
    331                            rt.setProperty(inObj, "transform", rt.getProperty(p, "transform"))
    332                progressCounter += 1
    333                if progressCounter >= 100:
    334                    progressCounter = 0
    335                rt.progressUpdate(progressCounter)
    336
    337            # 임시 포인트 객체 삭제
    338            rt.delete(p)
    339
    340            # 진행 상태 100% 업데이트 후 종료
    341            rt.progressUpdate(100)
    342            rt.progressEnd()
    343            rt.enableSceneRedraw()
    344
    345    def create_average_pos_transform(self, inTargetArray):
    346        """
    347        여러 객체들의 평균 위치를 계산하여 단일 변환 행렬을 생성함.
    348
    349        매개변수:
    350            inTargetArray : 평균 위치 계산 대상 객체 배열
    351
    352        반환:
    353            계산된 평균 위치를 적용한 변환 행렬
    354        """
    355        # 임시 포인트 객체 생성
    356        posConstDum = rt.Point()
    357
    358        # 포지션 제약 컨트롤러 생성
    359        targetPosConstraint = rt.Position_Constraint()
    360
    361        # 대상 객체에 동일 가중치 부여 (전체 100%)
    362        targetWeight = 100.0 / (len(inTargetArray) + 1)
    363
    364        # 제약 컨트롤러를 임시 객체에 할당
    365        rt.setPropertyController(posConstDum.controller, "Position", targetPosConstraint)
    366
    367        # 각 대상 객체를 제약에 추가
    368        for item in inTargetArray:
    369            targetPosConstraint.appendTarget(item, targetWeight)
    370
    371        # 계산된 변환 값을 복사
    372        returnTransform = rt.copy(rt.getProperty(posConstDum, "transform"))
    373
    374        # 임시 객체 삭제
    375        rt.delete(posConstDum)
    376
    377        return returnTransform
    378
    379    def create_average_rot_transform(self, inTargetArray):
    380        """
    381        여러 객체들의 평균 회전을 계산하여 단일 변환 행렬을 생성함.
    382
    383        매개변수:
    384            inTargetArray : 평균 회전 계산 대상 객체 배열
    385
    386        반환:
    387            계산된 평균 회전을 적용한 변환 행렬
    388        """
    389        # 임시 포인트 객체 생성
    390        rotConstDum = rt.Point()
    391
    392        # 방향(회전) 제약 컨트롤러 생성
    393        targetOriConstraint = rt.Orientation_Constraint()
    394
    395        # 대상 객체에 동일 가중치 부여
    396        targetWeight = 100.0 / (len(inTargetArray) + 1)
    397
    398        # 회전 제약 컨트롤러를 임시 객체에 할당
    399        rt.setPropertyController(rotConstDum.controller, "Rotation", targetOriConstraint)
    400
    401        # 각 대상 객체를 제약에 추가
    402        for item in inTargetArray:
    403            targetOriConstraint.appendTarget(item, targetWeight)
    404
    405        # 계산된 변환 값을 복사
    406        returnTransform = rt.copy(rt.getProperty(rotConstDum, "transform"))
    407
    408        # 임시 객체 삭제
    409        rt.delete(rotConstDum)
    410
    411        return returnTransform
    412
    413    def get_all_keys_in_controller(self, inController, keys_list):
    414        """
    415        주어진 컨트롤러와 그 하위 컨트롤러에서 모든 키프레임을 재귀적으로 수집함.
    416
    417        매개변수:
    418            inController : 키프레임 검색 대상 컨트롤러 객체
    419            keys_list    : 수집된 키프레임들을 저장할 리스트 (참조로 전달)
    420        """
    421        with undo(False):
    422            # 현재 컨트롤러에 키프레임이 있으면 리스트에 추가
    423            if rt.isProperty(inController, 'keys'):
    424                for k in inController.keys:
    425                    keys_list.append(k)
    426
    427            # 하위 컨트롤러에 대해서 재귀적으로 검색
    428            for i in range(inController.numSubs):
    429                sub_controller = inController[i]  # 1부터 시작하는 인덱스
    430                if sub_controller:
    431                    self.get_all_keys_in_controller(sub_controller, keys_list)
    432
    433    def get_all_keys(self, inObj):
    434        """
    435        객체에 적용된 모든 키프레임을 가져옴.
    436
    437        매개변수:
    438            inObj : 키프레임을 검색할 객체
    439
    440        반환:
    441            객체에 적용된 키프레임들의 리스트
    442        """
    443        with undo(False):
    444            keys_list = []
    445            if rt.isValidNode(inObj):
    446                self.get_all_keys_in_controller(inObj.controller, keys_list)
    447            return keys_list
    448
    449    def get_start_end_keys(self, inObj):
    450        """
    451        객체의 키프레임 중 가장 먼저와 마지막 키프레임을 찾음.
    452
    453        매개변수:
    454            inObj : 키프레임을 검색할 객체
    455
    456        반환:
    457            [시작 키프레임, 끝 키프레임] (키가 없으면 빈 리스트 반환)
    458        """
    459        with undo(False):
    460            keys = self.get_all_keys(inObj)
    461            if keys and len(keys) > 0:
    462                # 각 키의 시간값을 추출하여 최소, 최대값 확인
    463                keyTimes = [key.time for key in keys]
    464                minTime = rt.amin(keyTimes)
    465                maxTime = rt.amax(keyTimes)
    466                minIndex = keyTimes.index(minTime)
    467                maxIndex = keyTimes.index(maxTime)
    468                return [rt.amin(minIndex), rt.amax(maxIndex)]
    469            else:
    470                return []
    471
    472    def delete_all_keys(self, inObj):
    473        """
    474        객체에 적용된 모든 키프레임을 삭제함.
    475
    476        매개변수:
    477            inObj : 삭제 대상 객체
    478        """
    479        rt.deleteKeys(inObj, rt.Name('allKeys'))
    480
    481    def is_node_animated(self, node):
    482        """
    483        객체 및 그 하위 요소(애니메이션, 커스텀 속성 등)가 애니메이션 되었는지 재귀적으로 확인함.
    484
    485        매개변수:
    486            node : 애니메이션 여부를 확인할 객체 또는 서브 애니메이션
    487
    488        반환:
    489            True  : 애니메이션이 적용된 경우
    490            False : 애니메이션이 없는 경우
    491        """
    492        animated = False
    493        obj = node
    494
    495        # SubAnim인 경우 키프레임 여부 확인
    496        if rt.isKindOf(node, rt.SubAnim):
    497            if node.keys and len(node.keys) > 0:
    498                animated = True
    499            obj = node.object
    500
    501        # MaxWrapper인 경우 커스텀 속성에 대해 확인
    502        if rt.isKindOf(obj, rt.MaxWrapper):
    503            for ca in obj.custAttributes:
    504                animated = self.is_node_animated(ca)
    505                if animated:
    506                    break
    507
    508        # 하위 애니메이션에 대해 재귀적으로 검사
    509        for i in range(node.numSubs):
    510            animated = self.is_node_animated(node[i])
    511            if animated:
    512                break
    513
    514        return animated
    515
    516    def find_animated_nodes(self, nodes=None):
    517        """
    518        애니메이션이 적용된 객체들을 모두 찾음.
    519
    520        매개변수:
    521            nodes : 검색 대상 객체 리스트 (None이면 전체 객체)
    522
    523        반환:
    524            애니메이션이 적용된 객체들의 리스트
    525        """
    526        if nodes is None:
    527            nodes = rt.objects
    528
    529        result = []
    530        for node in nodes:
    531            if self.is_node_animated(node):
    532                result.append(node)
    533
    534        return result
    535
    536    def find_animated_material_nodes(self, nodes=None):
    537        """
    538        애니메이션이 적용된 재질을 가진 객체들을 모두 찾음.
    539
    540        매개변수:
    541            nodes : 검색 대상 객체 리스트 (None이면 전체 객체)
    542
    543        반환:
    544            애니메이션이 적용된 재질을 가진 객체들의 리스트
    545        """
    546        if nodes is None:
    547            nodes = rt.objects
    548
    549        result = []
    550        for node in nodes:
    551            mat = rt.getProperty(node, "material")
    552            if mat is not None and self.is_node_animated(mat):
    553                result.append(node)
    554
    555        return result
    556
    557    def find_animated_transform_nodes(self, nodes=None):
    558        """
    559        애니메이션이 적용된 변환 정보를 가진 객체들을 모두 찾음.
    560
    561        매개변수:
    562            nodes : 검색 대상 객체 리스트 (None이면 전체 객체)
    563
    564        반환:
    565            애니메이션이 적용된 변환 데이터를 가진 객체들의 리스트
    566        """
    567        if nodes is None:
    568            nodes = rt.objects
    569
    570        result = []
    571        for node in nodes:
    572            controller = rt.getProperty(node, "controller")
    573            if self.is_node_animated(controller):
    574                result.append(node)
    575
    576        return result
    577
    578    def save_xform(self, inObj):
    579        """
    580        객체의 현재 변환 행렬(월드, 부모 스페이스)을 저장하여 복원을 가능하게 함.
    581
    582        매개변수:
    583            inObj : 변환 값을 저장할 객체
    584        """
    585        # 월드 스페이스 행렬 저장
    586        transformString = str(inObj.transform)
    587        rt.setUserProp(inObj, rt.Name("WorldSpaceMatrix"), transformString)
    588
    589        # 부모가 존재하면 부모 스페이스 행렬도 저장
    590        parent = inObj.parent
    591        if parent is not None:
    592            parentTransform = parent.transform
    593            inverseParent = rt.inverse(parentTransform)
    594            objTransform = inObj.transform
    595            parentSpaceMatrix = objTransform * inverseParent
    596            rt.setUserProp(inObj, rt.Name("ParentSpaceMatrix"), str(parentSpaceMatrix))
    597
    598    def set_xform(self, inObj, space="World"):
    599        """
    600        저장된 변환 행렬을 객체에 적용함.
    601
    602        매개변수:
    603            inObj : 변환 값을 적용할 객체
    604            space : "World" 또는 "Parent" (적용할 변환 공간)
    605        """
    606        if space == "World":
    607            # 월드 스페이스 행렬 적용
    608            matrixString = rt.getUserProp(inObj, rt.Name("WorldSpaceMatrix"))
    609            transformMatrix = rt.execute(matrixString)
    610            rt.setProperty(inObj, "transform", transformMatrix)
    611        elif space == "Parent":
    612            # 부모 스페이스 행렬 적용
    613            parent = inObj.parent
    614            matrixString = rt.getUserProp(inObj, rt.Name("ParentSpaceMatrix"))
    615            parentSpaceMatrix = rt.execute(matrixString)
    616            if parent is not None:
    617                parentTransform = parent.transform
    618                transformMatrix = parentSpaceMatrix * parentTransform
    619                rt.setProperty(inObj, "transform", transformMatrix)

애니메이션 관련 기능을 제공하는 클래스. MAXScript의 _Anim 구조체 개념을 Python으로 재구현한 클래스이며, 3ds Max의 기능들을 pymxs API를 통해 제어합니다.

Anim() View Source

    22    def __init__(self):
    23        """클래스 초기화 (현재 특별한 초기화 동작은 없음)"""
    24        pass

클래스 초기화 (현재 특별한 초기화 동작은 없음)

def rotate_local(self, inObj, rx, ry, rz, dontAffectChildren=False): View Source

    26    def rotate_local(self, inObj, rx, ry, rz, dontAffectChildren=False):
    27        """
    28        객체를 로컬 좌표계에서 회전시킴.
    29
    30        매개변수:
    31            inObj : 회전할 객체
    32            rx    : X축 회전 각도 (도 단위)
    33            ry    : Y축 회전 각도 (도 단위)
    34            rz    : Z축 회전 각도 (도 단위)
    35        """
    36        tempParent = None
    37        tempChildren = []
    38        if dontAffectChildren:
    39            # 자식 객체에 영향을 주지 않도록 설정
    40            tempParent = inObj.parent
    41            for item in inObj.children:
    42                tempChildren.append(item)
    43            for item in tempChildren:
    44                item.parent = None
    45
    46        # 현재 객체의 변환 행렬을 가져옴
    47        currentMatrix = rt.getProperty(inObj, "transform")
    48        # 오일러 각도를 통해 회전 행렬(쿼터니언) 생성
    49        eulerAngles = rt.eulerAngles(rx, ry, rz)
    50        quatRotation = rt.eulertoquat(eulerAngles)
    51        # preRotate를 이용해 회전 적용
    52        rt.preRotate(currentMatrix, quatRotation)
    53        # 변경된 행렬을 객체에 설정
    54        rt.setProperty(inObj, "transform", currentMatrix)
    55
    56        if dontAffectChildren:
    57            # 자식 객체의 부모를 원래대로 복원
    58            for item in tempChildren:
    59                item.parent = inObj
    60            inObj.parent = tempParent

객체를 로컬 좌표계에서 회전시킴.

매개변수: inObj : 회전할 객체 rx : X축 회전 각도 (도 단위) ry : Y축 회전 각도 (도 단위) rz : Z축 회전 각도 (도 단위)

def move_local(self, inObj, mx, my, mz, dontAffectChildren=False): View Source

    62    def move_local(self, inObj, mx, my, mz, dontAffectChildren=False):
    63        """
    64        객체를 로컬 좌표계에서 이동시킴.
    65
    66        매개변수:
    67            inObj : 이동할 객체
    68            mx    : X축 이동 거리
    69            my    : Y축 이동 거리
    70            mz    : Z축 이동 거리
    71        """
    72        tempParent = None
    73        tempChildren = []
    74        if dontAffectChildren:
    75            # 자식 객체에 영향을 주지 않도록 설정
    76            tempParent = inObj.parent
    77            for item in inObj.children:
    78                tempChildren.append(item)
    79            for item in tempChildren:
    80                item.parent = None
    81
    82        # 현재 변환 행렬 가져오기
    83        currentMatrix = rt.getProperty(inObj, "transform", dontAffectChildren=False)
    84        # 이동량을 Point3 형태로 생성
    85        translation = rt.Point3(mx, my, mz)
    86        # preTranslate를 이용해 행렬에 이동 적용
    87        rt.preTranslate(currentMatrix, translation)
    88        # 적용된 이동 변환 행렬을 객체에 설정
    89        rt.setProperty(inObj, "transform", currentMatrix)
    90
    91        if dontAffectChildren:
    92            # 자식 객체의 부모를 원래대로 복원
    93            for item in tempChildren:
    94                item.parent = inObj
    95            inObj.parent = tempParent

객체를 로컬 좌표계에서 이동시킴.

매개변수: inObj : 이동할 객체 mx : X축 이동 거리 my : Y축 이동 거리 mz : Z축 이동 거리

def reset_transform_controller(self, inObj): View Source

     97    def reset_transform_controller(self, inObj):
     98        """
     99        객체의 트랜스폼 컨트롤러를 기본 상태로 재설정함.
    100
    101        매개변수:
    102            inObj : 초기화할 객체
    103        """
    104        # Biped_Object가 아닐 경우에만 실행
    105        if rt.classOf(inObj) != rt.Biped_Object:
    106            # 현재 변환 행렬 백업
    107            tempTransform = rt.getProperty(inObj, "transform")
    108            # 위치, 회전, 스케일 컨트롤러를 기본 컨트롤러로 재설정
    109            rt.setPropertyController(inObj.controller, "Position", rt.Position_XYZ())
    110            rt.setPropertyController(inObj.controller, "Rotation", rt.Euler_XYZ())
    111            rt.setPropertyController(inObj.controller, "Scale", rt.Bezier_Scale())
    112            # 백업한 행렬을 다시 객체에 할당
    113            inObj.transform = tempTransform

객체의 트랜스폼 컨트롤러를 기본 상태로 재설정함.

매개변수: inObj : 초기화할 객체

def freeze_transform(self, inObj): View Source

    115    def freeze_transform(self, inObj):
    116        """
    117        객체의 변환(회전, 위치)을 키프레임에 의한 애니메이션 영향 없이 고정함.
    118
    119        매개변수:
    120            inObj : 변환을 고정할 객체
    121        """
    122        curObj = inObj
    123
    124        # 회전 컨트롤러 고정 (Rotation_list 사용)
    125        if rt.classOf(rt.getPropertyController(curObj.controller, "Rotation")) != rt.Rotation_list():
    126            rotList = rt.Rotation_list()
    127            rt.setPropertyController(curObj.controller, "Rotation", rotList)
    128            rt.setPropertyController(rotList, "Available", rt.Euler_xyz())
    129
    130            # 컨트롤러 이름 설정
    131            rotList.setname(1, "Frozen Rotation")
    132            rotList.setname(2, "Zero Euler XYZ")
    133
    134            # 활성 컨트롤러 설정
    135            rotList.setActive(2)
    136
    137        # 포지션 컨트롤러 고정 (Position_list 사용)
    138        if rt.classOf(rt.getPropertyController(curObj.controller, "position")) != rt.Position_list():
    139            posList = rt.Position_list()
    140            rt.setPropertyController(curObj.controller, "position", posList)
    141            rt.setPropertyController(posList, "Available", rt.Position_XYZ())
    142
    143            # 컨트롤러 이름 설정
    144            posList.setname(1, "Frozen Position")
    145            posList.setname(2, "Zero Position XYZ")
    146
    147            # 활성 컨트롤러 설정
    148            posList.setActive(2)
    149
    150            # 위치를 0으로 초기화
    151            zeroPosController = rt.getPropertyController(posList, "Zero Position XYZ")
    152            xPosController = rt.getPropertyController(zeroPosController, "X Position")
    153            yPosController = rt.getPropertyController(zeroPosController, "Y Position")
    154            zPosController = rt.getPropertyController(zeroPosController, "Z Position")
    155
    156            rt.setProperty(xPosController, "value", 0.0)
    157            rt.setProperty(yPosController, "value", 0.0)
    158            rt.setProperty(zPosController, "value", 0.0)

객체의 변환(회전, 위치)을 키프레임에 의한 애니메이션 영향 없이 고정함.

매개변수: inObj : 변환을 고정할 객체

def collape_anim_transform(self, inObj, startFrame=None, endFrame=None): View Source

    160    def collape_anim_transform(self, inObj, startFrame=None, endFrame=None):
    161        """
    162        객체의 애니메이션 변환을 병합하여 단일 트랜스폼으로 통합함.
    163
    164        매개변수:
    165            inObj      : 변환 병합 대상 객체
    166            startFrame : 시작 프레임 (기본값: 애니메이션 범위의 시작)
    167            endFrame   : 끝 프레임 (기본값: 애니메이션 범위의 끝)
    168        """
    169        # 시작과 끝 프레임이 지정되지 않은 경우 기본값 할당
    170        if startFrame is None:
    171            startFrame = int(rt.animationRange.start)
    172        if endFrame is None:
    173            endFrame = int(rt.animationRange.end)
    174
    175        # 씬 리드로우(화면 업데이트)를 중단하여 성능 최적화
    176        rt.disableSceneRedraw()
    177
    178        # 진행 상태 표시 시작
    179        progressMessage = f"Collapse transform {inObj.name}..."
    180        rt.progressStart(progressMessage, allowCancel=True)
    181
    182        # 임시 포인트 객체 생성 (중간 변환값 저장용)
    183        p = rt.Point()
    184
    185        # 각 프레임에서 대상 객체의 변환 정보를 임시 포인트에 저장
    186        for k in range(startFrame, endFrame+1):
    187            with attime(k):
    188                with animate(True):
    189                    rt.setProperty(p, "transform", rt.getProperty(inObj, "transform"))
    190
    191        # 트랜스폼 컨트롤러를 스크립트와 PRS 컨트롤러로 재설정
    192        rt.setPropertyController(inObj.controller, "Transform", rt.transform_Script())
    193        rt.setPropertyController(inObj.controller, "Transform", rt.prs())
    194
    195        # 각 프레임별로 임시 포인트와의 차이를 계산해서 최종 변환 적용
    196        for k in range(startFrame, endFrame+1):
    197            with attime(k):
    198                with animate(True):
    199                    tm = inObj.transform * rt.inverse(p.transform)
    200                    rt.setProperty(inObj, "rotation", tm.rotation)
    201                    rt.setProperty(inObj, "position", p.transform.position)
    202                    rt.setProperty(inObj, "scale", p.transform.scale)
    203
    204            # 진행 상황 업데이트 (백분율 계산)
    205            rt.progressUpdate(100 * k / endFrame)
    206
    207        # 임시 포인트 객체 삭제
    208        rt.delete(p)
    209
    210        # 진행 상태 종료 및 씬 업데이트 재활성화
    211        rt.progressEnd()
    212        rt.enableSceneRedraw()

객체의 애니메이션 변환을 병합하여 단일 트랜스폼으로 통합함.

매개변수: inObj : 변환 병합 대상 객체 startFrame : 시작 프레임 (기본값: 애니메이션 범위의 시작) endFrame : 끝 프레임 (기본값: 애니메이션 범위의 끝)

def match_anim_transform(self, inObj, inTarget, startFrame=None, endFrame=None): View Source

    214    def match_anim_transform(self, inObj, inTarget, startFrame=None, endFrame=None):
    215        """
    216        한 객체의 애니메이션 변환을 다른 객체의 변환과 일치시킴.
    217
    218        매개변수:
    219            inObj      : 변환을 적용할 객체
    220            inTarget   : 기준이 될 대상 객체
    221            startFrame : 시작 프레임 (기본값: 애니메이션 범위의 시작)
    222            endFrame   : 끝 프레임 (기본값: 애니메이션 범위의 끝)
    223        """
    224        # 시작/끝 프레임 기본값 설정
    225        if startFrame is None:
    226            startFrame = int(rt.animationRange.start)
    227        if endFrame is None:
    228            endFrame = int(rt.animationRange.end)
    229
    230        # 대상 객체와 기준 객체가 유효한지 확인
    231        if rt.isValidNode(inObj) and rt.isValidNode(inTarget):
    232            # 씬 업데이트 중단
    233            rt.disableSceneRedraw()
    234
    235            # 진행 상태 표시 시작
    236            progressMessage = f"Match transform {inObj.name} to {inTarget.name}"
    237            rt.progressStart(progressMessage, allowCancel=True)
    238            progressCounter = 0
    239
    240            # 임시 포인트 객체 생성 (타겟 변환 저장용)
    241            p = rt.Point()
    242
    243            # 각 프레임마다 inTarget의 변환을 저장하고 inObj의 기존 키 삭제
    244            for k in range(startFrame, endFrame + 1):
    245                with attime(k):
    246                    with animate(True):
    247                        rt.setProperty(p, "transform", rt.getProperty(inTarget, "transform"))
    248
    249                # inObj의 위치, 회전, 스케일 컨트롤러에서 기존 키 삭제
    250                inObjControllers = []
    251                inObjControllers.append(rt.getPropertyController(inObj.controller, "Position"))
    252                inObjControllers.append(rt.getPropertyController(inObj.controller, "Rotation"))
    253                inObjControllers.append(rt.getPropertyController(inObj.controller, "Scale"))
    254
    255                for controller in inObjControllers:
    256                    rt.deselectKeys(controller)
    257                    rt.selectKeys(controller, k)
    258                    rt.deleteKeys(controller, rt.Name("selection"))
    259                    rt.deselectKeys(controller)
    260
    261                progressCounter += 1
    262                if progressCounter >= 100:
    263                    progressCounter = 0
    264                rt.progressUpdate(progressCounter)
    265
    266            # 시작 프레임 이전의 불필요한 키 삭제
    267            if startFrame != rt.animationRange.start:
    268                dumPointControllers = []
    269                dumPointControllers.append(rt.getPropertyController(p.controller, "Position"))
    270                dumPointControllers.append(rt.getPropertyController(p.controller, "Rotation"))
    271                dumPointControllers.append(rt.getPropertyController(p.controller, "Scale"))
    272
    273                for controller in dumPointControllers:
    274                    rt.deselectKeys(controller)
    275                    rt.selectKeys(controller, startFrame)
    276                    rt.deleteKeys(controller, rt.Name("selection"))
    277                    rt.deselectKeys(controller)
    278
    279                progressCounter += 1
    280                if progressCounter >= 100:
    281                    progressCounter = 0
    282                rt.progressUpdate(progressCounter)
    283
    284            # inTarget의 각 컨트롤러에서 키 배열을 가져옴
    285            inTargetPosController = rt.getPropertyController(inTarget.controller, "Position")
    286            inTargetRotController = rt.getPropertyController(inTarget.controller, "Rotation")
    287            inTargetScaleController = rt.getPropertyController(inTarget.controller, "Scale")
    288
    289            posKeyArray = inTargetPosController.keys
    290            rotKeyArray = inTargetRotController.keys
    291            scaleKeyArray = inTargetScaleController.keys
    292
    293            # 시작 프레임 및 끝 프레임의 변환 적용
    294            with attime(startFrame):
    295                with animate(True):
    296                    rt.setProperty(inObj, "transform", rt.getProperty(p, "transform"))
    297            with attime(endFrame):
    298                with animate(True):
    299                    rt.setProperty(inObj, "transform", rt.getProperty(p, "transform"))
    300
    301            # 위치 키프레임 적용
    302            for key in posKeyArray:
    303                keyTime = int(rt.getProperty(key, "time"))
    304                if keyTime >= startFrame and keyTime <= endFrame:
    305                    with attime(keyTime):
    306                        with animate(True):
    307                            rt.setProperty(inObj, "transform", rt.getProperty(p, "transform"))
    308                progressCounter += 1
    309                if progressCounter >= 100:
    310                    progressCounter = 0
    311                rt.progressUpdate(progressCounter)
    312
    313            # 회전 키프레임 적용
    314            for key in rotKeyArray:
    315                keyTime = int(rt.getProperty(key, "time"))
    316                if keyTime >= startFrame and keyTime <= endFrame:
    317                    with attime(keyTime):
    318                        with animate(True):
    319                            rt.setProperty(inObj, "transform", rt.getProperty(p, "transform"))
    320                progressCounter += 1
    321                if progressCounter >= 100:
    322                    progressCounter = 0
    323                rt.progressUpdate(progressCounter)
    324
    325            # 스케일 키프레임 적용
    326            for key in scaleKeyArray:
    327                keyTime = int(rt.getProperty(key, "time"))
    328                if keyTime >= startFrame and keyTime <= endFrame:
    329                    with attime(keyTime):
    330                        with animate(True):
    331                            rt.setProperty(inObj, "transform", rt.getProperty(p, "transform"))
    332                progressCounter += 1
    333                if progressCounter >= 100:
    334                    progressCounter = 0
    335                rt.progressUpdate(progressCounter)
    336
    337            # 임시 포인트 객체 삭제
    338            rt.delete(p)
    339
    340            # 진행 상태 100% 업데이트 후 종료
    341            rt.progressUpdate(100)
    342            rt.progressEnd()
    343            rt.enableSceneRedraw()

한 객체의 애니메이션 변환을 다른 객체의 변환과 일치시킴.

매개변수: inObj : 변환을 적용할 객체 inTarget : 기준이 될 대상 객체 startFrame : 시작 프레임 (기본값: 애니메이션 범위의 시작) endFrame : 끝 프레임 (기본값: 애니메이션 범위의 끝)

def create_average_pos_transform(self, inTargetArray): View Source

    345    def create_average_pos_transform(self, inTargetArray):
    346        """
    347        여러 객체들의 평균 위치를 계산하여 단일 변환 행렬을 생성함.
    348
    349        매개변수:
    350            inTargetArray : 평균 위치 계산 대상 객체 배열
    351
    352        반환:
    353            계산된 평균 위치를 적용한 변환 행렬
    354        """
    355        # 임시 포인트 객체 생성
    356        posConstDum = rt.Point()
    357
    358        # 포지션 제약 컨트롤러 생성
    359        targetPosConstraint = rt.Position_Constraint()
    360
    361        # 대상 객체에 동일 가중치 부여 (전체 100%)
    362        targetWeight = 100.0 / (len(inTargetArray) + 1)
    363
    364        # 제약 컨트롤러를 임시 객체에 할당
    365        rt.setPropertyController(posConstDum.controller, "Position", targetPosConstraint)
    366
    367        # 각 대상 객체를 제약에 추가
    368        for item in inTargetArray:
    369            targetPosConstraint.appendTarget(item, targetWeight)
    370
    371        # 계산된 변환 값을 복사
    372        returnTransform = rt.copy(rt.getProperty(posConstDum, "transform"))
    373
    374        # 임시 객체 삭제
    375        rt.delete(posConstDum)
    376
    377        return returnTransform

여러 객체들의 평균 위치를 계산하여 단일 변환 행렬을 생성함.

매개변수: inTargetArray : 평균 위치 계산 대상 객체 배열

반환: 계산된 평균 위치를 적용한 변환 행렬

def create_average_rot_transform(self, inTargetArray): View Source

    379    def create_average_rot_transform(self, inTargetArray):
    380        """
    381        여러 객체들의 평균 회전을 계산하여 단일 변환 행렬을 생성함.
    382
    383        매개변수:
    384            inTargetArray : 평균 회전 계산 대상 객체 배열
    385
    386        반환:
    387            계산된 평균 회전을 적용한 변환 행렬
    388        """
    389        # 임시 포인트 객체 생성
    390        rotConstDum = rt.Point()
    391
    392        # 방향(회전) 제약 컨트롤러 생성
    393        targetOriConstraint = rt.Orientation_Constraint()
    394
    395        # 대상 객체에 동일 가중치 부여
    396        targetWeight = 100.0 / (len(inTargetArray) + 1)
    397
    398        # 회전 제약 컨트롤러를 임시 객체에 할당
    399        rt.setPropertyController(rotConstDum.controller, "Rotation", targetOriConstraint)
    400
    401        # 각 대상 객체를 제약에 추가
    402        for item in inTargetArray:
    403            targetOriConstraint.appendTarget(item, targetWeight)
    404
    405        # 계산된 변환 값을 복사
    406        returnTransform = rt.copy(rt.getProperty(rotConstDum, "transform"))
    407
    408        # 임시 객체 삭제
    409        rt.delete(rotConstDum)
    410
    411        return returnTransform

여러 객체들의 평균 회전을 계산하여 단일 변환 행렬을 생성함.

매개변수: inTargetArray : 평균 회전 계산 대상 객체 배열

반환: 계산된 평균 회전을 적용한 변환 행렬

def get_all_keys_in_controller(self, inController, keys_list): View Source

    413    def get_all_keys_in_controller(self, inController, keys_list):
    414        """
    415        주어진 컨트롤러와 그 하위 컨트롤러에서 모든 키프레임을 재귀적으로 수집함.
    416
    417        매개변수:
    418            inController : 키프레임 검색 대상 컨트롤러 객체
    419            keys_list    : 수집된 키프레임들을 저장할 리스트 (참조로 전달)
    420        """
    421        with undo(False):
    422            # 현재 컨트롤러에 키프레임이 있으면 리스트에 추가
    423            if rt.isProperty(inController, 'keys'):
    424                for k in inController.keys:
    425                    keys_list.append(k)
    426
    427            # 하위 컨트롤러에 대해서 재귀적으로 검색
    428            for i in range(inController.numSubs):
    429                sub_controller = inController[i]  # 1부터 시작하는 인덱스
    430                if sub_controller:
    431                    self.get_all_keys_in_controller(sub_controller, keys_list)

주어진 컨트롤러와 그 하위 컨트롤러에서 모든 키프레임을 재귀적으로 수집함.

매개변수: inController : 키프레임 검색 대상 컨트롤러 객체 keys_list : 수집된 키프레임들을 저장할 리스트 (참조로 전달)

def get_all_keys(self, inObj): View Source

    433    def get_all_keys(self, inObj):
    434        """
    435        객체에 적용된 모든 키프레임을 가져옴.
    436
    437        매개변수:
    438            inObj : 키프레임을 검색할 객체
    439
    440        반환:
    441            객체에 적용된 키프레임들의 리스트
    442        """
    443        with undo(False):
    444            keys_list = []
    445            if rt.isValidNode(inObj):
    446                self.get_all_keys_in_controller(inObj.controller, keys_list)
    447            return keys_list

객체에 적용된 모든 키프레임을 가져옴.

매개변수: inObj : 키프레임을 검색할 객체

반환: 객체에 적용된 키프레임들의 리스트

def get_start_end_keys(self, inObj): View Source

    449    def get_start_end_keys(self, inObj):
    450        """
    451        객체의 키프레임 중 가장 먼저와 마지막 키프레임을 찾음.
    452
    453        매개변수:
    454            inObj : 키프레임을 검색할 객체
    455
    456        반환:
    457            [시작 키프레임, 끝 키프레임] (키가 없으면 빈 리스트 반환)
    458        """
    459        with undo(False):
    460            keys = self.get_all_keys(inObj)
    461            if keys and len(keys) > 0:
    462                # 각 키의 시간값을 추출하여 최소, 최대값 확인
    463                keyTimes = [key.time for key in keys]
    464                minTime = rt.amin(keyTimes)
    465                maxTime = rt.amax(keyTimes)
    466                minIndex = keyTimes.index(minTime)
    467                maxIndex = keyTimes.index(maxTime)
    468                return [rt.amin(minIndex), rt.amax(maxIndex)]
    469            else:
    470                return []

객체의 키프레임 중 가장 먼저와 마지막 키프레임을 찾음.

매개변수: inObj : 키프레임을 검색할 객체

반환: [시작 키프레임, 끝 키프레임] (키가 없으면 빈 리스트 반환)

def delete_all_keys(self, inObj): View Source

    472    def delete_all_keys(self, inObj):
    473        """
    474        객체에 적용된 모든 키프레임을 삭제함.
    475
    476        매개변수:
    477            inObj : 삭제 대상 객체
    478        """
    479        rt.deleteKeys(inObj, rt.Name('allKeys'))

객체에 적용된 모든 키프레임을 삭제함.

매개변수: inObj : 삭제 대상 객체

def is_node_animated(self, node): View Source

    481    def is_node_animated(self, node):
    482        """
    483        객체 및 그 하위 요소(애니메이션, 커스텀 속성 등)가 애니메이션 되었는지 재귀적으로 확인함.
    484
    485        매개변수:
    486            node : 애니메이션 여부를 확인할 객체 또는 서브 애니메이션
    487
    488        반환:
    489            True  : 애니메이션이 적용된 경우
    490            False : 애니메이션이 없는 경우
    491        """
    492        animated = False
    493        obj = node
    494
    495        # SubAnim인 경우 키프레임 여부 확인
    496        if rt.isKindOf(node, rt.SubAnim):
    497            if node.keys and len(node.keys) > 0:
    498                animated = True
    499            obj = node.object
    500
    501        # MaxWrapper인 경우 커스텀 속성에 대해 확인
    502        if rt.isKindOf(obj, rt.MaxWrapper):
    503            for ca in obj.custAttributes:
    504                animated = self.is_node_animated(ca)
    505                if animated:
    506                    break
    507
    508        # 하위 애니메이션에 대해 재귀적으로 검사
    509        for i in range(node.numSubs):
    510            animated = self.is_node_animated(node[i])
    511            if animated:
    512                break
    513
    514        return animated

객체 및 그 하위 요소(애니메이션, 커스텀 속성 등)가 애니메이션 되었는지 재귀적으로 확인함.

매개변수: node : 애니메이션 여부를 확인할 객체 또는 서브 애니메이션

반환: True : 애니메이션이 적용된 경우 False : 애니메이션이 없는 경우

def find_animated_nodes(self, nodes=None): View Source

    516    def find_animated_nodes(self, nodes=None):
    517        """
    518        애니메이션이 적용된 객체들을 모두 찾음.
    519
    520        매개변수:
    521            nodes : 검색 대상 객체 리스트 (None이면 전체 객체)
    522
    523        반환:
    524            애니메이션이 적용된 객체들의 리스트
    525        """
    526        if nodes is None:
    527            nodes = rt.objects
    528
    529        result = []
    530        for node in nodes:
    531            if self.is_node_animated(node):
    532                result.append(node)
    533
    534        return result

애니메이션이 적용된 객체들을 모두 찾음.

매개변수: nodes : 검색 대상 객체 리스트 (None이면 전체 객체)

반환: 애니메이션이 적용된 객체들의 리스트

def find_animated_material_nodes(self, nodes=None): View Source

    536    def find_animated_material_nodes(self, nodes=None):
    537        """
    538        애니메이션이 적용된 재질을 가진 객체들을 모두 찾음.
    539
    540        매개변수:
    541            nodes : 검색 대상 객체 리스트 (None이면 전체 객체)
    542
    543        반환:
    544            애니메이션이 적용된 재질을 가진 객체들의 리스트
    545        """
    546        if nodes is None:
    547            nodes = rt.objects
    548
    549        result = []
    550        for node in nodes:
    551            mat = rt.getProperty(node, "material")
    552            if mat is not None and self.is_node_animated(mat):
    553                result.append(node)
    554
    555        return result

애니메이션이 적용된 재질을 가진 객체들을 모두 찾음.

매개변수: nodes : 검색 대상 객체 리스트 (None이면 전체 객체)

반환: 애니메이션이 적용된 재질을 가진 객체들의 리스트

def find_animated_transform_nodes(self, nodes=None): View Source

    557    def find_animated_transform_nodes(self, nodes=None):
    558        """
    559        애니메이션이 적용된 변환 정보를 가진 객체들을 모두 찾음.
    560
    561        매개변수:
    562            nodes : 검색 대상 객체 리스트 (None이면 전체 객체)
    563
    564        반환:
    565            애니메이션이 적용된 변환 데이터를 가진 객체들의 리스트
    566        """
    567        if nodes is None:
    568            nodes = rt.objects
    569
    570        result = []
    571        for node in nodes:
    572            controller = rt.getProperty(node, "controller")
    573            if self.is_node_animated(controller):
    574                result.append(node)
    575
    576        return result

애니메이션이 적용된 변환 정보를 가진 객체들을 모두 찾음.

매개변수: nodes : 검색 대상 객체 리스트 (None이면 전체 객체)

반환: 애니메이션이 적용된 변환 데이터를 가진 객체들의 리스트

def save_xform(self, inObj): View Source

    578    def save_xform(self, inObj):
    579        """
    580        객체의 현재 변환 행렬(월드, 부모 스페이스)을 저장하여 복원을 가능하게 함.
    581
    582        매개변수:
    583            inObj : 변환 값을 저장할 객체
    584        """
    585        # 월드 스페이스 행렬 저장
    586        transformString = str(inObj.transform)
    587        rt.setUserProp(inObj, rt.Name("WorldSpaceMatrix"), transformString)
    588
    589        # 부모가 존재하면 부모 스페이스 행렬도 저장
    590        parent = inObj.parent
    591        if parent is not None:
    592            parentTransform = parent.transform
    593            inverseParent = rt.inverse(parentTransform)
    594            objTransform = inObj.transform
    595            parentSpaceMatrix = objTransform * inverseParent
    596            rt.setUserProp(inObj, rt.Name("ParentSpaceMatrix"), str(parentSpaceMatrix))

객체의 현재 변환 행렬(월드, 부모 스페이스)을 저장하여 복원을 가능하게 함.

매개변수: inObj : 변환 값을 저장할 객체

def set_xform(self, inObj, space='World'): View Source

    598    def set_xform(self, inObj, space="World"):
    599        """
    600        저장된 변환 행렬을 객체에 적용함.
    601
    602        매개변수:
    603            inObj : 변환 값을 적용할 객체
    604            space : "World" 또는 "Parent" (적용할 변환 공간)
    605        """
    606        if space == "World":
    607            # 월드 스페이스 행렬 적용
    608            matrixString = rt.getUserProp(inObj, rt.Name("WorldSpaceMatrix"))
    609            transformMatrix = rt.execute(matrixString)
    610            rt.setProperty(inObj, "transform", transformMatrix)
    611        elif space == "Parent":
    612            # 부모 스페이스 행렬 적용
    613            parent = inObj.parent
    614            matrixString = rt.getUserProp(inObj, rt.Name("ParentSpaceMatrix"))
    615            parentSpaceMatrix = rt.execute(matrixString)
    616            if parent is not None:
    617                parentTransform = parent.transform
    618                transformMatrix = parentSpaceMatrix * parentTransform
    619                rt.setProperty(inObj, "transform", transformMatrix)

저장된 변환 행렬을 객체에 적용함.

매개변수: inObj : 변환 값을 적용할 객체 space : "World" 또는 "Parent" (적용할 변환 공간)

class Helper: View Source

     13class Helper:
     14    """
     15    헬퍼 객체 관련 기능을 위한 클래스
     16    MAXScript의 _Helper 구조체를 Python 클래스로 변환
     17
     18    pymxs 모듈을 통해 3ds Max의 기능을 직접 접근합니다.
     19    """
     20
     21    def __init__(self, nameService=None):
     22        """
     23        초기화 함수
     24
     25        Args:
     26            nameService: Name 서비스 인스턴스 (제공되지 않으면 새로 생성)
     27        """
     28        self.name = nameService if nameService else Name()
     29
     30    def create_point(self, inName, size=2, boxToggle=False, crossToggle=True, pointColor=(14, 255, 2), pos=(0, 0, 0)):
     31        """
     32        포인트 헬퍼 생성
     33
     34        Args:
     35            inName: 헬퍼 이름
     36            size: 헬퍼 크기
     37            boxToggle: 박스 표시 여부
     38            crossToggle: 십자 표시 여부
     39            pointColor: 색상
     40            pos: 위치
     41
     42        Returns:
     43            생성된 포인트 헬퍼
     44        """
     45        # Point 객체 생성
     46        returnPoint = rt.Point()
     47        rt.setProperty(returnPoint, "size", size)
     48        rt.setProperty(returnPoint, "box", boxToggle)
     49        rt.setProperty(returnPoint, "cross", crossToggle)
     50
     51        # 색상 설정 (MAXScript의 color를 Point3로 변환)
     52        rt.setProperty(returnPoint, "wirecolor", rt.Color(pointColor[0], pointColor[1], pointColor[2]))
     53
     54        # 이름과 위치 설정
     55        rt.setProperty(returnPoint, "position", rt.Point3(pos[0], pos[1], pos[2]))
     56        rt.setProperty(returnPoint, "name", inName)
     57
     58        # 추가 속성 설정
     59        returnPoint.centermarker = False
     60        returnPoint.axistripod = False
     61        rt.setProperty(returnPoint, "centermarker", False)
     62        rt.setProperty(returnPoint, "axistripod", False)
     63
     64        return returnPoint
     65
     66    def create_empty_point(self, inName):
     67        """
     68        빈 포인트 헬퍼 생성
     69
     70        Args:
     71            inName: 헬퍼 이름
     72
     73        Returns:
     74            생성된 빈 포인트 헬퍼
     75        """
     76        # 빈 포인트 생성 (size:0, crossToggle:off)
     77        returnPoint = self.create_point(inName, size=0, crossToggle=False)
     78        rt.setProperty(returnPoint, "centermarker", False)
     79        rt.setProperty(returnPoint, "axistripod", False)
     80
     81        # MAXScript의 freeze 기능 구현
     82        rt.freeze(returnPoint)
     83
     84        return returnPoint
     85
     86    def get_name_by_type(self, helperType):
     87        """
     88        헬퍼 타입 패턴에 따라 Type namePart 값 찾기
     89
     90        Args:
     91            helperType: 헬퍼 타입 문자열 ("Dummy", "IK", "Target", "Parent", "ExposeTm")
     92
     93        Returns:
     94            찾은 Type namePart 값
     95        """
     96        typePart = self.name.get_name_part("Type")
     97        predefinedValues = typePart.get_predefined_values()
     98        firstTypeValue = typePart.get_value_by_min_weight()
     99
    100
    101        # 헬퍼 타입 패턴 정의
    102        helperNamePatterns = {
    103            "Dummy": ["dum", "Dum", "Dummy", "Helper", "Hpr", "Dmy"],
    104            "IK": ["ik", "IK", "Ik"],
    105            "Target": ["Tgt", "Target", "TG", "Tg", "T"],
    106            "Parent": ["Prn", "PRN", "Parent", "P"],
    107            "ExposeTm": ["Exp", "Etm", "EXP", "ETM"]
    108        }
    109
    110        # 타입 패턴 가져오기
    111        patterns = helperNamePatterns.get(helperType, [])
    112        if not patterns:
    113            return firstTypeValue
    114
    115        # 패턴과 일치하는 값 찾기
    116        for value in predefinedValues:
    117            if value in patterns:
    118                return value
    119
    120        # 일치하는 값이 없으면 기본값 반환
    121        return firstTypeValue
    122
    123    def gen_helper_name_from_obj(self, inObj, make_two=False, is_exp=False):
    124        """
    125        객체로부터 헬퍼 이름 생성
    126
    127        Args:
    128            inObj: 원본 객체
    129            make_two: 두 개의 이름 생성 여부
    130            is_exp: ExposeTM 타입 여부
    131
    132        Returns:
    133            생성된 헬퍼 이름 배열 [포인트 이름, 타겟 이름]
    134        """
    135        pointName = ""
    136        targetName = ""
    137
    138        # 타입 설정
    139        typeName = self.get_name_by_type("Dummy")
    140        if is_exp:
    141            typeName = self.get_name_by_type("ExposeTm")
    142
    143        # 이름 생성
    144        tempName = self.name.replace_name_part("Type", inObj.name, typeName)
    145        if self.name.get_name("Type", inObj.name) == typeName:
    146            tempName = self.name.increase_index(tempName, 1)
    147
    148        pointName = tempName
    149
    150        # 타겟 이름 생성
    151        if make_two:
    152            targetName = self.name.add_suffix_to_real_name(tempName, self.get_name_by_type("Target"))
    153
    154        return [pointName, targetName]
    155
    156    def gen_helper_shape_from_obj(self, inObj):
    157        """
    158        객체로부터 헬퍼 형태 생성
    159
    160        Args:
    161            inObj: 원본 객체
    162
    163        Returns:
    164            [헬퍼 크기, 십자 표시 여부, 박스 표시 여부]
    165        """
    166        helperSize = 2.0
    167        crossToggle = False
    168        boxToggle = True
    169
    170        # BoneGeometry 타입 처리
    171        if rt.classOf(inObj) == rt.BoneGeometry:
    172            # amax 함수를 사용하여 width, height 중 큰 값 선택
    173            helperSize = max(inObj.width, inObj.height)
    174
    175        # Point나 ExposeTm 타입 처리
    176        if rt.classOf(inObj) == rt.Point or rt.classOf(inObj) == rt.ExposeTm:
    177            helperSize = inObj.size + 0.5
    178            if inObj.cross:
    179                crossToggle = False
    180                boxToggle = True
    181            if inObj.box:
    182                crossToggle = True
    183                boxToggle = False
    184
    185        return [helperSize, crossToggle, boxToggle]
    186
    187    def create_helper(self, make_two=False):
    188        """
    189        헬퍼 생성
    190
    191        Args:
    192            make_two: 두 개의 헬퍼 생성 여부
    193
    194        Returns:
    195            생성된 헬퍼 배열
    196        """
    197        createdHelperArray = []
    198
    199        # 선택된 객체가 있는 경우
    200        if rt.selection.count > 0:
    201            selArray = rt.getCurrentSelection()
    202
    203            for item in selArray:
    204                # 헬퍼 크기 및 형태 설정
    205                helperShapeArray = self.gen_helper_shape_from_obj(item)
    206                helperSize = helperShapeArray[0]
    207                crossToggle = helperShapeArray[1]
    208                boxToggle = helperShapeArray[2]
    209
    210                # 헬퍼 이름 설정
    211                helperNameArray = self.gen_helper_name_from_obj(item, make_two=make_two)
    212                pointName = helperNameArray[0]
    213                targetName = helperNameArray[1]
    214
    215                # 두 개의 헬퍼 생성 (포인트와 타겟)
    216                if make_two:
    217                    # 타겟 포인트 생성
    218                    targetPoint = self.create_point(
    219                        targetName,
    220                        size=helperSize,
    221                        boxToggle=False,
    222                        crossToggle=True,
    223                        pointColor=(14, 255, 2),
    224                        pos=(0, 0, 0)
    225                    )
    226                    rt.setProperty(targetPoint, "transform", rt.getProperty(item, "transform"))
    227
    228                    # 메인 포인트 생성
    229                    genPoint = self.create_point(
    230                        pointName,
    231                        size=helperSize,
    232                        boxToggle=True,
    233                        crossToggle=False,
    234                        pointColor=(14, 255, 2),
    235                        pos=(0, 0, 0)
    236                    )
    237                    rt.setProperty(genPoint, "transform", rt.getProperty(item, "transform"))
    238
    239                    # 배열에 추가
    240                    createdHelperArray.append(targetPoint)
    241                    createdHelperArray.append(genPoint)
    242                else:
    243                    # 단일 포인트 생성
    244                    genPoint = self.create_point(
    245                        pointName,
    246                        size=helperSize,
    247                        boxToggle=boxToggle,
    248                        crossToggle=crossToggle,
    249                        pointColor=(14, 255, 2),
    250                        pos=(0, 0, 0)
    251                    )
    252                    rt.setProperty(genPoint, "transform", rt.getProperty(item, "transform"))
    253                    createdHelperArray.append(genPoint)
    254        else:
    255            # 선택된 객체가 없는 경우 기본 포인트 생성
    256            genPoint = rt.Point(wirecolor=rt.Color(14, 255, 2))
    257            createdHelperArray.append(genPoint)
    258
    259        # 생성된 헬퍼들 선택
    260        rt.select(createdHelperArray)
    261        return createdHelperArray
    262
    263    def create_parent_helper(self):
    264        """
    265        부모 헬퍼 생성
    266        """
    267        # 선택된 객체가 있는 경우에만 처리
    268        returnHelpers = []
    269        if rt.selection.count > 0:
    270            selArray = rt.getCurrentSelection()
    271
    272            for item in selArray:
    273                # 헬퍼 크기 및 형태 설정
    274                helperShapeArray = self.gen_helper_shape_from_obj(item)
    275                helperSize = helperShapeArray[0]
    276                crossToggle = helperShapeArray[1]
    277                boxToggle = helperShapeArray[2]
    278
    279                # 헬퍼 이름 설정
    280                helperNameArray = self.gen_helper_name_from_obj(item)
    281                pointName = helperNameArray[0]
    282                targetName = helperNameArray[1]
    283
    284                # 부모 헬퍼 생성
    285                genPoint = self.create_point(
    286                    pointName,
    287                    size=helperSize,
    288                    boxToggle=True,
    289                    crossToggle=False,
    290                    pointColor=(14, 255, 2),
    291                    pos=(0, 0, 0)
    292                )
    293
    294                # 트랜스폼 및 부모 설정
    295                rt.setProperty(genPoint, "transform", rt.getProperty(item, "transform"))
    296                rt.setProperty(genPoint, "parent", rt.getProperty(item, "parent"))
    297                rt.setProperty(item, "parent", genPoint)
    298
    299                # 부모 헬퍼로 이름 변경
    300                finalName = self.name.replace_name_part("Type", genPoint.name, self.get_name_by_type("Parent"))
    301                rt.setProperty(genPoint, "name", finalName)
    302
    303                returnHelpers.append(genPoint)
    304
    305        return returnHelpers
    306
    307
    308    def create_exp_tm(self):
    309        """
    310        ExposeTM 헬퍼 생성
    311
    312        Returns:
    313            생성된 ExposeTM 헬퍼 배열
    314        """
    315        createdHelperArray = []
    316
    317        # 선택된 객체가 있는 경우
    318        if rt.selection.count > 0:
    319            selArray = rt.getCurrentSelection()
    320
    321            for item in selArray:
    322                # 헬퍼 크기 및 형태 설정
    323                helperShapeArray = self.gen_helper_shape_from_obj(item)
    324                helperSize = helperShapeArray[0]
    325                crossToggle = helperShapeArray[1]
    326                boxToggle = helperShapeArray[2]
    327
    328                # 헬퍼 이름 설정 (ExposeTM 용)
    329                helperNameArray = self.gen_helper_name_from_obj(item, make_two=False, is_exp=True)
    330                pointName = helperNameArray[0]
    331
    332                # ExposeTM 객체 생성
    333                genPoint = rt.ExposeTM(
    334                    name=pointName,
    335                    size=helperSize,
    336                    box=boxToggle,
    337                    cross=crossToggle,
    338                    wirecolor=rt.Color(14, 255, 2),
    339                    pos=rt.Point3(0, 0, 0)
    340                )
    341                rt.setProperty(genPoint, "transform", rt.getProperty(item, "transform"))
    342                createdHelperArray.append(genPoint)
    343        else:
    344            # 선택된 객체가 없는 경우 기본 ExposeTM 생성
    345            genPoint = rt.ExposeTM(wirecolor=rt.Color(14, 255, 2))
    346            createdHelperArray.append(genPoint)
    347
    348        # 생성된 헬퍼 객체들 선택
    349        rt.select(createdHelperArray)
    350        return createdHelperArray
    351
    352    def set_size(self, inObj, inNewSize):
    353        """
    354        헬퍼 크기 설정
    355
    356        Args:
    357            inObj: 대상 객체
    358            inNewSize: 새 크기
    359
    360        Returns:
    361            설정된 객체
    362        """
    363        # 헬퍼 클래스 타입인 경우에만 처리
    364        if rt.superClassOf(inObj) == rt.Helper:
    365            rt.setProperty(inObj, "size", inNewSize)
    366            return inObj
    367        return None
    368
    369    def add_size(self, inObj, inAddSize):
    370        """
    371        헬퍼 크기 증가
    372
    373        Args:
    374            inObj: 대상 객체
    375            inAddSize: 증가할 크기
    376
    377        Returns:
    378            설정된 객체
    379        """
    380        # 헬퍼 클래스 타입인 경우에만 처리
    381        if rt.superClassOf(inObj) == rt.Helper:
    382            inObj.size += inAddSize
    383            return inObj
    384        return None
    385
    386    def set_shape_to_center(self, inObj):
    387        """
    388        형태를 센터 마커로 설정
    389
    390        Args:
    391            inObj: 대상 객체
    392        """
    393        # Point 또는 ExposeTm 클래스인 경우에만 처리
    394        if rt.classOf(inObj) == rt.ExposeTm or rt.classOf(inObj) == rt.Point:
    395            inObj.centermarker = True
    396            inObj.box = True
    397            inObj.axistripod = False
    398            inObj.cross = False
    399
    400    def set_shape_to_axis(self, inObj):
    401        """
    402        형태를 축 마커로 설정
    403
    404        Args:
    405            inObj: 대상 객체
    406        """
    407        # Point 또는 ExposeTm 클래스인 경우에만 처리
    408        if rt.classOf(inObj) == rt.ExposeTm or rt.classOf(inObj) == rt.Point:
    409            inObj.axistripod = True
    410            inObj.centermarker = False
    411            inObj.box = False
    412            inObj.cross = False
    413
    414    def set_shape_to_cross(self, inObj):
    415        """
    416        형태를 십자 마커로 설정
    417
    418        Args:
    419            inObj: 대상 객체
    420        """
    421        # Point 또는 ExposeTm 클래스인 경우에만 처리
    422        if rt.classOf(inObj) == rt.ExposeTm or rt.classOf(inObj) == rt.Point:
    423            inObj.cross = True
    424            inObj.box = False
    425            inObj.centermarker = False
    426            inObj.axistripod = False
    427
    428    def set_shape_to_box(self, inObj):
    429        """
    430        형태를 박스 마커로 설정
    431
    432        Args:
    433            inObj: 대상 객체
    434        """
    435        # Point 또는 ExposeTm 클래스인 경우에만 처리
    436        if rt.classOf(inObj) == rt.ExposeTm or rt.classOf(inObj) == rt.Point:
    437            inObj.box = True
    438            inObj.centermarker = False
    439            inObj.axistripod = False
    440            inObj.cross = False
    441
    442    def get_shape(self, inObj):
    443        """
    444        헬퍼 객체의 시각적 형태 속성을 가져옵니다.
    445            inObj (object): 형태 정보를 가져올 대상 3ds Max 헬퍼 객체.
    446            dict: 헬퍼의 형태 속성을 나타내는 딕셔너리.
    447                - "size" (float): 크기
    448                - "centermarker" (bool): 센터 마커 활성화 여부
    449                - "axistripod" (bool): 축 삼각대 활성화 여부
    450                - "cross" (bool): 십자 표시 활성화 여부
    451                - "box" (bool): 박스 표시 활성화 여부
    452                `inObj`가 `rt.ExposeTm` 또는 `rt.Point` 타입의 객체인 경우 해당 객체의
    453                속성값을 반영하며, 그렇지 않은 경우 미리 정의된 기본값을 반환합니다.
    454        """
    455        returnDict = {
    456            "size": 2.0,
    457            "centermarker": False,
    458            "axistripod": False,
    459            "cross": True,
    460            "box": False
    461        }
    462        if rt.classOf(inObj) == rt.ExposeTm or rt.classOf(inObj) == rt.Point:
    463            returnDict["size"] = inObj.size
    464            returnDict["centermarker"] = inObj.centermarker
    465            returnDict["axistripod"] = inObj.axistripod
    466            returnDict["cross"] = inObj.cross
    467            returnDict["box"] = inObj.box
    468
    469        return returnDict
    470
    471    def set_shape(self, inObj, inShapeDict):
    472        """
    473        헬퍼 객체의 표시 형태를 설정합니다.
    474        `rt.ExposeTm` 또는 `rt.Point` 타입의 헬퍼 객체에 대해 크기, 센터 마커, 축 삼각대, 십자, 박스 표시 여부를 설정합니다.
    475            inObj (rt.ExposeTm | rt.Point): 설정을 적용할 헬퍼 객체입니다.
    476            inShapeDict (dict): 헬퍼의 형태를 정의하는 딕셔너리입니다.
    477                다음 키와 값을 포함해야 합니다:
    478                - "size" (float | int): 헬퍼의 크기.
    479                - "centermarker" (bool): 센터 마커 표시 여부 (True/False).
    480                - "axistripod" (bool): 축 삼각대(axis tripod) 표시 여부 (True/False).
    481                - "cross" (bool): 십자(cross) 표시 여부 (True/False).
    482                - "box" (bool): 박스(box) 표시 여부 (True/False).
    483            rt.ExposeTm | rt.Point | None: 형태가 설정된 객체를 반환합니다.
    484                만약 `inObj`가 `rt.ExposeTm` 또는 `rt.Point` 타입이 아닐 경우,
    485                아무 작업도 수행하지 않고 `None`을 반환합니다.
    486        """
    487        if rt.classOf(inObj) == rt.ExposeTm or rt.classOf(inObj) == rt.Point:
    488            inObj.size = inShapeDict["size"]
    489            inObj.centermarker = inShapeDict["centermarker"]
    490            inObj.axistripod = inShapeDict["axistripod"]
    491            inObj.cross = inShapeDict["cross"]
    492            inObj.box = inShapeDict["box"]
    493
    494            return inObj

헬퍼 객체 관련 기능을 위한 클래스 MAXScript의 _Helper 구조체를 Python 클래스로 변환

pymxs 모듈을 통해 3ds Max의 기능을 직접 접근합니다.

Helper(nameService=None) View Source

    21    def __init__(self, nameService=None):
    22        """
    23        초기화 함수
    24
    25        Args:
    26            nameService: Name 서비스 인스턴스 (제공되지 않으면 새로 생성)
    27        """
    28        self.name = nameService if nameService else Name()

초기화 함수

Args: nameService: Name 서비스 인스턴스 (제공되지 않으면 새로 생성)

name
def create_point( self, inName, size=2, boxToggle=False, crossToggle=True, pointColor=(14, 255, 2), pos=(0, 0, 0)): View Source

    30    def create_point(self, inName, size=2, boxToggle=False, crossToggle=True, pointColor=(14, 255, 2), pos=(0, 0, 0)):
    31        """
    32        포인트 헬퍼 생성
    33
    34        Args:
    35            inName: 헬퍼 이름
    36            size: 헬퍼 크기
    37            boxToggle: 박스 표시 여부
    38            crossToggle: 십자 표시 여부
    39            pointColor: 색상
    40            pos: 위치
    41
    42        Returns:
    43            생성된 포인트 헬퍼
    44        """
    45        # Point 객체 생성
    46        returnPoint = rt.Point()
    47        rt.setProperty(returnPoint, "size", size)
    48        rt.setProperty(returnPoint, "box", boxToggle)
    49        rt.setProperty(returnPoint, "cross", crossToggle)
    50
    51        # 색상 설정 (MAXScript의 color를 Point3로 변환)
    52        rt.setProperty(returnPoint, "wirecolor", rt.Color(pointColor[0], pointColor[1], pointColor[2]))
    53
    54        # 이름과 위치 설정
    55        rt.setProperty(returnPoint, "position", rt.Point3(pos[0], pos[1], pos[2]))
    56        rt.setProperty(returnPoint, "name", inName)
    57
    58        # 추가 속성 설정
    59        returnPoint.centermarker = False
    60        returnPoint.axistripod = False
    61        rt.setProperty(returnPoint, "centermarker", False)
    62        rt.setProperty(returnPoint, "axistripod", False)
    63
    64        return returnPoint

포인트 헬퍼 생성

Args: inName: 헬퍼 이름 size: 헬퍼 크기 boxToggle: 박스 표시 여부 crossToggle: 십자 표시 여부 pointColor: 색상 pos: 위치

Returns: 생성된 포인트 헬퍼

def create_empty_point(self, inName): View Source

    66    def create_empty_point(self, inName):
    67        """
    68        빈 포인트 헬퍼 생성
    69
    70        Args:
    71            inName: 헬퍼 이름
    72
    73        Returns:
    74            생성된 빈 포인트 헬퍼
    75        """
    76        # 빈 포인트 생성 (size:0, crossToggle:off)
    77        returnPoint = self.create_point(inName, size=0, crossToggle=False)
    78        rt.setProperty(returnPoint, "centermarker", False)
    79        rt.setProperty(returnPoint, "axistripod", False)
    80
    81        # MAXScript의 freeze 기능 구현
    82        rt.freeze(returnPoint)
    83
    84        return returnPoint

빈 포인트 헬퍼 생성

Args: inName: 헬퍼 이름

Returns: 생성된 빈 포인트 헬퍼

def get_name_by_type(self, helperType): View Source

     86    def get_name_by_type(self, helperType):
     87        """
     88        헬퍼 타입 패턴에 따라 Type namePart 값 찾기
     89
     90        Args:
     91            helperType: 헬퍼 타입 문자열 ("Dummy", "IK", "Target", "Parent", "ExposeTm")
     92
     93        Returns:
     94            찾은 Type namePart 값
     95        """
     96        typePart = self.name.get_name_part("Type")
     97        predefinedValues = typePart.get_predefined_values()
     98        firstTypeValue = typePart.get_value_by_min_weight()
     99
    100
    101        # 헬퍼 타입 패턴 정의
    102        helperNamePatterns = {
    103            "Dummy": ["dum", "Dum", "Dummy", "Helper", "Hpr", "Dmy"],
    104            "IK": ["ik", "IK", "Ik"],
    105            "Target": ["Tgt", "Target", "TG", "Tg", "T"],
    106            "Parent": ["Prn", "PRN", "Parent", "P"],
    107            "ExposeTm": ["Exp", "Etm", "EXP", "ETM"]
    108        }
    109
    110        # 타입 패턴 가져오기
    111        patterns = helperNamePatterns.get(helperType, [])
    112        if not patterns:
    113            return firstTypeValue
    114
    115        # 패턴과 일치하는 값 찾기
    116        for value in predefinedValues:
    117            if value in patterns:
    118                return value
    119
    120        # 일치하는 값이 없으면 기본값 반환
    121        return firstTypeValue

헬퍼 타입 패턴에 따라 Type namePart 값 찾기

Args: helperType: 헬퍼 타입 문자열 ("Dummy", "IK", "Target", "Parent", "ExposeTm")

Returns: 찾은 Type namePart 값

def gen_helper_name_from_obj(self, inObj, make_two=False, is_exp=False): View Source

    123    def gen_helper_name_from_obj(self, inObj, make_two=False, is_exp=False):
    124        """
    125        객체로부터 헬퍼 이름 생성
    126
    127        Args:
    128            inObj: 원본 객체
    129            make_two: 두 개의 이름 생성 여부
    130            is_exp: ExposeTM 타입 여부
    131
    132        Returns:
    133            생성된 헬퍼 이름 배열 [포인트 이름, 타겟 이름]
    134        """
    135        pointName = ""
    136        targetName = ""
    137
    138        # 타입 설정
    139        typeName = self.get_name_by_type("Dummy")
    140        if is_exp:
    141            typeName = self.get_name_by_type("ExposeTm")
    142
    143        # 이름 생성
    144        tempName = self.name.replace_name_part("Type", inObj.name, typeName)
    145        if self.name.get_name("Type", inObj.name) == typeName:
    146            tempName = self.name.increase_index(tempName, 1)
    147
    148        pointName = tempName
    149
    150        # 타겟 이름 생성
    151        if make_two:
    152            targetName = self.name.add_suffix_to_real_name(tempName, self.get_name_by_type("Target"))
    153
    154        return [pointName, targetName]

객체로부터 헬퍼 이름 생성

Args: inObj: 원본 객체 make_two: 두 개의 이름 생성 여부 is_exp: ExposeTM 타입 여부

Returns: 생성된 헬퍼 이름 배열 [포인트 이름, 타겟 이름]

def gen_helper_shape_from_obj(self, inObj): View Source

    156    def gen_helper_shape_from_obj(self, inObj):
    157        """
    158        객체로부터 헬퍼 형태 생성
    159
    160        Args:
    161            inObj: 원본 객체
    162
    163        Returns:
    164            [헬퍼 크기, 십자 표시 여부, 박스 표시 여부]
    165        """
    166        helperSize = 2.0
    167        crossToggle = False
    168        boxToggle = True
    169
    170        # BoneGeometry 타입 처리
    171        if rt.classOf(inObj) == rt.BoneGeometry:
    172            # amax 함수를 사용하여 width, height 중 큰 값 선택
    173            helperSize = max(inObj.width, inObj.height)
    174
    175        # Point나 ExposeTm 타입 처리
    176        if rt.classOf(inObj) == rt.Point or rt.classOf(inObj) == rt.ExposeTm:
    177            helperSize = inObj.size + 0.5
    178            if inObj.cross:
    179                crossToggle = False
    180                boxToggle = True
    181            if inObj.box:
    182                crossToggle = True
    183                boxToggle = False
    184
    185        return [helperSize, crossToggle, boxToggle]

객체로부터 헬퍼 형태 생성

Args: inObj: 원본 객체

Returns: [헬퍼 크기, 십자 표시 여부, 박스 표시 여부]

def create_helper(self, make_two=False): View Source

    187    def create_helper(self, make_two=False):
    188        """
    189        헬퍼 생성
    190
    191        Args:
    192            make_two: 두 개의 헬퍼 생성 여부
    193
    194        Returns:
    195            생성된 헬퍼 배열
    196        """
    197        createdHelperArray = []
    198
    199        # 선택된 객체가 있는 경우
    200        if rt.selection.count > 0:
    201            selArray = rt.getCurrentSelection()
    202
    203            for item in selArray:
    204                # 헬퍼 크기 및 형태 설정
    205                helperShapeArray = self.gen_helper_shape_from_obj(item)
    206                helperSize = helperShapeArray[0]
    207                crossToggle = helperShapeArray[1]
    208                boxToggle = helperShapeArray[2]
    209
    210                # 헬퍼 이름 설정
    211                helperNameArray = self.gen_helper_name_from_obj(item, make_two=make_two)
    212                pointName = helperNameArray[0]
    213                targetName = helperNameArray[1]
    214
    215                # 두 개의 헬퍼 생성 (포인트와 타겟)
    216                if make_two:
    217                    # 타겟 포인트 생성
    218                    targetPoint = self.create_point(
    219                        targetName,
    220                        size=helperSize,
    221                        boxToggle=False,
    222                        crossToggle=True,
    223                        pointColor=(14, 255, 2),
    224                        pos=(0, 0, 0)
    225                    )
    226                    rt.setProperty(targetPoint, "transform", rt.getProperty(item, "transform"))
    227
    228                    # 메인 포인트 생성
    229                    genPoint = self.create_point(
    230                        pointName,
    231                        size=helperSize,
    232                        boxToggle=True,
    233                        crossToggle=False,
    234                        pointColor=(14, 255, 2),
    235                        pos=(0, 0, 0)
    236                    )
    237                    rt.setProperty(genPoint, "transform", rt.getProperty(item, "transform"))
    238
    239                    # 배열에 추가
    240                    createdHelperArray.append(targetPoint)
    241                    createdHelperArray.append(genPoint)
    242                else:
    243                    # 단일 포인트 생성
    244                    genPoint = self.create_point(
    245                        pointName,
    246                        size=helperSize,
    247                        boxToggle=boxToggle,
    248                        crossToggle=crossToggle,
    249                        pointColor=(14, 255, 2),
    250                        pos=(0, 0, 0)
    251                    )
    252                    rt.setProperty(genPoint, "transform", rt.getProperty(item, "transform"))
    253                    createdHelperArray.append(genPoint)
    254        else:
    255            # 선택된 객체가 없는 경우 기본 포인트 생성
    256            genPoint = rt.Point(wirecolor=rt.Color(14, 255, 2))
    257            createdHelperArray.append(genPoint)
    258
    259        # 생성된 헬퍼들 선택
    260        rt.select(createdHelperArray)
    261        return createdHelperArray

헬퍼 생성

Args: make_two: 두 개의 헬퍼 생성 여부

Returns: 생성된 헬퍼 배열

def create_parent_helper(self): View Source

    263    def create_parent_helper(self):
    264        """
    265        부모 헬퍼 생성
    266        """
    267        # 선택된 객체가 있는 경우에만 처리
    268        returnHelpers = []
    269        if rt.selection.count > 0:
    270            selArray = rt.getCurrentSelection()
    271
    272            for item in selArray:
    273                # 헬퍼 크기 및 형태 설정
    274                helperShapeArray = self.gen_helper_shape_from_obj(item)
    275                helperSize = helperShapeArray[0]
    276                crossToggle = helperShapeArray[1]
    277                boxToggle = helperShapeArray[2]
    278
    279                # 헬퍼 이름 설정
    280                helperNameArray = self.gen_helper_name_from_obj(item)
    281                pointName = helperNameArray[0]
    282                targetName = helperNameArray[1]
    283
    284                # 부모 헬퍼 생성
    285                genPoint = self.create_point(
    286                    pointName,
    287                    size=helperSize,
    288                    boxToggle=True,
    289                    crossToggle=False,
    290                    pointColor=(14, 255, 2),
    291                    pos=(0, 0, 0)
    292                )
    293
    294                # 트랜스폼 및 부모 설정
    295                rt.setProperty(genPoint, "transform", rt.getProperty(item, "transform"))
    296                rt.setProperty(genPoint, "parent", rt.getProperty(item, "parent"))
    297                rt.setProperty(item, "parent", genPoint)
    298
    299                # 부모 헬퍼로 이름 변경
    300                finalName = self.name.replace_name_part("Type", genPoint.name, self.get_name_by_type("Parent"))
    301                rt.setProperty(genPoint, "name", finalName)
    302
    303                returnHelpers.append(genPoint)
    304
    305        return returnHelpers

부모 헬퍼 생성

def create_exp_tm(self): View Source

    308    def create_exp_tm(self):
    309        """
    310        ExposeTM 헬퍼 생성
    311
    312        Returns:
    313            생성된 ExposeTM 헬퍼 배열
    314        """
    315        createdHelperArray = []
    316
    317        # 선택된 객체가 있는 경우
    318        if rt.selection.count > 0:
    319            selArray = rt.getCurrentSelection()
    320
    321            for item in selArray:
    322                # 헬퍼 크기 및 형태 설정
    323                helperShapeArray = self.gen_helper_shape_from_obj(item)
    324                helperSize = helperShapeArray[0]
    325                crossToggle = helperShapeArray[1]
    326                boxToggle = helperShapeArray[2]
    327
    328                # 헬퍼 이름 설정 (ExposeTM 용)
    329                helperNameArray = self.gen_helper_name_from_obj(item, make_two=False, is_exp=True)
    330                pointName = helperNameArray[0]
    331
    332                # ExposeTM 객체 생성
    333                genPoint = rt.ExposeTM(
    334                    name=pointName,
    335                    size=helperSize,
    336                    box=boxToggle,
    337                    cross=crossToggle,
    338                    wirecolor=rt.Color(14, 255, 2),
    339                    pos=rt.Point3(0, 0, 0)
    340                )
    341                rt.setProperty(genPoint, "transform", rt.getProperty(item, "transform"))
    342                createdHelperArray.append(genPoint)
    343        else:
    344            # 선택된 객체가 없는 경우 기본 ExposeTM 생성
    345            genPoint = rt.ExposeTM(wirecolor=rt.Color(14, 255, 2))
    346            createdHelperArray.append(genPoint)
    347
    348        # 생성된 헬퍼 객체들 선택
    349        rt.select(createdHelperArray)
    350        return createdHelperArray

ExposeTM 헬퍼 생성

Returns: 생성된 ExposeTM 헬퍼 배열

def set_size(self, inObj, inNewSize): View Source

    352    def set_size(self, inObj, inNewSize):
    353        """
    354        헬퍼 크기 설정
    355
    356        Args:
    357            inObj: 대상 객체
    358            inNewSize: 새 크기
    359
    360        Returns:
    361            설정된 객체
    362        """
    363        # 헬퍼 클래스 타입인 경우에만 처리
    364        if rt.superClassOf(inObj) == rt.Helper:
    365            rt.setProperty(inObj, "size", inNewSize)
    366            return inObj
    367        return None

헬퍼 크기 설정

Args: inObj: 대상 객체 inNewSize: 새 크기

Returns: 설정된 객체

def add_size(self, inObj, inAddSize): View Source

    369    def add_size(self, inObj, inAddSize):
    370        """
    371        헬퍼 크기 증가
    372
    373        Args:
    374            inObj: 대상 객체
    375            inAddSize: 증가할 크기
    376
    377        Returns:
    378            설정된 객체
    379        """
    380        # 헬퍼 클래스 타입인 경우에만 처리
    381        if rt.superClassOf(inObj) == rt.Helper:
    382            inObj.size += inAddSize
    383            return inObj
    384        return None

헬퍼 크기 증가

Args: inObj: 대상 객체 inAddSize: 증가할 크기

Returns: 설정된 객체

def set_shape_to_center(self, inObj): View Source

    386    def set_shape_to_center(self, inObj):
    387        """
    388        형태를 센터 마커로 설정
    389
    390        Args:
    391            inObj: 대상 객체
    392        """
    393        # Point 또는 ExposeTm 클래스인 경우에만 처리
    394        if rt.classOf(inObj) == rt.ExposeTm or rt.classOf(inObj) == rt.Point:
    395            inObj.centermarker = True
    396            inObj.box = True
    397            inObj.axistripod = False
    398            inObj.cross = False

형태를 센터 마커로 설정

Args: inObj: 대상 객체

def set_shape_to_axis(self, inObj): View Source

    400    def set_shape_to_axis(self, inObj):
    401        """
    402        형태를 축 마커로 설정
    403
    404        Args:
    405            inObj: 대상 객체
    406        """
    407        # Point 또는 ExposeTm 클래스인 경우에만 처리
    408        if rt.classOf(inObj) == rt.ExposeTm or rt.classOf(inObj) == rt.Point:
    409            inObj.axistripod = True
    410            inObj.centermarker = False
    411            inObj.box = False
    412            inObj.cross = False

형태를 축 마커로 설정

Args: inObj: 대상 객체

def set_shape_to_cross(self, inObj): View Source

    414    def set_shape_to_cross(self, inObj):
    415        """
    416        형태를 십자 마커로 설정
    417
    418        Args:
    419            inObj: 대상 객체
    420        """
    421        # Point 또는 ExposeTm 클래스인 경우에만 처리
    422        if rt.classOf(inObj) == rt.ExposeTm or rt.classOf(inObj) == rt.Point:
    423            inObj.cross = True
    424            inObj.box = False
    425            inObj.centermarker = False
    426            inObj.axistripod = False

형태를 십자 마커로 설정

Args: inObj: 대상 객체

def set_shape_to_box(self, inObj): View Source

    428    def set_shape_to_box(self, inObj):
    429        """
    430        형태를 박스 마커로 설정
    431
    432        Args:
    433            inObj: 대상 객체
    434        """
    435        # Point 또는 ExposeTm 클래스인 경우에만 처리
    436        if rt.classOf(inObj) == rt.ExposeTm or rt.classOf(inObj) == rt.Point:
    437            inObj.box = True
    438            inObj.centermarker = False
    439            inObj.axistripod = False
    440            inObj.cross = False

형태를 박스 마커로 설정

Args: inObj: 대상 객체

def get_shape(self, inObj): View Source

    442    def get_shape(self, inObj):
    443        """
    444        헬퍼 객체의 시각적 형태 속성을 가져옵니다.
    445            inObj (object): 형태 정보를 가져올 대상 3ds Max 헬퍼 객체.
    446            dict: 헬퍼의 형태 속성을 나타내는 딕셔너리.
    447                - "size" (float): 크기
    448                - "centermarker" (bool): 센터 마커 활성화 여부
    449                - "axistripod" (bool): 축 삼각대 활성화 여부
    450                - "cross" (bool): 십자 표시 활성화 여부
    451                - "box" (bool): 박스 표시 활성화 여부
    452                `inObj`가 `rt.ExposeTm` 또는 `rt.Point` 타입의 객체인 경우 해당 객체의
    453                속성값을 반영하며, 그렇지 않은 경우 미리 정의된 기본값을 반환합니다.
    454        """
    455        returnDict = {
    456            "size": 2.0,
    457            "centermarker": False,
    458            "axistripod": False,
    459            "cross": True,
    460            "box": False
    461        }
    462        if rt.classOf(inObj) == rt.ExposeTm or rt.classOf(inObj) == rt.Point:
    463            returnDict["size"] = inObj.size
    464            returnDict["centermarker"] = inObj.centermarker
    465            returnDict["axistripod"] = inObj.axistripod
    466            returnDict["cross"] = inObj.cross
    467            returnDict["box"] = inObj.box
    468
    469        return returnDict

헬퍼 객체의 시각적 형태 속성을 가져옵니다. inObj (object): 형태 정보를 가져올 대상 3ds Max 헬퍼 객체. dict: 헬퍼의 형태 속성을 나타내는 딕셔너리. - "size" (float): 크기 - "centermarker" (bool): 센터 마커 활성화 여부 - "axistripod" (bool): 축 삼각대 활성화 여부 - "cross" (bool): 십자 표시 활성화 여부 - "box" (bool): 박스 표시 활성화 여부 `inObj`가 `rt.ExposeTm` 또는 `rt.Point` 타입의 객체인 경우 해당 객체의 속성값을 반영하며, 그렇지 않은 경우 미리 정의된 기본값을 반환합니다.

def set_shape(self, inObj, inShapeDict): View Source

    471    def set_shape(self, inObj, inShapeDict):
    472        """
    473        헬퍼 객체의 표시 형태를 설정합니다.
    474        `rt.ExposeTm` 또는 `rt.Point` 타입의 헬퍼 객체에 대해 크기, 센터 마커, 축 삼각대, 십자, 박스 표시 여부를 설정합니다.
    475            inObj (rt.ExposeTm | rt.Point): 설정을 적용할 헬퍼 객체입니다.
    476            inShapeDict (dict): 헬퍼의 형태를 정의하는 딕셔너리입니다.
    477                다음 키와 값을 포함해야 합니다:
    478                - "size" (float | int): 헬퍼의 크기.
    479                - "centermarker" (bool): 센터 마커 표시 여부 (True/False).
    480                - "axistripod" (bool): 축 삼각대(axis tripod) 표시 여부 (True/False).
    481                - "cross" (bool): 십자(cross) 표시 여부 (True/False).
    482                - "box" (bool): 박스(box) 표시 여부 (True/False).
    483            rt.ExposeTm | rt.Point | None: 형태가 설정된 객체를 반환합니다.
    484                만약 `inObj`가 `rt.ExposeTm` 또는 `rt.Point` 타입이 아닐 경우,
    485                아무 작업도 수행하지 않고 `None`을 반환합니다.
    486        """
    487        if rt.classOf(inObj) == rt.ExposeTm or rt.classOf(inObj) == rt.Point:
    488            inObj.size = inShapeDict["size"]
    489            inObj.centermarker = inShapeDict["centermarker"]
    490            inObj.axistripod = inShapeDict["axistripod"]
    491            inObj.cross = inShapeDict["cross"]
    492            inObj.box = inShapeDict["box"]
    493
    494            return inObj

헬퍼 객체의 표시 형태를 설정합니다. `rt.ExposeTm` 또는 `rt.Point` 타입의 헬퍼 객체에 대해 크기, 센터 마커, 축 삼각대, 십자, 박스 표시 여부를 설정합니다. inObj (rt.ExposeTm | rt.Point): 설정을 적용할 헬퍼 객체입니다. inShapeDict (dict): 헬퍼의 형태를 정의하는 딕셔너리입니다. 다음 키와 값을 포함해야 합니다: - "size" (float | int): 헬퍼의 크기. - "centermarker" (bool): 센터 마커 표시 여부 (True/False). - "axistripod" (bool): 축 삼각대(axis tripod) 표시 여부 (True/False). - "cross" (bool): 십자(cross) 표시 여부 (True/False). - "box" (bool): 박스(box) 표시 여부 (True/False). rt.ExposeTm | rt.Point | None: 형태가 설정된 객체를 반환합니다. 만약 `inObj`가 `rt.ExposeTm` 또는 `rt.Point` 타입이 아닐 경우, 아무 작업도 수행하지 않고 `None`을 반환합니다.

class Constraint: View Source

      18class Constraint:
      19    """
      20    제약(Constraint) 관련 기능을 제공하는 클래스.
      21    MAXScript의 _Constraint 구조체 개념을 Python으로 재구현한 클래스이며,
      22    3ds Max의 기능들을 pymxs API를 통해 제어합니다.
      23    """
      24
      25    def __init__(self, nameService=None, helperService=None):
      26        """
      27        클래스 초기화.
      28
      29        Args:
      30            nameService: 이름 처리 서비스 (제공되지 않으면 새로 생성)
      31            helperService: 헬퍼 객체 관련 서비스 (제공되지 않으면 새로 생성)
      32        """
      33        self.name = nameService if nameService else Name()
      34        self.helper = helperService if helperService else Helper(nameService=self.name) # Pass the potentially newly created nameService
      35
      36    def collapse(self, inObj):
      37        """
      38        비 Biped 객체의 트랜스폼 컨트롤러를 기본 컨트롤러로 초기화하고 현재 변환 상태 유지.
      39
      40        Args:
      41            inObj: 초기화할 대상 객체
      42
      43        Returns:
      44            None
      45        """
      46        if rt.classOf(inObj) != rt.Biped_Object:
      47            # 현재 변환 상태 백업
      48            tempTransform = rt.getProperty(inObj, "transform")
      49
      50            # 기본 컨트롤러로 위치, 회전, 스케일 초기화
      51            rt.setPropertyController(inObj.controller, "Position", rt.Position_XYZ())
      52            rt.setPropertyController(inObj.controller, "Rotation", rt.Euler_XYZ())
      53            rt.setPropertyController(inObj.controller, "Scale", rt.Bezier_Scale())
      54
      55            # 백업한 변환 상태 복원
      56            rt.setProperty(inObj, "transform", tempTransform)
      57
      58    def set_active_last(self, inObj):
      59        """
      60        객체의 위치와 회전 컨트롤러 리스트에서 마지막 컨트롤러를 활성화.
      61
      62        Args:
      63            inObj: 대상 객체
      64
      65        Returns:
      66            None
      67        """
      68        # 위치 컨트롤러가 리스트 형태면 마지막 컨트롤러 활성화
      69        pos_controller = rt.getPropertyController(inObj.controller, "Position")
      70        if rt.classOf(pos_controller) == rt.Position_list:
      71            pos_controller.setActive(pos_controller.count)
      72
      73        # 회전 컨트롤러가 리스트 형태면 마지막 컨트롤러 활성화
      74        rot_controller = rt.getPropertyController(inObj.controller, "Rotation")
      75        if rt.classOf(rot_controller) == rt.Rotation_list:
      76            rot_controller.setActive(rot_controller.count)
      77
      78    def get_pos_list_controller(self, inObj):
      79        """
      80        객체의 위치 리스트 컨트롤러를 반환.
      81
      82        Args:
      83            inObj: 대상 객체
      84
      85        Returns:
      86            위치 리스트 컨트롤러 (없으면 None)
      87        """
      88        returnPosListCtr = None
      89
      90        # 위치 컨트롤러가 리스트 형태인지 확인
      91        pos_controller = rt.getPropertyController(inObj.controller, "Position")
      92        if rt.classOf(pos_controller) == rt.Position_list:
      93            returnPosListCtr = pos_controller
      94
      95        return returnPosListCtr
      96
      97    def assign_pos_list(self, inObj):
      98        """
      99        객체에 위치 리스트 컨트롤러를 할당하거나 기존 것을 반환.
     100
     101        Args:
     102            inObj: 대상 객체
     103
     104        Returns:
     105            위치 리스트 컨트롤러
     106        """
     107        returnPosListCtr = None
     108
     109        # 현재 위치 컨트롤러 확인
     110        pos_controller = rt.getPropertyController(inObj.controller, "Position")
     111
     112        # 리스트 형태가 아니면 새로 생성
     113        if rt.classOf(pos_controller) != rt.Position_list:
     114            returnPosListCtr = rt.Position_list()
     115            rt.setPropertyController(inObj.controller, "Position", returnPosListCtr)
     116            return returnPosListCtr
     117
     118        # 이미 리스트 형태면 그대로 반환
     119        if rt.classOf(pos_controller) == rt.Position_list:
     120            returnPosListCtr = pos_controller
     121
     122        return returnPosListCtr
     123
     124    def get_pos_const(self, inObj):
     125        """
     126        객체의 위치 제약 컨트롤러를 찾아 반환.
     127
     128        Args:
     129            inObj: 대상 객체
     130
     131        Returns:
     132            위치 제약 컨트롤러 (없으면 None)
     133        """
     134        returnConst = None
     135
     136        # 위치 컨트롤러가 리스트 형태인 경우
     137        pos_controller = rt.getPropertyController(inObj.controller, "Position")
     138        if rt.classOf(pos_controller) == rt.Position_list:
     139            lst = pos_controller
     140            constNum = lst.getCount()
     141            activeNum = lst.getActive()
     142
     143            # 리스트 내 모든 컨트롤러 검사
     144            for i in range(constNum):
     145                sub_controller = lst[i].controller
     146                if rt.classOf(sub_controller) == rt.Position_Constraint:
     147                    returnConst = sub_controller
     148                    # 현재 활성화된 컨트롤러면 즉시 반환
     149                    if activeNum == i:
     150                        return returnConst
     151
     152        # 위치 컨트롤러가 직접 Position_Constraint인 경우
     153        elif rt.classOf(pos_controller) == rt.Position_Constraint:
     154            returnConst = pos_controller
     155
     156        return returnConst
     157
     158    def assign_pos_const(self, inObj, inTarget, keepInit=False):
     159        """
     160        객체에 위치 제약 컨트롤러를 할당하고 지정된 타겟을 추가.
     161
     162        Args:
     163            inObj: 제약을 적용할 객체
     164            inTarget: 타겟 객체
     165            keepInit: 기존 변환 유지 여부 (기본값: False)
     166
     167        Returns:
     168            위치 제약 컨트롤러
     169        """
     170        # 위치 컨트롤러가 리스트 형태가 아니면 변환
     171        pos_controller = rt.getPropertyController(inObj.controller, "Position")
     172        if rt.classOf(pos_controller) != rt.Position_list:
     173            rt.setPropertyController(inObj.controller, "Position", rt.Position_list())
     174
     175        # 기존 위치 제약 컨트롤러 확인
     176        targetPosConstraint = self.get_pos_const(inObj)
     177
     178        # 위치 제약 컨트롤러가 없으면 새로 생성
     179        if targetPosConstraint is None:
     180            targetPosConstraint = rt.Position_Constraint()
     181            pos_list = self.get_pos_list_controller(inObj)
     182            rt.setPropertyController(pos_list, "Available", targetPosConstraint)
     183            pos_list.setActive(pos_list.count)
     184
     185        # 타겟 추가 및 가중치 조정
     186        targetNum = targetPosConstraint.getNumTargets()
     187        targetWeight = 100.0 / (targetNum + 1)
     188        targetPosConstraint.appendTarget(inTarget, targetWeight)
     189
     190        # 기존 타겟이 있으면 가중치 재조정
     191        if targetNum > 0:
     192            newWeightScale = 100.0 - targetWeight
     193            for i in range(1, targetNum + 1):  # Maxscript는 1부터 시작
     194                newWeight = targetPosConstraint.GetWeight(i) * 0.01 * newWeightScale
     195                targetPosConstraint.SetWeight(i, newWeight)
     196
     197        # 상대적 모드 설정
     198        targetPosConstraint.relative = keepInit
     199
     200        return targetPosConstraint
     201
     202    def assign_pos_const_multi(self, inObj, inTargetArray, keepInit=False):
     203        """
     204        객체에 여러 타겟을 가진 위치 제약 컨트롤러를 할당.
     205
     206        Args:
     207            inObj: 제약을 적용할 객체
     208            inTargetArray: 타겟 객체 배열
     209            keepInit: 기존 변환 유지 여부 (기본값: False)
     210
     211        Returns:
     212            None
     213        """
     214        for item in inTargetArray:
     215            self.assign_pos_const(inObj, item, keepInit=keepInit)
     216
     217        return self.get_pos_const(inObj)
     218
     219    def add_target_to_pos_const(self, inObj, inTarget, inWeight):
     220        """
     221        기존 위치 제약 컨트롤러에 새 타겟을 추가하고 지정된 가중치 설정.
     222
     223        Args:
     224            inObj: 제약이 적용된 객체
     225            inTarget: 추가할 타겟 객체
     226            inWeight: 적용할 가중치 값
     227
     228        Returns:
     229            None
     230        """
     231        # 위치 제약 컨트롤러에 타겟 추가
     232        targetPosConst = self.assign_pos_const(inObj, inTarget)
     233
     234        # 마지막 타겟에 특정 가중치 적용
     235        targetNum = targetPosConst.getNumTargets()
     236        targetPosConst.SetWeight(targetNum, inWeight)
     237
     238        return targetPosConst
     239
     240    def assign_pos_xyz(self, inObj):
     241        """
     242        객체에 위치 XYZ 컨트롤러를 할당.
     243
     244        Args:
     245            inObj: 컨트롤러를 할당할 객체
     246
     247        Returns:
     248            None
     249        """
     250        # 위치 컨트롤러가 리스트 형태가 아니면 변환
     251        pos_controller = rt.getPropertyController(inObj.controller, "Position")
     252        if rt.classOf(pos_controller) != rt.Position_list:
     253            rt.setPropertyController(inObj.controller, "Position", rt.Position_list())
     254
     255        # 위치 리스트 컨트롤러 가져오기
     256        posList = self.assign_pos_list(inObj)
     257
     258        # Position_XYZ 컨트롤러 할당
     259        posXYZ = rt.Position_XYZ()
     260        rt.setPropertyController(posList, "Available", posXYZ)
     261        posList.setActive(posList.count)
     262
     263        return posXYZ
     264
     265    def assign_pos_script_controller(self, inObj):
     266        """
     267        객체에 스크립트 기반 위치 컨트롤러를 할당.
     268
     269        Args:
     270            inObj: 컨트롤러를 할당할 객체
     271
     272        Returns:
     273            None
     274        """
     275        # 위치 컨트롤러가 리스트 형태가 아니면 변환
     276        pos_controller = rt.getPropertyController(inObj.controller, "Position")
     277        if rt.classOf(pos_controller) != rt.Position_list:
     278            rt.setPropertyController(inObj.controller, "Position", rt.Position_list())
     279
     280        # 위치 리스트 컨트롤러 가져오기
     281        posList = self.assign_pos_list(inObj)
     282
     283        # 스크립트 기반 위치 컨트롤러 할당
     284        scriptPos = rt.Position_Script()
     285        rt.setPropertyController(posList, "Available", scriptPos)
     286        posList.setActive(posList.count)
     287
     288        return scriptPos
     289
     290    def get_rot_list_controller(self, inObj):
     291        """
     292        객체의 회전 리스트 컨트롤러를 반환.
     293
     294        Args:
     295            inObj: 대상 객체
     296
     297        Returns:
     298            회전 리스트 컨트롤러 (없으면 None)
     299        """
     300        returnRotListCtr = None
     301
     302        # 회전 컨트롤러가 리스트 형태인지 확인
     303        rot_controller = rt.getPropertyController(inObj.controller, "Rotation")
     304        if rt.classOf(rot_controller) == rt.Rotation_list:
     305            returnRotListCtr = rot_controller
     306
     307        return returnRotListCtr
     308
     309    def assign_rot_list(self, inObj):
     310        """
     311        객체에 회전 리스트 컨트롤러를 할당하거나 기존 것을 반환.
     312
     313        Args:
     314            inObj: 대상 객체
     315
     316        Returns:
     317            회전 리스트 컨트롤러
     318        """
     319        returnRotListCtr = None
     320
     321        # 현재 회전 컨트롤러 확인
     322        rot_controller = rt.getPropertyController(inObj.controller, "Rotation")
     323
     324        # 리스트 형태가 아니면 새로 생성
     325        if rt.classOf(rot_controller) != rt.Rotation_list:
     326            returnRotListCtr = rt.Rotation_list()
     327            rt.setPropertyController(inObj.controller, "Rotation", returnRotListCtr)
     328            return returnRotListCtr
     329
     330        # 이미 리스트 형태면 그대로 반환
     331        if rt.classOf(rot_controller) == rt.Rotation_list:
     332            returnRotListCtr = rot_controller
     333
     334        return returnRotListCtr
     335
     336    def get_rot_const(self, inObj):
     337        """
     338        객체의 회전 제약 컨트롤러를 찾아 반환.
     339
     340        Args:
     341            inObj: 대상 객체
     342
     343        Returns:
     344            회전 제약 컨트롤러 (없으면 None)
     345        """
     346        returnConst = None
     347
     348        # 회전 컨트롤러가 리스트 형태인 경우
     349        rot_controller = rt.getPropertyController(inObj.controller, "Rotation")
     350        if rt.classOf(rot_controller) == rt.Rotation_list:
     351            lst = rot_controller
     352            constNum = lst.getCount()
     353            activeNum = lst.getActive()
     354
     355            # 리스트 내 모든 컨트롤러 검사
     356            for i in range(constNum):  # Maxscript는 1부터 시작
     357                sub_controller = lst[i].controller
     358                if rt.classOf(sub_controller) == rt.Orientation_Constraint:
     359                    returnConst = sub_controller
     360                    # 현재 활성화된 컨트롤러면 즉시 반환
     361                    if activeNum == i:
     362                        return returnConst
     363
     364        # 회전 컨트롤러가 직접 Orientation_Constraint인 경우
     365        elif rt.classOf(rot_controller) == rt.Orientation_Constraint:
     366            returnConst = rot_controller
     367
     368        return returnConst
     369
     370    def assign_rot_const(self, inObj, inTarget, keepInit=False):
     371        """
     372        객체에 회전 제약 컨트롤러를 할당하고 지정된 타겟을 추가.
     373
     374        Args:
     375            inObj: 제약을 적용할 객체
     376            inTarget: 타겟 객체
     377            keepInit: 기존 변환 유지 여부 (기본값: False)
     378
     379        Returns:
     380            회전 제약 컨트롤러
     381        """
     382        # 회전 컨트롤러가 리스트 형태가 아니면 변환
     383        rot_controller = rt.getPropertyController(inObj.controller, "Rotation")
     384        if rt.classOf(rot_controller) != rt.Rotation_list:
     385            rt.setPropertyController(inObj.controller, "Rotation", rt.Rotation_list())
     386
     387        # 기존 회전 제약 컨트롤러 확인
     388        targetRotConstraint = self.get_rot_const(inObj)
     389
     390        # 회전 제약 컨트롤러가 없으면 새로 생성
     391        if targetRotConstraint is None:
     392            targetRotConstraint = rt.Orientation_Constraint()
     393            rot_list = self.get_rot_list_controller(inObj)
     394            rt.setPropertyController(rot_list, "Available", targetRotConstraint)
     395            rot_list.setActive(rot_list.count)
     396
     397        # 타겟 추가 및 가중치 조정
     398        targetNum = targetRotConstraint.getNumTargets()
     399        targetWeight = 100.0 / (targetNum + 1)
     400        targetRotConstraint.appendTarget(inTarget, targetWeight)
     401
     402        # 기존 타겟이 있으면 가중치 재조정
     403        if targetNum > 0:
     404            newWeightScale = 100.0 - targetWeight
     405            for i in range(1, targetNum + 1):  # Maxscript는 1부터 시작
     406                newWeight = targetRotConstraint.GetWeight(i) * 0.01 * newWeightScale
     407                targetRotConstraint.SetWeight(i, newWeight)
     408
     409        # 상대적 모드 설정
     410        targetRotConstraint.relative = keepInit
     411
     412        return targetRotConstraint
     413
     414    def assign_rot_const_multi(self, inObj, inTargetArray, keepInit=False):
     415        """
     416        객체에 여러 타겟을 가진 회전 제약 컨트롤러를 할당.
     417
     418        Args:
     419            inObj: 제약을 적용할 객체
     420            inTargetArray: 타겟 객체 배열
     421            keepInit: 기존 변환 유지 여부 (기본값: False)
     422
     423        Returns:
     424            None
     425        """
     426        for item in inTargetArray:
     427            self.assign_rot_const(inObj, item, keepInit=keepInit)
     428
     429        return self.get_rot_const(inObj)
     430
     431    def add_target_to_rot_const(self, inObj, inTarget, inWeight):
     432        """
     433        기존 회전 제약 컨트롤러에 새 타겟을 추가하고 지정된 가중치 설정.
     434
     435        Args:
     436            inObj: 제약이 적용된 객체
     437            inTarget: 추가할 타겟 객체
     438            inWeight: 적용할 가중치 값
     439
     440        Returns:
     441            None
     442        """
     443        # 회전 제약 컨트롤러에 타겟 추가
     444        targetRotConstraint = self.assign_rot_const(inObj, inTarget)
     445
     446        # 마지막 타겟에 특정 가중치 적용
     447        targetNum = targetRotConstraint.getNumTargets()
     448        targetRotConstraint.SetWeight(targetNum, inWeight)
     449
     450        return targetRotConstraint
     451
     452    def assign_euler_xyz(self, inObj):
     453        """
     454        객체에 오일러 XYZ 회전 컨트롤러를 할당.
     455
     456        Args:
     457            inObj: 컨트롤러를 할당할 객체
     458
     459        Returns:
     460            None
     461        """
     462        # 회전 컨트롤러가 리스트 형태가 아니면 변환
     463        rot_controller = rt.getPropertyController(inObj.controller, "Rotation")
     464        if rt.classOf(rot_controller) != rt.Rotation_list:
     465            rt.setPropertyController(inObj.controller, "Rotation", rt.Rotation_list())
     466
     467        # 회전 리스트 컨트롤러 가져오기
     468        rotList = self.assign_rot_list(inObj)
     469
     470        # Euler_XYZ 컨트롤러 할당
     471        eulerXYZ = rt.Euler_XYZ()
     472        rt.setPropertyController(rotList, "Available", eulerXYZ)
     473        rotList.setActive(rotList.count)
     474
     475        return eulerXYZ
     476
     477    def get_lookat(self, inObj):
     478        """
     479        객체의 LookAt 제약 컨트롤러를 찾아 반환.
     480
     481        Args:
     482            inObj: 대상 객체
     483
     484        Returns:
     485            LookAt 제약 컨트롤러 (없으면 None)
     486        """
     487        returnConst = None
     488
     489        # 회전 컨트롤러가 리스트 형태인 경우
     490        rot_controller = rt.getPropertyController(inObj.controller, "Rotation")
     491        if rt.classOf(rot_controller) == rt.Rotation_list:
     492            lst = rot_controller
     493            constNum = lst.getCount()
     494            activeNum = lst.getActive()
     495
     496            # 리스트 내 모든 컨트롤러 검사
     497            for i in range(constNum):
     498                sub_controller = lst[i].controller
     499                if rt.classOf(sub_controller) == rt.LookAt_Constraint:
     500                    returnConst = sub_controller
     501                    # 현재 활성화된 컨트롤러면 즉시 반환
     502                    if activeNum == i:
     503                        return returnConst
     504
     505        # 회전 컨트롤러가 직접 LookAt_Constraint인 경우
     506        elif rt.classOf(rot_controller) == rt.LookAt_Constraint:
     507            returnConst = rot_controller
     508
     509        return returnConst
     510
     511    def assign_lookat(self, inObj, inTarget, keepInit=False):
     512        """
     513        객체에 LookAt 제약 컨트롤러를 할당하고 지정된 타겟을 추가.
     514
     515        Args:
     516            inObj: 제약을 적용할 객체
     517            inTarget: 타겟 객체
     518            keepInit: 기존 변환 유지 여부 (기본값: False)
     519
     520        Returns:
     521            LookAt 제약 컨트롤러
     522        """
     523        # 회전 컨트롤러가 리스트 형태가 아니면 변환
     524        rot_controller = rt.getPropertyController(inObj.controller, "Rotation")
     525        if rt.classOf(rot_controller) != rt.Rotation_list:
     526            rt.setPropertyController(inObj.controller, "Rotation", rt.Rotation_list())
     527
     528        # 기존 LookAt 제약 컨트롤러 확인
     529        targetRotConstraint = self.get_lookat(inObj)
     530
     531        # LookAt 제약 컨트롤러가 없으면 새로 생성
     532        if targetRotConstraint is None:
     533            targetRotConstraint = rt.LookAt_Constraint()
     534            rot_list = self.get_rot_list_controller(inObj)
     535            rt.setPropertyController(rot_list, "Available", targetRotConstraint)
     536            rot_list.setActive(rot_list.count)
     537
     538        # 타겟 추가 및 가중치 조정
     539        targetNum = targetRotConstraint.getNumTargets()
     540        targetWeight = 100.0 / (targetNum + 1)
     541        targetRotConstraint.appendTarget(inTarget, targetWeight)
     542
     543        # 기존 타겟이 있으면 가중치 재조정
     544        if targetNum > 0:
     545            newWeightScale = 100.0 - targetWeight
     546            for i in range(1, targetNum + 1):  # Maxscript는 1부터 시작
     547                newWeight = targetRotConstraint.GetWeight(i) * 0.01 * newWeightScale
     548                targetRotConstraint.SetWeight(i, newWeight)
     549
     550        # 상대적 모드 설정
     551        targetRotConstraint.relative = keepInit
     552
     553        targetRotConstraint.lookat_vector_length = 0
     554
     555        return targetRotConstraint
     556
     557    def assign_lookat_multi(self, inObj, inTargetArray, keepInit=False):
     558        """
     559        객체에 여러 타겟을 가진 LookAt 제약 컨트롤러를 할당.
     560
     561        Args:
     562            inObj: 제약을 적용할 객체
     563            inTargetArray: 타겟 객체 배열
     564            keepInit: 기존 변환 유지 여부 (기본값: False)
     565
     566        Returns:
     567            None
     568        """
     569        for item in inTargetArray:
     570            self.assign_lookat(inObj, item, keepInit=keepInit)
     571
     572        return self.get_lookat(inObj)
     573
     574    def assign_lookat_flipless(self, inObj, inTarget):
     575        """
     576        플립 없는 LookAt 제약 컨트롤러를 스크립트 기반으로 구현하여 할당.
     577        부모가 있는 객체에만 적용 가능.
     578
     579        Args:
     580            inObj: 제약을 적용할 객체 (부모가 있어야 함)
     581            inTarget: 바라볼 타겟 객체
     582
     583        Returns:
     584            None
     585        """
     586        # 객체에 부모가 있는 경우에만 실행
     587        if inObj.parent is not None:
     588            # 회전 스크립트 컨트롤러 생성
     589            targetRotConstraint = rt.Rotation_Script()
     590
     591            # 스크립트에 필요한 노드 추가
     592            targetRotConstraint.AddNode("Target", inTarget)
     593            targetRotConstraint.AddNode("Parent", inObj.parent)
     594
     595            # 객체 위치 컨트롤러 추가
     596            pos_controller = rt.getPropertyController(inObj.controller, "Position")
     597            targetRotConstraint.AddObject("NodePos", pos_controller)
     598
     599            # 회전 계산 스크립트 설정
     600            script = textwrap.dedent(r'''
     601                theTargetVector=(Target.transform.position * Inverse Parent.transform)-NodePos.value
     602                theAxis=Normalize (cross theTargetVector [1,0,0])
     603                theAngle=acos (dot (Normalize theTargetVector) [1,0,0])
     604                Quat theAngle theAxis
     605                ''')
     606            targetRotConstraint.script = script
     607
     608            # 회전 컨트롤러가 리스트 형태가 아니면 변환
     609            rot_controller = rt.getPropertyController(inObj.controller, "Rotation")
     610            if rt.classOf(rot_controller) != rt.Rotation_list:
     611                rt.setPropertyController(inObj.controller, "Rotation", rt.Rotation_list())
     612
     613            # 회전 리스트에 스크립트 컨트롤러 추가
     614            rot_list = self.get_rot_list_controller(inObj)
     615            rt.setPropertyController(rot_list, "Available", targetRotConstraint)
     616            rot_list.setActive(rot_list.count)
     617
     618            return targetRotConstraint
     619
     620    def assign_rot_const_scripted(self, inObj, inTarget):
     621        """
     622        스크립트 기반 회전 제약을 구현하여 할당.
     623        ExposeTransform을 활용한 고급 회전 제약 구현.
     624
     625        Args:
     626            inObj: 제약을 적용할 객체
     627            inTarget: 회전 참조 타겟 객체
     628
     629        Returns:
     630            생성된 회전 스크립트 컨트롤러
     631        """
     632        # 회전 스크립트 컨트롤러 생성
     633        targetRotConstraint = rt.Rotation_Script()
     634
     635        # 회전 컨트롤러 리스트에 추가
     636        rot_controller = rt.getPropertyController(inObj.controller, "Rotation")
     637        if rt.classOf(rot_controller) != rt.Rotation_list:
     638            rt.setPropertyController(inObj.controller, "Rotation", rt.Rotation_list())
     639
     640        rot_list = self.get_rot_list_controller(inObj)
     641        rt.setPropertyController(rot_list, "Available", targetRotConstraint)
     642        rot_list.setActive(rot_list.count)
     643
     644        # 헬퍼 객체 이름 생성
     645        rotPointName = self.name.replace_Type(inObj.name, self.name.get_dummy_value())
     646        rotMeasurePointName = self.name.increase_index(rotPointName, 1)
     647        rotExpName = self.name.replace_Type(inObj.name, self.name.get_exposeTm_value())
     648        rotExpName = self.name.replace_Index(rotExpName, "0")
     649
     650        print(f"dumStr: {self.name.get_dummy_value()}")
     651        print(f"exposeTmStr: {self.name.get_exposeTm_value()}")
     652        print(f"rotPointName: {rotPointName}, rotMeasurePointName: {rotMeasurePointName}, rotExpName: {rotExpName}")
     653
     654        # 헬퍼 객체 생성
     655        rotPoint = self.helper.create_point(rotPointName, size=2, boxToggle=True, crossToggle=False)
     656        rotMeasuerPoint = self.helper.create_point(rotMeasurePointName, size=3, boxToggle=True, crossToggle=False)
     657        rotExpPoint = rt.ExposeTm(name=rotExpName, size=3, box=False, cross=True, wirecolor=rt.Color(14, 255, 2))
     658
     659        # 초기 변환 설정
     660        rt.setProperty(rotPoint, "transform", rt.getProperty(inObj, "transform"))
     661        rt.setProperty(rotMeasuerPoint, "transform", rt.getProperty(inObj, "transform"))
     662        rt.setProperty(rotExpPoint, "transform", rt.getProperty(inObj, "transform"))
     663
     664        # 부모 관계 설정
     665        rotPoint.parent = inTarget
     666        rotMeasuerPoint.parent = inTarget.parent
     667        rotExpPoint.parent = inTarget
     668
     669        # ExposeTm 설정
     670        rotExpPoint.exposeNode = rotPoint
     671        rotExpPoint.useParent = False
     672        rotExpPoint.localReferenceNode = rotMeasuerPoint
     673
     674        # 회전 스크립트 생성
     675        rotScript = textwrap.dedent(r'''
     676            local targetRot = rot.localEuler
     677            local rotX = (radToDeg targetRot.x)
     678            local rotY = (radToDeg targetRot.y)
     679            local rotZ = (radToDeg targetRot.z)
     680            local result = eulerAngles rotX rotY rotZ
     681            eulerToQuat result
     682            ''')
     683
     684        # 스크립트에 노드 추가 및 표현식 설정
     685        targetRotConstraint.AddNode("rot", rotExpPoint)
     686        targetRotConstraint.SetExpression(rotScript)
     687
     688        return targetRotConstraint
     689
     690    def assign_scripted_lookat(self, inOri, inTarget):
     691        """
     692        스크립트 기반 LookAt 제약을 구현하여 할당.
     693        여러 개의 헬퍼 객체를 생성하여 복잡한 LookAt 제약 구현.
     694
     695        Args:
     696            inOri: 제약을 적용할 객체
     697            inTarget: 바라볼 타겟 객체 배열
     698
     699        Returns:
     700            None
     701        """
     702        oriObj = inOri
     703        oriParentObj = inOri.parent
     704        targetObjArray = inTarget
     705
     706        # 객체 이름 생성
     707        objName = self.name.get_string(oriObj.name)
     708        indexVal = self.name.get_index_as_digit(oriObj.name)
     709        indexNum = 0 if indexVal is False else indexVal
     710        dummyName = self.name.add_prefix_to_real_name(objName, self.name.get_dummy_value())
     711
     712        lookAtPointName = self.name.replace_Index(dummyName, str(indexNum))
     713        lookAtMeasurePointName = self.name.replace_Index(dummyName, str(indexNum+1))
     714        lookAtExpPointName = dummyName + self.name.get_exposeTm_value()
     715        lookAtExpPointName = self.name.replace_Index(lookAtExpPointName, "0")
     716
     717        # 헬퍼 객체 생성
     718        lookAtPoint = self.helper.create_point(lookAtPointName, size=2, boxToggle=True, crossToggle=False)
     719        lookAtMeasurePoint = self.helper.create_point(lookAtMeasurePointName, size=3, boxToggle=True, crossToggle=False)
     720        lookAtExpPoint = rt.ExposeTm(name=lookAtExpPointName, size=3, box=False, cross=True, wirecolor=rt.Color(14, 255, 2))
     721
     722        # 초기 변환 설정
     723        rt.setProperty(lookAtPoint, "transform", rt.getProperty(oriObj, "transform"))
     724        rt.setProperty(lookAtMeasurePoint, "transform", rt.getProperty(oriObj, "transform"))
     725        rt.setProperty(lookAtExpPoint, "transform", rt.getProperty(oriObj, "transform"))
     726
     727        # 부모 관계 설정
     728        rt.setProperty(lookAtPoint, "parent", oriParentObj)
     729        rt.setProperty(lookAtMeasurePoint, "parent", oriParentObj)
     730        rt.setProperty(lookAtExpPoint, "parent", oriParentObj)
     731
     732        # ExposeTm 설정
     733        lookAtExpPoint.exposeNode = lookAtPoint
     734        lookAtExpPoint.useParent = False
     735        lookAtExpPoint.localReferenceNode = lookAtMeasurePoint
     736
     737        # LookAt 제약 설정
     738        lookAtPoint_rot_controller = rt.LookAt_Constraint()
     739        rt.setPropertyController(lookAtPoint.controller, "Rotation", lookAtPoint_rot_controller)
     740
     741        # 타겟 추가
     742        target_weight = 100.0 / len(targetObjArray)
     743        for item in targetObjArray:
     744            lookAtPoint_rot_controller.appendTarget(item, target_weight)
     745
     746        # 오일러 XYZ 컨트롤러 생성
     747        rotControl = rt.Euler_XYZ()
     748
     749        x_controller = rt.Float_Expression()
     750        y_controller = rt.Float_Expression()
     751        z_controller = rt.Float_Expression()
     752
     753        # 스칼라 타겟 추가
     754        x_controller.AddScalarTarget("rotX", rt.getPropertyController(lookAtExpPoint, "localEulerX"))
     755        y_controller.AddScalarTarget("rotY", rt.getPropertyController(lookAtExpPoint, "localEulerY"))
     756        z_controller.AddScalarTarget("rotZ", rt.getPropertyController(lookAtExpPoint, "localEulerZ"))
     757
     758        # 표현식 설정
     759        x_controller.SetExpression("rotX")
     760        y_controller.SetExpression("rotY")
     761        z_controller.SetExpression("rotZ")
     762
     763        # 각 축별 회전에 Float_Expression 컨트롤러 할당
     764        rt.setPropertyController(rotControl, "X_Rotation", x_controller)
     765        rt.setPropertyController(rotControl, "Y_Rotation", y_controller)
     766        rt.setPropertyController(rotControl, "Z_Rotation", z_controller)
     767
     768        # 회전 컨트롤러 목록 확인 또는 생성
     769        rot_controller = rt.getPropertyController(oriObj.controller, "Rotation")
     770        if rt.classOf(rot_controller) != rt.Rotation_list:
     771            rt.setPropertyController(oriObj.controller, "Rotation", rt.Rotation_list())
     772
     773        # 회전 리스트에 오일러 컨트롤러 추가
     774        rot_list = self.get_rot_list_controller(oriObj)
     775        rt.setPropertyController(rot_list, "Available", rotControl)
     776
     777        # 컨트롤러 이름 설정
     778        rot_controller_num = rot_list.count
     779        rot_list.setname(rot_controller_num, "Script Rotation")
     780
     781        # 컨트롤러 업데이트
     782        x_controller.Update()
     783        y_controller.Update()
     784        z_controller.Update()
     785
     786        return {"lookAt":lookAtPoint_rot_controller, "x":x_controller, "y":y_controller, "z":z_controller}
     787
     788    def assign_attachment(self, inPlacedObj, inSurfObj, bAlign=False, shiftAxis=(0, 0, 1), shiftAmount=3.0):
     789        """
     790        객체를 다른 객체의 표면에 부착하는 Attachment 제약 컨트롤러 할당.
     791
     792        Args:
     793            inPlacedObj: 부착될 객체
     794            inSurfObj: 표면 객체
     795            bAlign: 표면 법선에 맞춰 정렬할지 여부
     796            shiftAxis: 레이 방향 축 (기본값: Z축)
     797            shiftAmount: 레이 거리 (기본값: 3.0)
     798
     799        Returns:
     800            생성된 Attachment 컨트롤러 또는 None (실패 시)
     801        """
     802        # 현재 변환 행렬 백업 및 시작 위치 계산
     803        placedObjTm = rt.getProperty(inPlacedObj, "transform")
     804        rt.preTranslate(placedObjTm, rt.Point3(shiftAxis[0], shiftAxis[1], shiftAxis[2]) * (-shiftAmount))
     805        dirStartPos = placedObjTm.pos
     806
     807        # 끝 위치 계산
     808        placedObjTm = rt.getProperty(inPlacedObj, "transform")
     809        rt.preTranslate(placedObjTm, rt.Point3(shiftAxis[0], shiftAxis[1], shiftAxis[2]) * shiftAmount)
     810        dirEndPos = placedObjTm.pos
     811
     812        # 방향 벡터 및 레이 생성
     813        dirVec = dirEndPos - dirStartPos
     814        dirRay = rt.ray(dirEndPos, -dirVec)
     815
     816        # 레이 교차 검사
     817        intersectArr = rt.intersectRayEx(inSurfObj, dirRay)
     818
     819        # 교차점이 있으면 Attachment 제약 생성
     820        if intersectArr is not None:
     821            # 위치 컨트롤러 리스트 생성 또는 가져오기
     822            posListConst = self.assign_pos_list(inPlacedObj)
     823
     824            # Attachment 컨트롤러 생성
     825            attConst = rt.Attachment()
     826            rt.setPropertyController(posListConst, "Available", attConst)
     827
     828            # 제약 속성 설정
     829            attConst.node = inSurfObj
     830            attConst.align = bAlign
     831
     832            # 부착 키 추가
     833            attachKey = rt.attachCtrl.addNewKey(attConst, 0)
     834            attachKey.face = intersectArr[2] - 1  # 인덱스 조정 (MAXScript는 1부터, Python은 0부터)
     835            attachKey.coord = intersectArr[3]
     836
     837            return attConst
     838        else:
     839            return None
     840
     841    def get_pos_controllers_name_from_list(self, inObj):
     842        """
     843        객체의 위치 컨트롤러 리스트에서 각 컨트롤러의 이름을 가져옴.
     844
     845        Args:
     846            inObj: 대상 객체
     847
     848        Returns:
     849            컨트롤러 이름 배열
     850        """
     851        returnNameArray = []
     852
     853        # 위치 컨트롤러가 리스트 형태인지 확인
     854        pos_controller = rt.getPropertyController(inObj.controller, "Position")
     855        if rt.classOf(pos_controller) == rt.Position_list:
     856            posList = pos_controller
     857
     858            # 각 컨트롤러의 이름을 배열에 추가
     859            for i in range(1, posList.count + 1):  # MAXScript는 1부터 시작
     860                returnNameArray.append(posList.getName(i))
     861
     862        return returnNameArray
     863
     864    def get_pos_controllers_weight_from_list(self, inObj):
     865        """
     866        객체의 위치 컨트롤러 리스트에서 각 컨트롤러의 가중치를 가져옴.
     867
     868        Args:
     869            inObj: 대상 객체
     870
     871        Returns:
     872            컨트롤러 가중치 배열
     873        """
     874        returnWeightArray = []
     875
     876        # 위치 컨트롤러가 리스트 형태인지 확인
     877        pos_controller = rt.getPropertyController(inObj.controller, "Position")
     878        if rt.classOf(pos_controller) == rt.Position_list:
     879            posList = pos_controller
     880
     881            # 가중치 배열 가져오기
     882            returnWeightArray = list(posList.weight)
     883
     884        return returnWeightArray
     885
     886    def set_pos_controllers_name_in_list(self, inObj, inLayerNum, inNewName):
     887        """
     888        객체의 위치 컨트롤러 리스트에서 특정 컨트롤러의 이름을 설정.
     889
     890        Args:
     891            inObj: 대상 객체
     892            inLayerNum: 컨트롤러 인덱스 (1부터 시작)
     893            inNewName: 새 이름
     894
     895        Returns:
     896            None
     897        """
     898        # 위치 컨트롤러 리스트 가져오기
     899        listCtr = self.get_pos_list_controller(inObj)
     900
     901        # 리스트가 있으면 이름 설정
     902        if listCtr is not None:
     903            listCtr.setName(inLayerNum, inNewName)
     904
     905    def set_pos_controllers_weight_in_list(self, inObj, inLayerNum, inNewWeight):
     906        """
     907        객체의 위치 컨트롤러 리스트에서 특정 컨트롤러의 가중치를 설정.
     908
     909        Args:
     910            inObj: 대상 객체
     911            inLayerNum: 컨트롤러 인덱스 (1부터 시작)
     912            inNewWeight: 새 가중치
     913
     914        Returns:
     915            None
     916        """
     917        # 위치 컨트롤러 리스트 가져오기
     918        listCtr = self.get_pos_list_controller(inObj)
     919
     920        # 리스트가 있으면 가중치 설정
     921        if listCtr is not None:
     922            listCtr.weight[inLayerNum] = inNewWeight
     923
     924    def get_rot_controllers_name_from_list(self, inObj):
     925        """
     926        객체의 회전 컨트롤러 리스트에서 각 컨트롤러의 이름을 가져옴.
     927
     928        Args:
     929            inObj: 대상 객체
     930
     931        Returns:
     932            컨트롤러 이름 배열
     933        """
     934        returnNameArray = []
     935
     936        # 회전 컨트롤러가 리스트 형태인지 확인
     937        rot_controller = rt.getPropertyController(inObj.controller, "Rotation")
     938        if rt.classOf(rot_controller) == rt.Rotation_list:
     939            rotList = rot_controller
     940
     941            # 각 컨트롤러의 이름을 배열에 추가
     942            for i in range(1, rotList.count + 1):  # MAXScript는 1부터 시작
     943                returnNameArray.append(rotList.getName(i))
     944
     945        return returnNameArray
     946
     947    def get_rot_controllers_weight_from_list(self, inObj):
     948        """
     949        객체의 회전 컨트롤러 리스트에서 각 컨트롤러의 가중치를 가져옴.
     950
     951        Args:
     952            inObj: 대상 객체
     953
     954        Returns:
     955            컨트롤러 가중치 배열
     956        """
     957        returnWeightArray = []
     958
     959        # 회전 컨트롤러가 리스트 형태인지 확인
     960        rot_controller = rt.getPropertyController(inObj.controller, "Rotation")
     961        if rt.classOf(rot_controller) == rt.Rotation_list:
     962            rotList = rot_controller
     963
     964            # 가중치 배열 가져오기
     965            returnWeightArray = list(rotList.weight)
     966
     967        return returnWeightArray
     968
     969    def set_rot_controllers_name_in_list(self, inObj, inLayerNum, inNewName):
     970        """
     971        객체의 회전 컨트롤러 리스트에서 특정 컨트롤러의 이름을 설정.
     972
     973        Args:
     974            inObj: 대상 객체
     975            inLayerNum: 컨트롤러 인덱스 (1부터 시작)
     976            inNewName: 새 이름
     977
     978        Returns:
     979            None
     980        """
     981        # 회전 컨트롤러 리스트 가져오기
     982        listCtr = self.get_rot_list_controller(inObj)
     983
     984        # 리스트가 있으면 이름 설정
     985        if listCtr is not None:
     986            listCtr.setName(inLayerNum, inNewName)
     987
     988    def set_rot_controllers_weight_in_list(self, inObj, inLayerNum, inNewWeight):
     989        """
     990        객체의 회전 컨트롤러 리스트에서 특정 컨트롤러의 가중치를 설정.
     991
     992        Args:
     993            inObj: 대상 객체
     994            inLayerNum: 컨트롤러 인덱스 (1부터 시작)
     995            inNewWeight: 새 가중치
     996
     997        Returns:
     998            None
     999        """
    1000        # 회전 컨트롤러 리스트 가져오기
    1001        listCtr = self.get_rot_list_controller(inObj)
    1002
    1003        # 리스트가 있으면 가중치 설정
    1004        if listCtr is not None:
    1005            listCtr.weight[inLayerNum] = inNewWeight

제약(Constraint) 관련 기능을 제공하는 클래스. MAXScript의 _Constraint 구조체 개념을 Python으로 재구현한 클래스이며, 3ds Max의 기능들을 pymxs API를 통해 제어합니다.

Constraint(nameService=None, helperService=None) View Source

    25    def __init__(self, nameService=None, helperService=None):
    26        """
    27        클래스 초기화.
    28
    29        Args:
    30            nameService: 이름 처리 서비스 (제공되지 않으면 새로 생성)
    31            helperService: 헬퍼 객체 관련 서비스 (제공되지 않으면 새로 생성)
    32        """
    33        self.name = nameService if nameService else Name()
    34        self.helper = helperService if helperService else Helper(nameService=self.name) # Pass the potentially newly created nameService

클래스 초기화.

Args: nameService: 이름 처리 서비스 (제공되지 않으면 새로 생성) helperService: 헬퍼 객체 관련 서비스 (제공되지 않으면 새로 생성)

name
helper
def collapse(self, inObj): View Source

    36    def collapse(self, inObj):
    37        """
    38        비 Biped 객체의 트랜스폼 컨트롤러를 기본 컨트롤러로 초기화하고 현재 변환 상태 유지.
    39
    40        Args:
    41            inObj: 초기화할 대상 객체
    42
    43        Returns:
    44            None
    45        """
    46        if rt.classOf(inObj) != rt.Biped_Object:
    47            # 현재 변환 상태 백업
    48            tempTransform = rt.getProperty(inObj, "transform")
    49
    50            # 기본 컨트롤러로 위치, 회전, 스케일 초기화
    51            rt.setPropertyController(inObj.controller, "Position", rt.Position_XYZ())
    52            rt.setPropertyController(inObj.controller, "Rotation", rt.Euler_XYZ())
    53            rt.setPropertyController(inObj.controller, "Scale", rt.Bezier_Scale())
    54
    55            # 백업한 변환 상태 복원
    56            rt.setProperty(inObj, "transform", tempTransform)

비 Biped 객체의 트랜스폼 컨트롤러를 기본 컨트롤러로 초기화하고 현재 변환 상태 유지.

Args: inObj: 초기화할 대상 객체

Returns: None

def set_active_last(self, inObj): View Source

    58    def set_active_last(self, inObj):
    59        """
    60        객체의 위치와 회전 컨트롤러 리스트에서 마지막 컨트롤러를 활성화.
    61
    62        Args:
    63            inObj: 대상 객체
    64
    65        Returns:
    66            None
    67        """
    68        # 위치 컨트롤러가 리스트 형태면 마지막 컨트롤러 활성화
    69        pos_controller = rt.getPropertyController(inObj.controller, "Position")
    70        if rt.classOf(pos_controller) == rt.Position_list:
    71            pos_controller.setActive(pos_controller.count)
    72
    73        # 회전 컨트롤러가 리스트 형태면 마지막 컨트롤러 활성화
    74        rot_controller = rt.getPropertyController(inObj.controller, "Rotation")
    75        if rt.classOf(rot_controller) == rt.Rotation_list:
    76            rot_controller.setActive(rot_controller.count)

객체의 위치와 회전 컨트롤러 리스트에서 마지막 컨트롤러를 활성화.

Args: inObj: 대상 객체

Returns: None

def get_pos_list_controller(self, inObj): View Source

    78    def get_pos_list_controller(self, inObj):
    79        """
    80        객체의 위치 리스트 컨트롤러를 반환.
    81
    82        Args:
    83            inObj: 대상 객체
    84
    85        Returns:
    86            위치 리스트 컨트롤러 (없으면 None)
    87        """
    88        returnPosListCtr = None
    89
    90        # 위치 컨트롤러가 리스트 형태인지 확인
    91        pos_controller = rt.getPropertyController(inObj.controller, "Position")
    92        if rt.classOf(pos_controller) == rt.Position_list:
    93            returnPosListCtr = pos_controller
    94
    95        return returnPosListCtr

객체의 위치 리스트 컨트롤러를 반환.

Args: inObj: 대상 객체

Returns: 위치 리스트 컨트롤러 (없으면 None)

def assign_pos_list(self, inObj): View Source

     97    def assign_pos_list(self, inObj):
     98        """
     99        객체에 위치 리스트 컨트롤러를 할당하거나 기존 것을 반환.
    100
    101        Args:
    102            inObj: 대상 객체
    103
    104        Returns:
    105            위치 리스트 컨트롤러
    106        """
    107        returnPosListCtr = None
    108
    109        # 현재 위치 컨트롤러 확인
    110        pos_controller = rt.getPropertyController(inObj.controller, "Position")
    111
    112        # 리스트 형태가 아니면 새로 생성
    113        if rt.classOf(pos_controller) != rt.Position_list:
    114            returnPosListCtr = rt.Position_list()
    115            rt.setPropertyController(inObj.controller, "Position", returnPosListCtr)
    116            return returnPosListCtr
    117
    118        # 이미 리스트 형태면 그대로 반환
    119        if rt.classOf(pos_controller) == rt.Position_list:
    120            returnPosListCtr = pos_controller
    121
    122        return returnPosListCtr

객체에 위치 리스트 컨트롤러를 할당하거나 기존 것을 반환.

Args: inObj: 대상 객체

Returns: 위치 리스트 컨트롤러

def get_pos_const(self, inObj): View Source

    124    def get_pos_const(self, inObj):
    125        """
    126        객체의 위치 제약 컨트롤러를 찾아 반환.
    127
    128        Args:
    129            inObj: 대상 객체
    130
    131        Returns:
    132            위치 제약 컨트롤러 (없으면 None)
    133        """
    134        returnConst = None
    135
    136        # 위치 컨트롤러가 리스트 형태인 경우
    137        pos_controller = rt.getPropertyController(inObj.controller, "Position")
    138        if rt.classOf(pos_controller) == rt.Position_list:
    139            lst = pos_controller
    140            constNum = lst.getCount()
    141            activeNum = lst.getActive()
    142
    143            # 리스트 내 모든 컨트롤러 검사
    144            for i in range(constNum):
    145                sub_controller = lst[i].controller
    146                if rt.classOf(sub_controller) == rt.Position_Constraint:
    147                    returnConst = sub_controller
    148                    # 현재 활성화된 컨트롤러면 즉시 반환
    149                    if activeNum == i:
    150                        return returnConst
    151
    152        # 위치 컨트롤러가 직접 Position_Constraint인 경우
    153        elif rt.classOf(pos_controller) == rt.Position_Constraint:
    154            returnConst = pos_controller
    155
    156        return returnConst

객체의 위치 제약 컨트롤러를 찾아 반환.

Args: inObj: 대상 객체

Returns: 위치 제약 컨트롤러 (없으면 None)

def assign_pos_const(self, inObj, inTarget, keepInit=False): View Source

    158    def assign_pos_const(self, inObj, inTarget, keepInit=False):
    159        """
    160        객체에 위치 제약 컨트롤러를 할당하고 지정된 타겟을 추가.
    161
    162        Args:
    163            inObj: 제약을 적용할 객체
    164            inTarget: 타겟 객체
    165            keepInit: 기존 변환 유지 여부 (기본값: False)
    166
    167        Returns:
    168            위치 제약 컨트롤러
    169        """
    170        # 위치 컨트롤러가 리스트 형태가 아니면 변환
    171        pos_controller = rt.getPropertyController(inObj.controller, "Position")
    172        if rt.classOf(pos_controller) != rt.Position_list:
    173            rt.setPropertyController(inObj.controller, "Position", rt.Position_list())
    174
    175        # 기존 위치 제약 컨트롤러 확인
    176        targetPosConstraint = self.get_pos_const(inObj)
    177
    178        # 위치 제약 컨트롤러가 없으면 새로 생성
    179        if targetPosConstraint is None:
    180            targetPosConstraint = rt.Position_Constraint()
    181            pos_list = self.get_pos_list_controller(inObj)
    182            rt.setPropertyController(pos_list, "Available", targetPosConstraint)
    183            pos_list.setActive(pos_list.count)
    184
    185        # 타겟 추가 및 가중치 조정
    186        targetNum = targetPosConstraint.getNumTargets()
    187        targetWeight = 100.0 / (targetNum + 1)
    188        targetPosConstraint.appendTarget(inTarget, targetWeight)
    189
    190        # 기존 타겟이 있으면 가중치 재조정
    191        if targetNum > 0:
    192            newWeightScale = 100.0 - targetWeight
    193            for i in range(1, targetNum + 1):  # Maxscript는 1부터 시작
    194                newWeight = targetPosConstraint.GetWeight(i) * 0.01 * newWeightScale
    195                targetPosConstraint.SetWeight(i, newWeight)
    196
    197        # 상대적 모드 설정
    198        targetPosConstraint.relative = keepInit
    199
    200        return targetPosConstraint

객체에 위치 제약 컨트롤러를 할당하고 지정된 타겟을 추가.

Args: inObj: 제약을 적용할 객체 inTarget: 타겟 객체 keepInit: 기존 변환 유지 여부 (기본값: False)

Returns: 위치 제약 컨트롤러

def assign_pos_const_multi(self, inObj, inTargetArray, keepInit=False): View Source

    202    def assign_pos_const_multi(self, inObj, inTargetArray, keepInit=False):
    203        """
    204        객체에 여러 타겟을 가진 위치 제약 컨트롤러를 할당.
    205
    206        Args:
    207            inObj: 제약을 적용할 객체
    208            inTargetArray: 타겟 객체 배열
    209            keepInit: 기존 변환 유지 여부 (기본값: False)
    210
    211        Returns:
    212            None
    213        """
    214        for item in inTargetArray:
    215            self.assign_pos_const(inObj, item, keepInit=keepInit)
    216
    217        return self.get_pos_const(inObj)

객체에 여러 타겟을 가진 위치 제약 컨트롤러를 할당.

Args: inObj: 제약을 적용할 객체 inTargetArray: 타겟 객체 배열 keepInit: 기존 변환 유지 여부 (기본값: False)

Returns: None

def add_target_to_pos_const(self, inObj, inTarget, inWeight): View Source

    219    def add_target_to_pos_const(self, inObj, inTarget, inWeight):
    220        """
    221        기존 위치 제약 컨트롤러에 새 타겟을 추가하고 지정된 가중치 설정.
    222
    223        Args:
    224            inObj: 제약이 적용된 객체
    225            inTarget: 추가할 타겟 객체
    226            inWeight: 적용할 가중치 값
    227
    228        Returns:
    229            None
    230        """
    231        # 위치 제약 컨트롤러에 타겟 추가
    232        targetPosConst = self.assign_pos_const(inObj, inTarget)
    233
    234        # 마지막 타겟에 특정 가중치 적용
    235        targetNum = targetPosConst.getNumTargets()
    236        targetPosConst.SetWeight(targetNum, inWeight)
    237
    238        return targetPosConst

기존 위치 제약 컨트롤러에 새 타겟을 추가하고 지정된 가중치 설정.

Args: inObj: 제약이 적용된 객체 inTarget: 추가할 타겟 객체 inWeight: 적용할 가중치 값

Returns: None

def assign_pos_xyz(self, inObj): View Source

    240    def assign_pos_xyz(self, inObj):
    241        """
    242        객체에 위치 XYZ 컨트롤러를 할당.
    243
    244        Args:
    245            inObj: 컨트롤러를 할당할 객체
    246
    247        Returns:
    248            None
    249        """
    250        # 위치 컨트롤러가 리스트 형태가 아니면 변환
    251        pos_controller = rt.getPropertyController(inObj.controller, "Position")
    252        if rt.classOf(pos_controller) != rt.Position_list:
    253            rt.setPropertyController(inObj.controller, "Position", rt.Position_list())
    254
    255        # 위치 리스트 컨트롤러 가져오기
    256        posList = self.assign_pos_list(inObj)
    257
    258        # Position_XYZ 컨트롤러 할당
    259        posXYZ = rt.Position_XYZ()
    260        rt.setPropertyController(posList, "Available", posXYZ)
    261        posList.setActive(posList.count)
    262
    263        return posXYZ

객체에 위치 XYZ 컨트롤러를 할당.

Args: inObj: 컨트롤러를 할당할 객체

Returns: None

def assign_pos_script_controller(self, inObj): View Source

    265    def assign_pos_script_controller(self, inObj):
    266        """
    267        객체에 스크립트 기반 위치 컨트롤러를 할당.
    268
    269        Args:
    270            inObj: 컨트롤러를 할당할 객체
    271
    272        Returns:
    273            None
    274        """
    275        # 위치 컨트롤러가 리스트 형태가 아니면 변환
    276        pos_controller = rt.getPropertyController(inObj.controller, "Position")
    277        if rt.classOf(pos_controller) != rt.Position_list:
    278            rt.setPropertyController(inObj.controller, "Position", rt.Position_list())
    279
    280        # 위치 리스트 컨트롤러 가져오기
    281        posList = self.assign_pos_list(inObj)
    282
    283        # 스크립트 기반 위치 컨트롤러 할당
    284        scriptPos = rt.Position_Script()
    285        rt.setPropertyController(posList, "Available", scriptPos)
    286        posList.setActive(posList.count)
    287
    288        return scriptPos

객체에 스크립트 기반 위치 컨트롤러를 할당.

Args: inObj: 컨트롤러를 할당할 객체

Returns: None

def get_rot_list_controller(self, inObj): View Source

    290    def get_rot_list_controller(self, inObj):
    291        """
    292        객체의 회전 리스트 컨트롤러를 반환.
    293
    294        Args:
    295            inObj: 대상 객체
    296
    297        Returns:
    298            회전 리스트 컨트롤러 (없으면 None)
    299        """
    300        returnRotListCtr = None
    301
    302        # 회전 컨트롤러가 리스트 형태인지 확인
    303        rot_controller = rt.getPropertyController(inObj.controller, "Rotation")
    304        if rt.classOf(rot_controller) == rt.Rotation_list:
    305            returnRotListCtr = rot_controller
    306
    307        return returnRotListCtr

객체의 회전 리스트 컨트롤러를 반환.

Args: inObj: 대상 객체

Returns: 회전 리스트 컨트롤러 (없으면 None)

def assign_rot_list(self, inObj): View Source

    309    def assign_rot_list(self, inObj):
    310        """
    311        객체에 회전 리스트 컨트롤러를 할당하거나 기존 것을 반환.
    312
    313        Args:
    314            inObj: 대상 객체
    315
    316        Returns:
    317            회전 리스트 컨트롤러
    318        """
    319        returnRotListCtr = None
    320
    321        # 현재 회전 컨트롤러 확인
    322        rot_controller = rt.getPropertyController(inObj.controller, "Rotation")
    323
    324        # 리스트 형태가 아니면 새로 생성
    325        if rt.classOf(rot_controller) != rt.Rotation_list:
    326            returnRotListCtr = rt.Rotation_list()
    327            rt.setPropertyController(inObj.controller, "Rotation", returnRotListCtr)
    328            return returnRotListCtr
    329
    330        # 이미 리스트 형태면 그대로 반환
    331        if rt.classOf(rot_controller) == rt.Rotation_list:
    332            returnRotListCtr = rot_controller
    333
    334        return returnRotListCtr

객체에 회전 리스트 컨트롤러를 할당하거나 기존 것을 반환.

Args: inObj: 대상 객체

Returns: 회전 리스트 컨트롤러

def get_rot_const(self, inObj): View Source

    336    def get_rot_const(self, inObj):
    337        """
    338        객체의 회전 제약 컨트롤러를 찾아 반환.
    339
    340        Args:
    341            inObj: 대상 객체
    342
    343        Returns:
    344            회전 제약 컨트롤러 (없으면 None)
    345        """
    346        returnConst = None
    347
    348        # 회전 컨트롤러가 리스트 형태인 경우
    349        rot_controller = rt.getPropertyController(inObj.controller, "Rotation")
    350        if rt.classOf(rot_controller) == rt.Rotation_list:
    351            lst = rot_controller
    352            constNum = lst.getCount()
    353            activeNum = lst.getActive()
    354
    355            # 리스트 내 모든 컨트롤러 검사
    356            for i in range(constNum):  # Maxscript는 1부터 시작
    357                sub_controller = lst[i].controller
    358                if rt.classOf(sub_controller) == rt.Orientation_Constraint:
    359                    returnConst = sub_controller
    360                    # 현재 활성화된 컨트롤러면 즉시 반환
    361                    if activeNum == i:
    362                        return returnConst
    363
    364        # 회전 컨트롤러가 직접 Orientation_Constraint인 경우
    365        elif rt.classOf(rot_controller) == rt.Orientation_Constraint:
    366            returnConst = rot_controller
    367
    368        return returnConst

객체의 회전 제약 컨트롤러를 찾아 반환.

Args: inObj: 대상 객체

Returns: 회전 제약 컨트롤러 (없으면 None)

def assign_rot_const(self, inObj, inTarget, keepInit=False): View Source

    370    def assign_rot_const(self, inObj, inTarget, keepInit=False):
    371        """
    372        객체에 회전 제약 컨트롤러를 할당하고 지정된 타겟을 추가.
    373
    374        Args:
    375            inObj: 제약을 적용할 객체
    376            inTarget: 타겟 객체
    377            keepInit: 기존 변환 유지 여부 (기본값: False)
    378
    379        Returns:
    380            회전 제약 컨트롤러
    381        """
    382        # 회전 컨트롤러가 리스트 형태가 아니면 변환
    383        rot_controller = rt.getPropertyController(inObj.controller, "Rotation")
    384        if rt.classOf(rot_controller) != rt.Rotation_list:
    385            rt.setPropertyController(inObj.controller, "Rotation", rt.Rotation_list())
    386
    387        # 기존 회전 제약 컨트롤러 확인
    388        targetRotConstraint = self.get_rot_const(inObj)
    389
    390        # 회전 제약 컨트롤러가 없으면 새로 생성
    391        if targetRotConstraint is None:
    392            targetRotConstraint = rt.Orientation_Constraint()
    393            rot_list = self.get_rot_list_controller(inObj)
    394            rt.setPropertyController(rot_list, "Available", targetRotConstraint)
    395            rot_list.setActive(rot_list.count)
    396
    397        # 타겟 추가 및 가중치 조정
    398        targetNum = targetRotConstraint.getNumTargets()
    399        targetWeight = 100.0 / (targetNum + 1)
    400        targetRotConstraint.appendTarget(inTarget, targetWeight)
    401
    402        # 기존 타겟이 있으면 가중치 재조정
    403        if targetNum > 0:
    404            newWeightScale = 100.0 - targetWeight
    405            for i in range(1, targetNum + 1):  # Maxscript는 1부터 시작
    406                newWeight = targetRotConstraint.GetWeight(i) * 0.01 * newWeightScale
    407                targetRotConstraint.SetWeight(i, newWeight)
    408
    409        # 상대적 모드 설정
    410        targetRotConstraint.relative = keepInit
    411
    412        return targetRotConstraint

객체에 회전 제약 컨트롤러를 할당하고 지정된 타겟을 추가.

Args: inObj: 제약을 적용할 객체 inTarget: 타겟 객체 keepInit: 기존 변환 유지 여부 (기본값: False)

Returns: 회전 제약 컨트롤러

def assign_rot_const_multi(self, inObj, inTargetArray, keepInit=False): View Source

    414    def assign_rot_const_multi(self, inObj, inTargetArray, keepInit=False):
    415        """
    416        객체에 여러 타겟을 가진 회전 제약 컨트롤러를 할당.
    417
    418        Args:
    419            inObj: 제약을 적용할 객체
    420            inTargetArray: 타겟 객체 배열
    421            keepInit: 기존 변환 유지 여부 (기본값: False)
    422
    423        Returns:
    424            None
    425        """
    426        for item in inTargetArray:
    427            self.assign_rot_const(inObj, item, keepInit=keepInit)
    428
    429        return self.get_rot_const(inObj)

객체에 여러 타겟을 가진 회전 제약 컨트롤러를 할당.

Args: inObj: 제약을 적용할 객체 inTargetArray: 타겟 객체 배열 keepInit: 기존 변환 유지 여부 (기본값: False)

Returns: None

def add_target_to_rot_const(self, inObj, inTarget, inWeight): View Source

    431    def add_target_to_rot_const(self, inObj, inTarget, inWeight):
    432        """
    433        기존 회전 제약 컨트롤러에 새 타겟을 추가하고 지정된 가중치 설정.
    434
    435        Args:
    436            inObj: 제약이 적용된 객체
    437            inTarget: 추가할 타겟 객체
    438            inWeight: 적용할 가중치 값
    439
    440        Returns:
    441            None
    442        """
    443        # 회전 제약 컨트롤러에 타겟 추가
    444        targetRotConstraint = self.assign_rot_const(inObj, inTarget)
    445
    446        # 마지막 타겟에 특정 가중치 적용
    447        targetNum = targetRotConstraint.getNumTargets()
    448        targetRotConstraint.SetWeight(targetNum, inWeight)
    449
    450        return targetRotConstraint

기존 회전 제약 컨트롤러에 새 타겟을 추가하고 지정된 가중치 설정.

Args: inObj: 제약이 적용된 객체 inTarget: 추가할 타겟 객체 inWeight: 적용할 가중치 값

Returns: None

def assign_euler_xyz(self, inObj): View Source

    452    def assign_euler_xyz(self, inObj):
    453        """
    454        객체에 오일러 XYZ 회전 컨트롤러를 할당.
    455
    456        Args:
    457            inObj: 컨트롤러를 할당할 객체
    458
    459        Returns:
    460            None
    461        """
    462        # 회전 컨트롤러가 리스트 형태가 아니면 변환
    463        rot_controller = rt.getPropertyController(inObj.controller, "Rotation")
    464        if rt.classOf(rot_controller) != rt.Rotation_list:
    465            rt.setPropertyController(inObj.controller, "Rotation", rt.Rotation_list())
    466
    467        # 회전 리스트 컨트롤러 가져오기
    468        rotList = self.assign_rot_list(inObj)
    469
    470        # Euler_XYZ 컨트롤러 할당
    471        eulerXYZ = rt.Euler_XYZ()
    472        rt.setPropertyController(rotList, "Available", eulerXYZ)
    473        rotList.setActive(rotList.count)
    474
    475        return eulerXYZ

객체에 오일러 XYZ 회전 컨트롤러를 할당.

Args: inObj: 컨트롤러를 할당할 객체

Returns: None

def get_lookat(self, inObj): View Source

    477    def get_lookat(self, inObj):
    478        """
    479        객체의 LookAt 제약 컨트롤러를 찾아 반환.
    480
    481        Args:
    482            inObj: 대상 객체
    483
    484        Returns:
    485            LookAt 제약 컨트롤러 (없으면 None)
    486        """
    487        returnConst = None
    488
    489        # 회전 컨트롤러가 리스트 형태인 경우
    490        rot_controller = rt.getPropertyController(inObj.controller, "Rotation")
    491        if rt.classOf(rot_controller) == rt.Rotation_list:
    492            lst = rot_controller
    493            constNum = lst.getCount()
    494            activeNum = lst.getActive()
    495
    496            # 리스트 내 모든 컨트롤러 검사
    497            for i in range(constNum):
    498                sub_controller = lst[i].controller
    499                if rt.classOf(sub_controller) == rt.LookAt_Constraint:
    500                    returnConst = sub_controller
    501                    # 현재 활성화된 컨트롤러면 즉시 반환
    502                    if activeNum == i:
    503                        return returnConst
    504
    505        # 회전 컨트롤러가 직접 LookAt_Constraint인 경우
    506        elif rt.classOf(rot_controller) == rt.LookAt_Constraint:
    507            returnConst = rot_controller
    508
    509        return returnConst

객체의 LookAt 제약 컨트롤러를 찾아 반환.

Args: inObj: 대상 객체

Returns: LookAt 제약 컨트롤러 (없으면 None)

def assign_lookat(self, inObj, inTarget, keepInit=False): View Source

    511    def assign_lookat(self, inObj, inTarget, keepInit=False):
    512        """
    513        객체에 LookAt 제약 컨트롤러를 할당하고 지정된 타겟을 추가.
    514
    515        Args:
    516            inObj: 제약을 적용할 객체
    517            inTarget: 타겟 객체
    518            keepInit: 기존 변환 유지 여부 (기본값: False)
    519
    520        Returns:
    521            LookAt 제약 컨트롤러
    522        """
    523        # 회전 컨트롤러가 리스트 형태가 아니면 변환
    524        rot_controller = rt.getPropertyController(inObj.controller, "Rotation")
    525        if rt.classOf(rot_controller) != rt.Rotation_list:
    526            rt.setPropertyController(inObj.controller, "Rotation", rt.Rotation_list())
    527
    528        # 기존 LookAt 제약 컨트롤러 확인
    529        targetRotConstraint = self.get_lookat(inObj)
    530
    531        # LookAt 제약 컨트롤러가 없으면 새로 생성
    532        if targetRotConstraint is None:
    533            targetRotConstraint = rt.LookAt_Constraint()
    534            rot_list = self.get_rot_list_controller(inObj)
    535            rt.setPropertyController(rot_list, "Available", targetRotConstraint)
    536            rot_list.setActive(rot_list.count)
    537
    538        # 타겟 추가 및 가중치 조정
    539        targetNum = targetRotConstraint.getNumTargets()
    540        targetWeight = 100.0 / (targetNum + 1)
    541        targetRotConstraint.appendTarget(inTarget, targetWeight)
    542
    543        # 기존 타겟이 있으면 가중치 재조정
    544        if targetNum > 0:
    545            newWeightScale = 100.0 - targetWeight
    546            for i in range(1, targetNum + 1):  # Maxscript는 1부터 시작
    547                newWeight = targetRotConstraint.GetWeight(i) * 0.01 * newWeightScale
    548                targetRotConstraint.SetWeight(i, newWeight)
    549
    550        # 상대적 모드 설정
    551        targetRotConstraint.relative = keepInit
    552
    553        targetRotConstraint.lookat_vector_length = 0
    554
    555        return targetRotConstraint

객체에 LookAt 제약 컨트롤러를 할당하고 지정된 타겟을 추가.

Args: inObj: 제약을 적용할 객체 inTarget: 타겟 객체 keepInit: 기존 변환 유지 여부 (기본값: False)

Returns: LookAt 제약 컨트롤러

def assign_lookat_multi(self, inObj, inTargetArray, keepInit=False): View Source

    557    def assign_lookat_multi(self, inObj, inTargetArray, keepInit=False):
    558        """
    559        객체에 여러 타겟을 가진 LookAt 제약 컨트롤러를 할당.
    560
    561        Args:
    562            inObj: 제약을 적용할 객체
    563            inTargetArray: 타겟 객체 배열
    564            keepInit: 기존 변환 유지 여부 (기본값: False)
    565
    566        Returns:
    567            None
    568        """
    569        for item in inTargetArray:
    570            self.assign_lookat(inObj, item, keepInit=keepInit)
    571
    572        return self.get_lookat(inObj)

객체에 여러 타겟을 가진 LookAt 제약 컨트롤러를 할당.

Args: inObj: 제약을 적용할 객체 inTargetArray: 타겟 객체 배열 keepInit: 기존 변환 유지 여부 (기본값: False)

Returns: None

def assign_lookat_flipless(self, inObj, inTarget): View Source

    574    def assign_lookat_flipless(self, inObj, inTarget):
    575        """
    576        플립 없는 LookAt 제약 컨트롤러를 스크립트 기반으로 구현하여 할당.
    577        부모가 있는 객체에만 적용 가능.
    578
    579        Args:
    580            inObj: 제약을 적용할 객체 (부모가 있어야 함)
    581            inTarget: 바라볼 타겟 객체
    582
    583        Returns:
    584            None
    585        """
    586        # 객체에 부모가 있는 경우에만 실행
    587        if inObj.parent is not None:
    588            # 회전 스크립트 컨트롤러 생성
    589            targetRotConstraint = rt.Rotation_Script()
    590
    591            # 스크립트에 필요한 노드 추가
    592            targetRotConstraint.AddNode("Target", inTarget)
    593            targetRotConstraint.AddNode("Parent", inObj.parent)
    594
    595            # 객체 위치 컨트롤러 추가
    596            pos_controller = rt.getPropertyController(inObj.controller, "Position")
    597            targetRotConstraint.AddObject("NodePos", pos_controller)
    598
    599            # 회전 계산 스크립트 설정
    600            script = textwrap.dedent(r'''
    601                theTargetVector=(Target.transform.position * Inverse Parent.transform)-NodePos.value
    602                theAxis=Normalize (cross theTargetVector [1,0,0])
    603                theAngle=acos (dot (Normalize theTargetVector) [1,0,0])
    604                Quat theAngle theAxis
    605                ''')
    606            targetRotConstraint.script = script
    607
    608            # 회전 컨트롤러가 리스트 형태가 아니면 변환
    609            rot_controller = rt.getPropertyController(inObj.controller, "Rotation")
    610            if rt.classOf(rot_controller) != rt.Rotation_list:
    611                rt.setPropertyController(inObj.controller, "Rotation", rt.Rotation_list())
    612
    613            # 회전 리스트에 스크립트 컨트롤러 추가
    614            rot_list = self.get_rot_list_controller(inObj)
    615            rt.setPropertyController(rot_list, "Available", targetRotConstraint)
    616            rot_list.setActive(rot_list.count)
    617
    618            return targetRotConstraint

플립 없는 LookAt 제약 컨트롤러를 스크립트 기반으로 구현하여 할당. 부모가 있는 객체에만 적용 가능.

Args: inObj: 제약을 적용할 객체 (부모가 있어야 함) inTarget: 바라볼 타겟 객체

Returns: None

def assign_rot_const_scripted(self, inObj, inTarget): View Source

    620    def assign_rot_const_scripted(self, inObj, inTarget):
    621        """
    622        스크립트 기반 회전 제약을 구현하여 할당.
    623        ExposeTransform을 활용한 고급 회전 제약 구현.
    624
    625        Args:
    626            inObj: 제약을 적용할 객체
    627            inTarget: 회전 참조 타겟 객체
    628
    629        Returns:
    630            생성된 회전 스크립트 컨트롤러
    631        """
    632        # 회전 스크립트 컨트롤러 생성
    633        targetRotConstraint = rt.Rotation_Script()
    634
    635        # 회전 컨트롤러 리스트에 추가
    636        rot_controller = rt.getPropertyController(inObj.controller, "Rotation")
    637        if rt.classOf(rot_controller) != rt.Rotation_list:
    638            rt.setPropertyController(inObj.controller, "Rotation", rt.Rotation_list())
    639
    640        rot_list = self.get_rot_list_controller(inObj)
    641        rt.setPropertyController(rot_list, "Available", targetRotConstraint)
    642        rot_list.setActive(rot_list.count)
    643
    644        # 헬퍼 객체 이름 생성
    645        rotPointName = self.name.replace_Type(inObj.name, self.name.get_dummy_value())
    646        rotMeasurePointName = self.name.increase_index(rotPointName, 1)
    647        rotExpName = self.name.replace_Type(inObj.name, self.name.get_exposeTm_value())
    648        rotExpName = self.name.replace_Index(rotExpName, "0")
    649
    650        print(f"dumStr: {self.name.get_dummy_value()}")
    651        print(f"exposeTmStr: {self.name.get_exposeTm_value()}")
    652        print(f"rotPointName: {rotPointName}, rotMeasurePointName: {rotMeasurePointName}, rotExpName: {rotExpName}")
    653
    654        # 헬퍼 객체 생성
    655        rotPoint = self.helper.create_point(rotPointName, size=2, boxToggle=True, crossToggle=False)
    656        rotMeasuerPoint = self.helper.create_point(rotMeasurePointName, size=3, boxToggle=True, crossToggle=False)
    657        rotExpPoint = rt.ExposeTm(name=rotExpName, size=3, box=False, cross=True, wirecolor=rt.Color(14, 255, 2))
    658
    659        # 초기 변환 설정
    660        rt.setProperty(rotPoint, "transform", rt.getProperty(inObj, "transform"))
    661        rt.setProperty(rotMeasuerPoint, "transform", rt.getProperty(inObj, "transform"))
    662        rt.setProperty(rotExpPoint, "transform", rt.getProperty(inObj, "transform"))
    663
    664        # 부모 관계 설정
    665        rotPoint.parent = inTarget
    666        rotMeasuerPoint.parent = inTarget.parent
    667        rotExpPoint.parent = inTarget
    668
    669        # ExposeTm 설정
    670        rotExpPoint.exposeNode = rotPoint
    671        rotExpPoint.useParent = False
    672        rotExpPoint.localReferenceNode = rotMeasuerPoint
    673
    674        # 회전 스크립트 생성
    675        rotScript = textwrap.dedent(r'''
    676            local targetRot = rot.localEuler
    677            local rotX = (radToDeg targetRot.x)
    678            local rotY = (radToDeg targetRot.y)
    679            local rotZ = (radToDeg targetRot.z)
    680            local result = eulerAngles rotX rotY rotZ
    681            eulerToQuat result
    682            ''')
    683
    684        # 스크립트에 노드 추가 및 표현식 설정
    685        targetRotConstraint.AddNode("rot", rotExpPoint)
    686        targetRotConstraint.SetExpression(rotScript)
    687
    688        return targetRotConstraint

스크립트 기반 회전 제약을 구현하여 할당. ExposeTransform을 활용한 고급 회전 제약 구현.

Args: inObj: 제약을 적용할 객체 inTarget: 회전 참조 타겟 객체

Returns: 생성된 회전 스크립트 컨트롤러

def assign_scripted_lookat(self, inOri, inTarget): View Source

    690    def assign_scripted_lookat(self, inOri, inTarget):
    691        """
    692        스크립트 기반 LookAt 제약을 구현하여 할당.
    693        여러 개의 헬퍼 객체를 생성하여 복잡한 LookAt 제약 구현.
    694
    695        Args:
    696            inOri: 제약을 적용할 객체
    697            inTarget: 바라볼 타겟 객체 배열
    698
    699        Returns:
    700            None
    701        """
    702        oriObj = inOri
    703        oriParentObj = inOri.parent
    704        targetObjArray = inTarget
    705
    706        # 객체 이름 생성
    707        objName = self.name.get_string(oriObj.name)
    708        indexVal = self.name.get_index_as_digit(oriObj.name)
    709        indexNum = 0 if indexVal is False else indexVal
    710        dummyName = self.name.add_prefix_to_real_name(objName, self.name.get_dummy_value())
    711
    712        lookAtPointName = self.name.replace_Index(dummyName, str(indexNum))
    713        lookAtMeasurePointName = self.name.replace_Index(dummyName, str(indexNum+1))
    714        lookAtExpPointName = dummyName + self.name.get_exposeTm_value()
    715        lookAtExpPointName = self.name.replace_Index(lookAtExpPointName, "0")
    716
    717        # 헬퍼 객체 생성
    718        lookAtPoint = self.helper.create_point(lookAtPointName, size=2, boxToggle=True, crossToggle=False)
    719        lookAtMeasurePoint = self.helper.create_point(lookAtMeasurePointName, size=3, boxToggle=True, crossToggle=False)
    720        lookAtExpPoint = rt.ExposeTm(name=lookAtExpPointName, size=3, box=False, cross=True, wirecolor=rt.Color(14, 255, 2))
    721
    722        # 초기 변환 설정
    723        rt.setProperty(lookAtPoint, "transform", rt.getProperty(oriObj, "transform"))
    724        rt.setProperty(lookAtMeasurePoint, "transform", rt.getProperty(oriObj, "transform"))
    725        rt.setProperty(lookAtExpPoint, "transform", rt.getProperty(oriObj, "transform"))
    726
    727        # 부모 관계 설정
    728        rt.setProperty(lookAtPoint, "parent", oriParentObj)
    729        rt.setProperty(lookAtMeasurePoint, "parent", oriParentObj)
    730        rt.setProperty(lookAtExpPoint, "parent", oriParentObj)
    731
    732        # ExposeTm 설정
    733        lookAtExpPoint.exposeNode = lookAtPoint
    734        lookAtExpPoint.useParent = False
    735        lookAtExpPoint.localReferenceNode = lookAtMeasurePoint
    736
    737        # LookAt 제약 설정
    738        lookAtPoint_rot_controller = rt.LookAt_Constraint()
    739        rt.setPropertyController(lookAtPoint.controller, "Rotation", lookAtPoint_rot_controller)
    740
    741        # 타겟 추가
    742        target_weight = 100.0 / len(targetObjArray)
    743        for item in targetObjArray:
    744            lookAtPoint_rot_controller.appendTarget(item, target_weight)
    745
    746        # 오일러 XYZ 컨트롤러 생성
    747        rotControl = rt.Euler_XYZ()
    748
    749        x_controller = rt.Float_Expression()
    750        y_controller = rt.Float_Expression()
    751        z_controller = rt.Float_Expression()
    752
    753        # 스칼라 타겟 추가
    754        x_controller.AddScalarTarget("rotX", rt.getPropertyController(lookAtExpPoint, "localEulerX"))
    755        y_controller.AddScalarTarget("rotY", rt.getPropertyController(lookAtExpPoint, "localEulerY"))
    756        z_controller.AddScalarTarget("rotZ", rt.getPropertyController(lookAtExpPoint, "localEulerZ"))
    757
    758        # 표현식 설정
    759        x_controller.SetExpression("rotX")
    760        y_controller.SetExpression("rotY")
    761        z_controller.SetExpression("rotZ")
    762
    763        # 각 축별 회전에 Float_Expression 컨트롤러 할당
    764        rt.setPropertyController(rotControl, "X_Rotation", x_controller)
    765        rt.setPropertyController(rotControl, "Y_Rotation", y_controller)
    766        rt.setPropertyController(rotControl, "Z_Rotation", z_controller)
    767
    768        # 회전 컨트롤러 목록 확인 또는 생성
    769        rot_controller = rt.getPropertyController(oriObj.controller, "Rotation")
    770        if rt.classOf(rot_controller) != rt.Rotation_list:
    771            rt.setPropertyController(oriObj.controller, "Rotation", rt.Rotation_list())
    772
    773        # 회전 리스트에 오일러 컨트롤러 추가
    774        rot_list = self.get_rot_list_controller(oriObj)
    775        rt.setPropertyController(rot_list, "Available", rotControl)
    776
    777        # 컨트롤러 이름 설정
    778        rot_controller_num = rot_list.count
    779        rot_list.setname(rot_controller_num, "Script Rotation")
    780
    781        # 컨트롤러 업데이트
    782        x_controller.Update()
    783        y_controller.Update()
    784        z_controller.Update()
    785
    786        return {"lookAt":lookAtPoint_rot_controller, "x":x_controller, "y":y_controller, "z":z_controller}

스크립트 기반 LookAt 제약을 구현하여 할당. 여러 개의 헬퍼 객체를 생성하여 복잡한 LookAt 제약 구현.

Args: inOri: 제약을 적용할 객체 inTarget: 바라볼 타겟 객체 배열

Returns: None

def assign_attachment( self, inPlacedObj, inSurfObj, bAlign=False, shiftAxis=(0, 0, 1), shiftAmount=3.0): View Source

    788    def assign_attachment(self, inPlacedObj, inSurfObj, bAlign=False, shiftAxis=(0, 0, 1), shiftAmount=3.0):
    789        """
    790        객체를 다른 객체의 표면에 부착하는 Attachment 제약 컨트롤러 할당.
    791
    792        Args:
    793            inPlacedObj: 부착될 객체
    794            inSurfObj: 표면 객체
    795            bAlign: 표면 법선에 맞춰 정렬할지 여부
    796            shiftAxis: 레이 방향 축 (기본값: Z축)
    797            shiftAmount: 레이 거리 (기본값: 3.0)
    798
    799        Returns:
    800            생성된 Attachment 컨트롤러 또는 None (실패 시)
    801        """
    802        # 현재 변환 행렬 백업 및 시작 위치 계산
    803        placedObjTm = rt.getProperty(inPlacedObj, "transform")
    804        rt.preTranslate(placedObjTm, rt.Point3(shiftAxis[0], shiftAxis[1], shiftAxis[2]) * (-shiftAmount))
    805        dirStartPos = placedObjTm.pos
    806
    807        # 끝 위치 계산
    808        placedObjTm = rt.getProperty(inPlacedObj, "transform")
    809        rt.preTranslate(placedObjTm, rt.Point3(shiftAxis[0], shiftAxis[1], shiftAxis[2]) * shiftAmount)
    810        dirEndPos = placedObjTm.pos
    811
    812        # 방향 벡터 및 레이 생성
    813        dirVec = dirEndPos - dirStartPos
    814        dirRay = rt.ray(dirEndPos, -dirVec)
    815
    816        # 레이 교차 검사
    817        intersectArr = rt.intersectRayEx(inSurfObj, dirRay)
    818
    819        # 교차점이 있으면 Attachment 제약 생성
    820        if intersectArr is not None:
    821            # 위치 컨트롤러 리스트 생성 또는 가져오기
    822            posListConst = self.assign_pos_list(inPlacedObj)
    823
    824            # Attachment 컨트롤러 생성
    825            attConst = rt.Attachment()
    826            rt.setPropertyController(posListConst, "Available", attConst)
    827
    828            # 제약 속성 설정
    829            attConst.node = inSurfObj
    830            attConst.align = bAlign
    831
    832            # 부착 키 추가
    833            attachKey = rt.attachCtrl.addNewKey(attConst, 0)
    834            attachKey.face = intersectArr[2] - 1  # 인덱스 조정 (MAXScript는 1부터, Python은 0부터)
    835            attachKey.coord = intersectArr[3]
    836
    837            return attConst
    838        else:
    839            return None

객체를 다른 객체의 표면에 부착하는 Attachment 제약 컨트롤러 할당.

Args: inPlacedObj: 부착될 객체 inSurfObj: 표면 객체 bAlign: 표면 법선에 맞춰 정렬할지 여부 shiftAxis: 레이 방향 축 (기본값: Z축) shiftAmount: 레이 거리 (기본값: 3.0)

Returns: 생성된 Attachment 컨트롤러 또는 None (실패 시)

def get_pos_controllers_name_from_list(self, inObj): View Source

    841    def get_pos_controllers_name_from_list(self, inObj):
    842        """
    843        객체의 위치 컨트롤러 리스트에서 각 컨트롤러의 이름을 가져옴.
    844
    845        Args:
    846            inObj: 대상 객체
    847
    848        Returns:
    849            컨트롤러 이름 배열
    850        """
    851        returnNameArray = []
    852
    853        # 위치 컨트롤러가 리스트 형태인지 확인
    854        pos_controller = rt.getPropertyController(inObj.controller, "Position")
    855        if rt.classOf(pos_controller) == rt.Position_list:
    856            posList = pos_controller
    857
    858            # 각 컨트롤러의 이름을 배열에 추가
    859            for i in range(1, posList.count + 1):  # MAXScript는 1부터 시작
    860                returnNameArray.append(posList.getName(i))
    861
    862        return returnNameArray

객체의 위치 컨트롤러 리스트에서 각 컨트롤러의 이름을 가져옴.

Args: inObj: 대상 객체

Returns: 컨트롤러 이름 배열

def get_pos_controllers_weight_from_list(self, inObj): View Source

    864    def get_pos_controllers_weight_from_list(self, inObj):
    865        """
    866        객체의 위치 컨트롤러 리스트에서 각 컨트롤러의 가중치를 가져옴.
    867
    868        Args:
    869            inObj: 대상 객체
    870
    871        Returns:
    872            컨트롤러 가중치 배열
    873        """
    874        returnWeightArray = []
    875
    876        # 위치 컨트롤러가 리스트 형태인지 확인
    877        pos_controller = rt.getPropertyController(inObj.controller, "Position")
    878        if rt.classOf(pos_controller) == rt.Position_list:
    879            posList = pos_controller
    880
    881            # 가중치 배열 가져오기
    882            returnWeightArray = list(posList.weight)
    883
    884        return returnWeightArray

객체의 위치 컨트롤러 리스트에서 각 컨트롤러의 가중치를 가져옴.

Args: inObj: 대상 객체

Returns: 컨트롤러 가중치 배열

def set_pos_controllers_name_in_list(self, inObj, inLayerNum, inNewName): View Source

    886    def set_pos_controllers_name_in_list(self, inObj, inLayerNum, inNewName):
    887        """
    888        객체의 위치 컨트롤러 리스트에서 특정 컨트롤러의 이름을 설정.
    889
    890        Args:
    891            inObj: 대상 객체
    892            inLayerNum: 컨트롤러 인덱스 (1부터 시작)
    893            inNewName: 새 이름
    894
    895        Returns:
    896            None
    897        """
    898        # 위치 컨트롤러 리스트 가져오기
    899        listCtr = self.get_pos_list_controller(inObj)
    900
    901        # 리스트가 있으면 이름 설정
    902        if listCtr is not None:
    903            listCtr.setName(inLayerNum, inNewName)

객체의 위치 컨트롤러 리스트에서 특정 컨트롤러의 이름을 설정.

Args: inObj: 대상 객체 inLayerNum: 컨트롤러 인덱스 (1부터 시작) inNewName: 새 이름

Returns: None

def set_pos_controllers_weight_in_list(self, inObj, inLayerNum, inNewWeight): View Source

    905    def set_pos_controllers_weight_in_list(self, inObj, inLayerNum, inNewWeight):
    906        """
    907        객체의 위치 컨트롤러 리스트에서 특정 컨트롤러의 가중치를 설정.
    908
    909        Args:
    910            inObj: 대상 객체
    911            inLayerNum: 컨트롤러 인덱스 (1부터 시작)
    912            inNewWeight: 새 가중치
    913
    914        Returns:
    915            None
    916        """
    917        # 위치 컨트롤러 리스트 가져오기
    918        listCtr = self.get_pos_list_controller(inObj)
    919
    920        # 리스트가 있으면 가중치 설정
    921        if listCtr is not None:
    922            listCtr.weight[inLayerNum] = inNewWeight

객체의 위치 컨트롤러 리스트에서 특정 컨트롤러의 가중치를 설정.

Args: inObj: 대상 객체 inLayerNum: 컨트롤러 인덱스 (1부터 시작) inNewWeight: 새 가중치

Returns: None

def get_rot_controllers_name_from_list(self, inObj): View Source

    924    def get_rot_controllers_name_from_list(self, inObj):
    925        """
    926        객체의 회전 컨트롤러 리스트에서 각 컨트롤러의 이름을 가져옴.
    927
    928        Args:
    929            inObj: 대상 객체
    930
    931        Returns:
    932            컨트롤러 이름 배열
    933        """
    934        returnNameArray = []
    935
    936        # 회전 컨트롤러가 리스트 형태인지 확인
    937        rot_controller = rt.getPropertyController(inObj.controller, "Rotation")
    938        if rt.classOf(rot_controller) == rt.Rotation_list:
    939            rotList = rot_controller
    940
    941            # 각 컨트롤러의 이름을 배열에 추가
    942            for i in range(1, rotList.count + 1):  # MAXScript는 1부터 시작
    943                returnNameArray.append(rotList.getName(i))
    944
    945        return returnNameArray

객체의 회전 컨트롤러 리스트에서 각 컨트롤러의 이름을 가져옴.

Args: inObj: 대상 객체

Returns: 컨트롤러 이름 배열

def get_rot_controllers_weight_from_list(self, inObj): View Source

    947    def get_rot_controllers_weight_from_list(self, inObj):
    948        """
    949        객체의 회전 컨트롤러 리스트에서 각 컨트롤러의 가중치를 가져옴.
    950
    951        Args:
    952            inObj: 대상 객체
    953
    954        Returns:
    955            컨트롤러 가중치 배열
    956        """
    957        returnWeightArray = []
    958
    959        # 회전 컨트롤러가 리스트 형태인지 확인
    960        rot_controller = rt.getPropertyController(inObj.controller, "Rotation")
    961        if rt.classOf(rot_controller) == rt.Rotation_list:
    962            rotList = rot_controller
    963
    964            # 가중치 배열 가져오기
    965            returnWeightArray = list(rotList.weight)
    966
    967        return returnWeightArray

객체의 회전 컨트롤러 리스트에서 각 컨트롤러의 가중치를 가져옴.

Args: inObj: 대상 객체

Returns: 컨트롤러 가중치 배열

def set_rot_controllers_name_in_list(self, inObj, inLayerNum, inNewName): View Source

    969    def set_rot_controllers_name_in_list(self, inObj, inLayerNum, inNewName):
    970        """
    971        객체의 회전 컨트롤러 리스트에서 특정 컨트롤러의 이름을 설정.
    972
    973        Args:
    974            inObj: 대상 객체
    975            inLayerNum: 컨트롤러 인덱스 (1부터 시작)
    976            inNewName: 새 이름
    977
    978        Returns:
    979            None
    980        """
    981        # 회전 컨트롤러 리스트 가져오기
    982        listCtr = self.get_rot_list_controller(inObj)
    983
    984        # 리스트가 있으면 이름 설정
    985        if listCtr is not None:
    986            listCtr.setName(inLayerNum, inNewName)

객체의 회전 컨트롤러 리스트에서 특정 컨트롤러의 이름을 설정.

Args: inObj: 대상 객체 inLayerNum: 컨트롤러 인덱스 (1부터 시작) inNewName: 새 이름

Returns: None

def set_rot_controllers_weight_in_list(self, inObj, inLayerNum, inNewWeight): View Source

     988    def set_rot_controllers_weight_in_list(self, inObj, inLayerNum, inNewWeight):
     989        """
     990        객체의 회전 컨트롤러 리스트에서 특정 컨트롤러의 가중치를 설정.
     991
     992        Args:
     993            inObj: 대상 객체
     994            inLayerNum: 컨트롤러 인덱스 (1부터 시작)
     995            inNewWeight: 새 가중치
     996
     997        Returns:
     998            None
     999        """
    1000        # 회전 컨트롤러 리스트 가져오기
    1001        listCtr = self.get_rot_list_controller(inObj)
    1002
    1003        # 리스트가 있으면 가중치 설정
    1004        if listCtr is not None:
    1005            listCtr.weight[inLayerNum] = inNewWeight

객체의 회전 컨트롤러 리스트에서 특정 컨트롤러의 가중치를 설정.

Args: inObj: 대상 객체 inLayerNum: 컨트롤러 인덱스 (1부터 시작) inNewWeight: 새 가중치

Returns: None

class Bone: View Source

      19class Bone:
      20    """
      21    뼈대(Bone) 관련 기능을 제공하는 클래스.
      22    MAXScript의 _Bone 구조체 개념을 Python으로 재구현한 클래스이며,
      23    3ds Max의 기능들을 pymxs API를 통해 제어합니다.
      24    """
      25
      26    def __init__(self, nameService=None, animService=None, helperService=None, constraintService=None):
      27        """
      28        클래스 초기화.
      29
      30        Args:
      31            nameService: 이름 처리 서비스 (제공되지 않으면 새로 생성)
      32            animService: 애니메이션 서비스 (제공되지 않으면 새로 생성)
      33            helperService: 헬퍼 객체 서비스 (제공되지 않으면 새로 생성)
      34            constraintService: 제약 서비스 (제공되지 않으면 새로 생성)
      35        """
      36        self.name = nameService if nameService else Name()
      37        self.anim = animService if animService else Anim()
      38        self.helper = helperService if helperService else Helper(nameService=self.name)
      39        self.const = constraintService if constraintService else Constraint(nameService=self.name, helperService=self.helper)
      40
      41    def remove_ik(self, inBone):
      42        """
      43        뼈대에서 IK 체인을 제거.
      44
      45        Args:
      46            inBone: IK 체인을 제거할 뼈대 객체
      47        """
      48        # pos 또는 rotation 속성이 없는 경우에만 IK 체인 제거
      49        if (not rt.isProperty(inBone, "pos")) or (not rt.isProperty(inBone, "rotation")):
      50            rt.HDIKSys.RemoveChain(inBone)
      51
      52    def get_bone_assemblyHead(self, inBone):
      53        """
      54        뼈대 어셈블리의 헤드를 가져옴.
      55
      56        Args:
      57            inBone: 대상 뼈대 객체
      58
      59        Returns:
      60            어셈블리 헤드 또는 None
      61        """
      62        tempBone = inBone
      63        while tempBone is not None:
      64            if tempBone.assemblyHead:
      65                return tempBone
      66            if not tempBone.assemblyMember:
      67                break
      68            tempBone = tempBone.parent
      69
      70        return None
      71
      72    def put_child_into_bone_assembly(self, inBone):
      73        """
      74        자식 뼈대를 어셈블리에 추가.
      75
      76        Args:
      77            inBone: 어셈블리에 추가할 자식 뼈대
      78        """
      79        if inBone.parent is not None and inBone.parent.assemblyMember:
      80            inBone.assemblyMember = True
      81            inBone.assemblyMemberOpen = True
      82
      83    def sort_bones_as_hierarchy(self, inBoneArray):
      84        """
      85        뼈대 배열을 계층 구조에 따라 정렬.
      86
      87        Args:
      88            inBoneArray: 정렬할 뼈대 객체 배열
      89
      90        Returns:
      91            계층 구조에 따라 정렬된 뼈대 배열
      92        """
      93        # BoneLevel 구조체 정의 (Python 클래스로 구현)
      94        @dataclass
      95        class BoneLevel:
      96            index: int
      97            level: int
      98
      99        # 뼈대 구조체 배열 초기화
     100        bones = []
     101
     102        # 뼈대 구조체 배열 채우기. 계층 수준을 0으로 초기화
     103        for i in range(len(inBoneArray)):
     104            bones.append(BoneLevel(i, 0))
     105
     106        # 뼈대 배열의 각 뼈대에 대한 계층 수준 계산
     107        # 계층 수준은 현재 뼈대와 루트 노드 사이의 조상 수
     108        for i in range(len(bones)):
     109            node = inBoneArray[bones[i].index]
     110            n = 0
     111            while node is not None:
     112                n += 1
     113                node = node.parent
     114            bones[i].level = n
     115
     116        # 계층 수준에 따라 뼈대 배열 정렬
     117        bones.sort(key=lambda x: x.level)
     118
     119        # 정렬된 뼈대를 저장할 새 배열 준비
     120        returnBonesArray = []
     121        for i in range(len(inBoneArray)):
     122            returnBonesArray.append(inBoneArray[bones[i].index])
     123
     124        return returnBonesArray
     125
     126    def correct_negative_stretch(self, bone, ask=True):
     127        """
     128        뼈대의 음수 스케일 보정.
     129
     130        Args:
     131            bone: 보정할 뼈대 객체
     132            ask: 사용자에게 확인 요청 여부 (기본값: True)
     133
     134        Returns:
     135            None
     136        """
     137        axisIndex = 0
     138
     139        # 뼈대 축에 따라 인덱스 설정
     140        if bone.boneAxis == rt.Name("X"):
     141            axisIndex = 0
     142        elif bone.boneAxis == rt.Name("Y"):
     143            axisIndex = 1
     144        elif bone.boneAxis == rt.Name("Z"):
     145            axisIndex = 2
     146
     147        ooscale = bone.objectOffsetScale
     148
     149        # 음수 스케일 보정
     150        if (ooscale[axisIndex] < 0) and ((not ask) or rt.queryBox("Correct negative scale?", title=bone.Name)):
     151            ooscale[axisIndex] = -ooscale[axisIndex]
     152            axisIndex = axisIndex + 2
     153            if axisIndex > 2:
     154                axisIndex = axisIndex - 3
     155            ooscale[axisIndex] = -ooscale[axisIndex]
     156            bone.objectOffsetScale = ooscale
     157
     158    def reset_scale_of_selected_bones(self, ask=True):
     159        """
     160        선택된 뼈대들의 스케일 초기화.
     161
     162        Args:
     163            ask: 음수 스케일 보정 확인 요청 여부 (기본값: True)
     164
     165        Returns:
     166            None
     167        """
     168        # 선택된 객체 중 BoneGeometry 타입만 수집
     169        bones = [item for item in rt.selection if rt.classOf(item) == rt.BoneGeometry]
     170
     171        # 계층 구조에 따라 뼈대 정렬
     172        bones = self.sort_bones_as_hierarchy(rt.selection)
     173
     174        # 뼈대 배열의 모든 뼈대에 대해 스케일 초기화
     175        for i in range(len(bones)):
     176            rt.ResetScale(bones[i])
     177            if ask:
     178                self.correct_negative_stretch(bones[i], False)
     179
     180    def is_nub_bone(self, inputBone):
     181        """
     182        뼈대가 Nub 뼈대인지 확인 (부모 및 자식이 없는 단일 뼈대).
     183
     184        Args:
     185            inputBone: 확인할 뼈대 객체
     186
     187        Returns:
     188            True: Nub 뼈대인 경우
     189            False: 그 외의 경우
     190        """
     191        if rt.classOf(inputBone) == rt.BoneGeometry:
     192            if inputBone.parent is None and inputBone.children.count == 0:
     193                return True
     194            else:
     195                return False
     196        return False
     197
     198    def is_end_bone(self, inputBone):
     199        """
     200        뼈대가 End 뼈대인지 확인 (부모는 있지만 자식이 없는 뼈대).
     201
     202        Args:
     203            inputBone: 확인할 뼈대 객체
     204
     205        Returns:
     206            True: End 뼈대인 경우
     207            False: 그 외의 경우
     208        """
     209        if rt.classOf(inputBone) == rt.BoneGeometry:
     210            if inputBone.parent is not None and inputBone.children.count == 0:
     211                return True
     212            else:
     213                return False
     214        return False
     215
     216    def create_nub_bone(self, inName, inSize):
     217        """
     218        Nub 뼈대 생성.
     219
     220        Args:
     221            inName: 뼈대 이름
     222            inSize: 뼈대 크기
     223
     224        Returns:
     225            생성된 Nub 뼈대
     226        """
     227        nubBone = None
     228
     229        # 화면 갱신 중지 상태에서 뼈대 생성
     230        rt.disableSceneRedraw()
     231
     232        # 뼈대 생성 및 속성 설정
     233        nubBone = rt.BoneSys.createBone(rt.Point3(0, 0, 0), rt.Point3(1, 0, 0), rt.Point3(0, 0, 1))
     234
     235        nubBone.width = inSize
     236        nubBone.height = inSize
     237        nubBone.taper = 90
     238        nubBone.length = inSize
     239        nubBone.frontfin = False
     240        nubBone.backfin = False
     241        nubBone.sidefins = False
     242        nubBone.name = self.name.remove_name_part("Index", inName)
     243        nubBone.name = self.name.replace_name_part("Nub", nubBone.name, self.name.get_name_part_value_by_description("Nub", "Nub"))
     244
     245        # 화면 갱신 재개
     246        rt.enableSceneRedraw()
     247        rt.redrawViews()
     248
     249        return nubBone
     250
     251    def create_nub_bone_on_obj(self, inObj, inSize=1):
     252        """
     253        객체 위치에 Nub 뼈대 생성.
     254
     255        Args:
     256            inObj: 위치를 참조할 객체
     257            inSize: 뼈대 크기 (기본값: 1)
     258
     259        Returns:
     260            생성된 Nub 뼈대
     261        """
     262        boneName = self.name.get_string(inObj.name)
     263        newBone = self.create_nub_bone(boneName, inSize)
     264        newBone.transform = inObj.transform
     265
     266        return newBone
     267
     268    def create_end_bone(self, inBone):
     269        """
     270        뼈대의 끝에 End 뼈대 생성.
     271
     272        Args:
     273            inBone: 부모가 될 뼈대 객체
     274
     275        Returns:
     276            생성된 End 뼈대
     277        """
     278        parentBone = inBone
     279        parentTrans = parentBone.transform
     280        parentPos = parentTrans.translation
     281        boneName = self.name.get_string(parentBone.name)
     282        newBone = self.create_nub_bone(boneName, parentBone.width)
     283
     284        newBone.transform = parentTrans
     285
     286        # 로컬 좌표계에서 이동
     287        self.anim.move_local(newBone, parentBone.length, 0, 0)
     288
     289        newBone.parent = parentBone
     290        self.put_child_into_bone_assembly(newBone)
     291
     292        # 뼈대 속성 설정
     293        newBone.width = parentBone.width
     294        newBone.height = parentBone.height
     295        newBone.frontfin = False
     296        newBone.backfin = False
     297        newBone.sidefins = False
     298        newBone.taper = 90
     299        newBone.length = (parentBone.width + parentBone.height) / 2
     300        newBone.wirecolor = parentBone.wirecolor
     301
     302        return newBone
     303
     304    def create_bone(self, inPointArray, inName, end=True, delPoint=False, parent=False, size=2, normals=None):
     305        """
     306        포인트 배열을 따라 뼈대 체인 생성.
     307
     308        Args:
     309            inPointArray: 뼈대 위치를 정의하는 포인트 배열
     310            inName: 뼈대 기본 이름
     311            end: End 뼈대 생성 여부 (기본값: True)
     312            delPoint: 포인트 삭제 여부 (기본값: False)
     313            parent: 부모 Nub 포인트 생성 여부 (기본값: False)
     314            size: 뼈대 크기 (기본값: 2)
     315            normals: 법선 벡터 배열 (기본값: None)
     316
     317        Returns:
     318            생성된 뼈대 배열 또는 False (실패 시)
     319        """
     320        if normals is None:
     321            normals = []
     322
     323        tempBone = None
     324        newBone = None
     325
     326        returnBoneArray = []
     327
     328        if len(inPointArray) != 1:
     329            for i in range(len(inPointArray) - 1):
     330                boneNum = i
     331
     332                if len(normals) == len(inPointArray):
     333                    xDir = rt.normalize(inPointArray[i+1].transform.position - inPointArray[i].transform.position)
     334                    zDir = rt.normalize(rt.cross(xDir, normals[i]))
     335                    newBone = rt.BoneSys.createBone(inPointArray[i].transform.position, inPointArray[i+1].transform.position, zDir)
     336                else:
     337                    newBone = rt.BoneSys.createBone(inPointArray[i].transform.position, inPointArray[i+1].transform.position, rt.Point3(0, -1, 0))
     338
     339                newBone.boneFreezeLength = True
     340                newBone.name = self.name.replace_name_part("Index", inName, str(boneNum))
     341                newBone.height = size
     342                newBone.width = size
     343                newBone.frontfin = False
     344                newBone.backfin = False
     345                newBone.sidefins = False
     346
     347                returnBoneArray.append(newBone)
     348
     349                if tempBone is not None:
     350                    tempTm = rt.copy(newBone.transform * rt.Inverse(tempBone.transform))
     351                    localRot = rt.quatToEuler(tempTm.rotation).x
     352
     353                    self.anim.rotate_local(newBone, -localRot, 0, 0)
     354
     355                newBone.parent = tempBone
     356                tempBone = newBone
     357
     358            if delPoint:
     359                for i in range(len(inPointArray)):
     360                    if (rt.classOf(inPointArray[i]) == rt.Dummy) or (rt.classOf(inPointArray[i]) == rt.ExposeTm) or (rt.classOf(inPointArray[i]) == rt.Point):
     361                        rt.delete(inPointArray[i])
     362
     363            if parent:
     364                parentNubPointName = self.name.replace_type(inName, self.name.get_parent_str())
     365                parentNubPoint = self.helper.create_point(parentNubPointName, size=size, boxToggle=True, crossToggle=True)
     366                parentNubPoint.transform = returnBoneArray[0].transform
     367                returnBoneArray[0].parent = parentNubPoint
     368
     369            rt.select(newBone)
     370
     371            if end:
     372                endBone = self.create_end_bone(newBone)
     373                returnBoneArray.append(endBone)
     374
     375                rt.clearSelection()
     376
     377                return returnBoneArray
     378            else:
     379                return returnBoneArray
     380        else:
     381            return False
     382
     383    def create_simple_bone(self, inLength, inName, end=True, size=1):
     384        """
     385        간단한 뼈대 생성 (시작점과 끝점 지정).
     386
     387        Args:
     388            inLength: 뼈대 길이
     389            inName: 뼈대 이름
     390            end: End 뼈대 생성 여부 (기본값: True)
     391            size: 뼈대 크기 (기본값: 1)
     392
     393        Returns:
     394            생성된 뼈대 배열
     395        """
     396        startPoint = self.helper.create_point("tempStart")
     397        endPoint = self.helper.create_point("tempEnd", pos=(inLength, 0, 0))
     398        returnBoneArray = self.create_bone([startPoint, endPoint], inName, end=end, delPoint=True, size=size)
     399
     400        return returnBoneArray
     401
     402    def create_stretch_bone(self, inPointArray, inName, size=2):
     403        """
     404        스트레치 뼈대 생성 (포인트를 따라 움직이는 뼈대).
     405
     406        Args:
     407            inPointArray: 뼈대 위치를 정의하는 포인트 배열
     408            inName: 뼈대 기본 이름
     409            size: 뼈대 크기 (기본값: 2)
     410
     411        Returns:
     412            생성된 스트레치 뼈대 배열
     413        """
     414        tempBone = []
     415        tempBone = self.create_bone(inPointArray, inName, size=size)
     416
     417        for i in range(len(tempBone) - 1):
     418            self.const.assign_pos_const(tempBone[i], inPointArray[i])
     419            self.const.assign_lookat(tempBone[i], inPointArray[i+1])
     420        self.const.assign_pos_const(tempBone[-1], inPointArray[-1])
     421
     422        return tempBone
     423
     424    def create_simple_stretch_bone(self, inStart, inEnd, inName, squash=False, size=1):
     425        """
     426        간단한 스트레치 뼈대 생성 (시작점과 끝점 지정).
     427
     428        Args:
     429            inStart: 시작 포인트
     430            inEnd: 끝 포인트
     431            inName: 뼈대 이름
     432            squash: 스쿼시 효과 적용 여부 (기본값: False)
     433            size: 뼈대 크기 (기본값: 1)
     434
     435        Returns:
     436            생성된 스트레치 뼈대 배열
     437        """
     438        returnArray = []
     439        returnArray = self.create_stretch_bone([inStart, inEnd], inName, size=size)
     440        if squash:
     441            returnArray[0].boneScaleType = rt.Name("squash")
     442
     443        return returnArray
     444
     445    def get_bone_shape(self, inBone):
     446        """
     447        뼈대의 형태 속성 가져오기.
     448
     449        Args:
     450            inBone: 속성을 가져올 뼈대 객체
     451
     452        Returns:
     453            뼈대 형태 속성 배열
     454        """
     455        returnArray = []
     456        if rt.classOf(inBone) == rt.BoneGeometry:
     457            returnArray = [None] * 16  # 빈 배열 초기화
     458            returnArray[0] = inBone.width
     459            returnArray[1] = inBone.height
     460            returnArray[2] = inBone.taper
     461            returnArray[3] = inBone.length
     462            returnArray[4] = inBone.sidefins
     463            returnArray[5] = inBone.sidefinssize
     464            returnArray[6] = inBone.sidefinsstarttaper
     465            returnArray[7] = inBone.sidefinsendtaper
     466            returnArray[8] = inBone.frontfin
     467            returnArray[9] = inBone.frontfinsize
     468            returnArray[10] = inBone.frontfinstarttaper
     469            returnArray[11] = inBone.frontfinendtaper
     470            returnArray[12] = inBone.backfin
     471            returnArray[13] = inBone.backfinsize
     472            returnArray[14] = inBone.backfinstarttaper
     473            returnArray[15] = inBone.backfinendtaper
     474
     475        return returnArray
     476
     477    def pasete_bone_shape(self, targetBone, shapeArray):
     478        """
     479        뼈대에 형태 속성 적용.
     480
     481        Args:
     482            targetBone: 속성을 적용할 뼈대 객체
     483            shapeArray: 적용할 뼈대 형태 속성 배열
     484
     485        Returns:
     486            True: 성공
     487            False: 실패
     488        """
     489        if rt.classOf(targetBone) == rt.BoneGeometry:
     490            targetBone.width = shapeArray[0]
     491            targetBone.height = shapeArray[1]
     492            targetBone.taper = shapeArray[2]
     493            #targetBone.length = shapeArray[3]  # 길이는 변경하지 않음
     494            targetBone.sidefins = shapeArray[4]
     495            targetBone.sidefinssize = shapeArray[5]
     496            targetBone.sidefinsstarttaper = shapeArray[6]
     497            targetBone.sidefinsendtaper = shapeArray[7]
     498            targetBone.frontfin = shapeArray[8]
     499            targetBone.frontfinsize = shapeArray[9]
     500            targetBone.frontfinstarttaper = shapeArray[10]
     501            targetBone.frontfinendtaper = shapeArray[11]
     502            targetBone.backfin = shapeArray[12]
     503            targetBone.backfinsize = shapeArray[13]
     504            targetBone.backfinstarttaper = shapeArray[14]
     505            targetBone.backfinendtaper = shapeArray[15]
     506
     507            if self.is_end_bone(targetBone):
     508                targetBone.taper = 90
     509                targetBone.length = (targetBone.width + targetBone.height) / 2
     510                targetBone.frontfin = False
     511                targetBone.backfin = False
     512                targetBone.sidefins = False
     513
     514            return True
     515        return False
     516
     517    def set_fin_on(self, inBone, side=True, front=True, back=False, inSize=2.0, inTaper=0.0):
     518        """
     519        뼈대의 핀(fin) 설정 활성화.
     520
     521        Args:
     522            inBone: 핀을 설정할 뼈대 객체
     523            side: 측면 핀 활성화 여부 (기본값: True)
     524            front: 전면 핀 활성화 여부 (기본값: True)
     525            back: 후면 핀 활성화 여부 (기본값: False)
     526            inSize: 핀 크기 (기본값: 2.0)
     527            inTaper: 핀 테이퍼 (기본값: 0.0)
     528        """
     529        if rt.classOf(inBone) == rt.BoneGeometry:
     530            if not self.is_end_bone(inBone):
     531                inBone.frontfin = front
     532                inBone.frontfinsize = inSize
     533                inBone.frontfinstarttaper = inTaper
     534                inBone.frontfinendtaper = inTaper
     535
     536                inBone.sidefins = side
     537                inBone.sidefinssize = inSize
     538                inBone.sidefinsstarttaper = inTaper
     539                inBone.sidefinsendtaper = inTaper
     540
     541                inBone.backfin = back
     542                inBone.backfinsize = inSize
     543                inBone.backfinstarttaper = inTaper
     544                inBone.backfinendtaper = inTaper
     545
     546    def set_fin_off(self, inBone):
     547        """
     548        뼈대의 모든 핀(fin) 비활성화.
     549
     550        Args:
     551            inBone: 핀을 비활성화할 뼈대 객체
     552        """
     553        if rt.classOf(inBone) == rt.BoneGeometry:
     554            inBone.frontfin = False
     555            inBone.sidefins = False
     556            inBone.backfin = False
     557
     558    def set_bone_size(self, inBone, inSize):
     559        """
     560        뼈대 크기 설정.
     561
     562        Args:
     563            inBone: 크기를 설정할 뼈대 객체
     564            inSize: 설정할 크기
     565        """
     566        if rt.classOf(inBone) == rt.BoneGeometry:
     567            inBone.width = inSize
     568            inBone.height = inSize
     569
     570            if self.is_end_bone(inBone) or self.is_nub_bone(inBone):
     571                inBone.taper = 90
     572                inBone.length = inSize
     573
     574    def set_bone_taper(self, inBone, inTaper):
     575        """
     576        뼈대 테이퍼 설정.
     577
     578        Args:
     579            inBone: 테이퍼를 설정할 뼈대 객체
     580            inTaper: 설정할 테이퍼 값
     581        """
     582        if rt.classOf(inBone) == rt.BoneGeometry:
     583            if not self.is_end_bone(inBone):
     584                inBone.taper = inTaper
     585
     586    def delete_bones_safely(self, inBoneArray):
     587        """
     588        뼈대 배열을 안전하게 삭제.
     589
     590        Args:
     591            inBoneArray: 삭제할 뼈대 배열
     592        """
     593        if len(inBoneArray) > 0:
     594            for targetBone in inBoneArray:
     595                self.const.collapse(targetBone)
     596                targetBone.parent = None
     597                rt.delete(targetBone)
     598
     599            inBoneArray.clear()
     600
     601    def select_first_children(self, inObj):
     602        """
     603        객체의 첫 번째 자식들을 재귀적으로 선택.
     604
     605        Args:
     606            inObj: 시작 객체
     607
     608        Returns:
     609            True: 자식이 있는 경우
     610            False: 자식이 없는 경우
     611        """
     612        rt.selectmore(inObj)
     613
     614        for i in range(inObj.children.count):
     615            if self.select_first_children(inObj.children[i]):
     616                if inObj.children.count == 0 or inObj.children[0] is None:
     617                    return True
     618            else:
     619                return False
     620
     621    def get_every_children(self, inObj):
     622        """
     623        객체의 모든 자식들을 가져옴.
     624
     625        Args:
     626            inObj: 시작 객체
     627
     628        Returns:
     629            자식 객체 배열
     630        """
     631        children = []
     632
     633        if inObj.children.count != 0 and inObj.children[0] is not None:
     634            for i in range(inObj.children.count):
     635                children.append(inObj.children[i])
     636                children.extend(self.get_every_children(inObj.children[i]))
     637
     638        return children
     639
     640    def select_every_children(self, inObj, includeSelf=False):
     641        """
     642        객체의 모든 자식들을 선택.
     643
     644        Args:
     645            inObj: 시작 객체
     646            includeSelf: 자신도 포함할지 여부 (기본값: False)
     647
     648        Returns:
     649            선택된 자식 객체 배열
     650        """
     651        children = self.get_every_children(inObj)
     652
     653        # 자신도 포함하는 경우
     654        if includeSelf:
     655            children.insert(0, inObj)
     656
     657        rt.select(children)
     658
     659    def get_bone_end_position(self, inBone):
     660        """
     661        뼈대 끝 위치 가져오기.
     662
     663        Args:
     664            inBone: 대상 뼈대 객체
     665
     666        Returns:
     667            뼈대 끝 위치 좌표
     668        """
     669        if rt.classOf(inBone) == rt.BoneGeometry:
     670            return rt.Point3(inBone.length, 0, 0) * inBone.objectTransform
     671        else:
     672            return inBone.transform.translation
     673
     674    def link_skin_bone(self, inSkinBone, inOriBone):
     675        """
     676        스킨 뼈대를 원본 뼈대에 연결.
     677
     678        Args:
     679            inSkinBone: 연결할 스킨 뼈대
     680            inOriBone: 원본 뼈대
     681        """
     682        self.anim.save_xform(inSkinBone)
     683        self.anim.set_xform(inSkinBone, space="World")
     684
     685        self.anim.save_xform(inOriBone)
     686
     687        rt.setPropertyController(inSkinBone.controller, "Scale", rt.scaleXYZ())
     688
     689        linkConst = rt.link_constraint()
     690        linkConst.addTarget(inOriBone, 0)
     691
     692        inSkinBone.controller = linkConst
     693
     694        self.anim.set_xform(inSkinBone, space="World")
     695
     696    def link_skin_bones(self, inSkinBoneArray, inOriBoneArray):
     697        """
     698        스킨 뼈대 배열을 원본 뼈대 배열에 연결.
     699
     700        Args:
     701            inSkinBoneArray: 연결할 스킨 뼈대 배열
     702            inOriBoneArray: 원본 뼈대 배열
     703
     704        Returns:
     705            True: 성공
     706            False: 실패
     707        """
     708        if len(inSkinBoneArray) != len(inOriBoneArray):
     709            print("Error: Skin bone array and original bone array must have the same length.")
     710            return False
     711
     712        skinBoneDict = {}
     713        oriBoneDict = {}
     714
     715        # 스킨 뼈대 딕셔너리 생성 (이름과 패턴화된 이름을 함께 저장)
     716        for item in inSkinBoneArray:
     717            # 아이템 저장
     718            skinBoneDict[item.name] = item
     719            # 언더스코어를 별표로 변환한 패턴 생성
     720            namePattern = self.name.remove_name_part("Base", item.name)
     721            namePattern = namePattern.replace("_", "*")
     722            skinBoneDict[item.name + "_Pattern"] = namePattern
     723
     724        # 원본 뼈대 딕셔너리 생성 (이름과 패턴화된 이름을 함께 저장)
     725        for item in inOriBoneArray:
     726            # 아이템 저장
     727            oriBoneDict[item.name] = item
     728            # 공백을 별표로 변환한 패턴 생성
     729            namePattern = self.name.remove_name_part("Base", item.name)
     730            namePattern = namePattern.replace(" ", "*")
     731            oriBoneDict[item.name + "_Pattern"] = namePattern
     732
     733        # 정렬된 배열 생성
     734        sortedSkinBoneArray = []
     735        sortedOriBoneArray = []
     736
     737        # 같은 패턴을 가진 뼈대들을 찾아 매칭
     738        for skinName, skinBone in [(k, v) for k, v in skinBoneDict.items() if not k.endswith("_Pattern")]:
     739            skinPattern = skinBoneDict[skinName + "_Pattern"]
     740
     741            for oriName, oriBone in [(k, v) for k, v in oriBoneDict.items() if not k.endswith("_Pattern")]:
     742                oriPattern = oriBoneDict[oriName + "_Pattern"]
     743
     744                if rt.matchPattern(skinName, pattern=oriPattern):
     745                    sortedSkinBoneArray.append(skinBone)
     746                    sortedOriBoneArray.append(oriBone)
     747                    break
     748        # 링크 연결 수행
     749        for i in range(len(sortedSkinBoneArray)):
     750            self.link_skin_bone(sortedSkinBoneArray[i], sortedOriBoneArray[i])
     751
     752        return True
     753
     754    def create_skin_bone(self, inBoneArray, skipNub=True, mesh=True, link=True, skinBoneBaseName=""):
     755        """
     756        스킨 뼈대 생성.
     757
     758        Args:
     759            inBoneArray: 원본 뼈대 배열
     760            skipNub: Nub 뼈대 건너뛰기 (기본값: True)
     761            mesh: 메시 스냅샷 사용 (기본값: True)
     762            link: 원본 뼈대에 연결 (기본값: True)
     763            skinBoneBaseName: 스킨 뼈대 기본 이름 (기본값: "b")
     764
     765        Returns:
     766            생성된 스킨 뼈대 배열
     767        """
     768        bones = []
     769        skinBoneFilteringChar = "_"
     770        skinBonePushAmount = -0.02
     771        returnBones = []
     772
     773        definedSkinBoneBaseName = self.name.get_name_part_value_by_description("Base", "SkinBone")
     774
     775        for i in range(len(inBoneArray)):
     776            skinBoneName = self.name.replace_name_part("Base", inBoneArray[i].name, definedSkinBoneBaseName)
     777            skinBoneName = self.name.replace_filtering_char(skinBoneName, skinBoneFilteringChar)
     778
     779            skinBone = self.create_nub_bone(f"{definedSkinBoneBaseName}_TempSkin", 2)
     780            skinBone.name = skinBoneName
     781            skinBone.wireColor = rt.Color(255, 88, 199)
     782            skinBone.transform = inBoneArray[i].transform
     783
     784            if mesh:
     785                snapShotObj = rt.snapshot(inBoneArray[i])
     786                rt.addModifier(snapShotObj, rt.Push())
     787                snapShotObj.modifiers[rt.Name("Push")].Push_Value = skinBonePushAmount
     788                rt.collapseStack(snapShotObj)
     789
     790                rt.addModifier(skinBone, rt.Edit_Poly())
     791                rt.execute("max modify mode")
     792                rt.modPanel.setCurrentObject(skinBone.modifiers[rt.Name("Edit_Poly")])
     793                skinBone.modifiers[rt.Name("Edit_Poly")].Attach(snapShotObj, editPolyNode=skinBone)
     794
     795            skinBone.boneEnable = True
     796            skinBone.renderable = False
     797            skinBone.boneScaleType = rt.Name("None")
     798
     799            bones.append(skinBone)
     800
     801        for i in range(len(inBoneArray)):
     802            oriParentObj = inBoneArray[i].parent
     803            if oriParentObj is not None:
     804                skinBoneParentObjName = self.name.replace_name_part("Base", oriParentObj.name, definedSkinBoneBaseName)
     805                skinBoneParentObjName = self.name.replace_filtering_char(skinBoneParentObjName, skinBoneFilteringChar)
     806                bones[i].parent = rt.getNodeByName(skinBoneParentObjName)
     807            else:
     808                bones[i].parent = None
     809
     810        for item in bones:
     811            item.showLinks = True
     812            item.showLinksOnly = True
     813
     814        for item in bones:
     815            item.name = self.name.replace_name_part("Base", item.name, skinBoneBaseName)
     816
     817        if link:
     818            self.link_skin_bones(bones, inBoneArray)
     819
     820        if skipNub:
     821            for item in bones:
     822                if not rt.matchPattern(item.name, pattern=("*" + self.name.get_name_part_value_by_description("Nub", "Nub"))):
     823                    returnBones.append(item)
     824                else:
     825                    rt.delete(item)
     826        else:
     827            returnBones = bones.copy()
     828
     829        bones.clear()
     830
     831        return returnBones
     832
     833    def create_skin_bone_from_bip(self, inBoneArray, skipNub=True, mesh=False, link=True, skinBoneBaseName=""):
     834        """
     835        바이페드 객체에서 스킨 뼈대 생성.
     836
     837        Args:
     838            inBoneArray: 바이페드 객체 배열
     839            skipNub: Nub 뼈대 건너뛰기 (기본값: True)
     840            mesh: 메시 스냅샷 사용 (기본값: False)
     841            link: 원본 뼈대에 연결 (기본값: True)
     842            skinBoneBaseName: 스킨 뼈대 기본 이름 (기본값: "")
     843
     844        Returns:
     845            생성된 스킨 뼈대 배열
     846        """
     847        # 바이페드 객체만 필터링, Twist 뼈대 제외, 루트 노드 제외
     848        targetBones = [item for item in inBoneArray
     849                      if (rt.classOf(item) == rt.Biped_Object)
     850                      and (not rt.matchPattern(item.name, pattern="*Twist*"))
     851                      and (item != item.controller.rootNode)]
     852
     853        returnSkinBones = self.create_skin_bone(targetBones, skipNub=skipNub, mesh=mesh, link=link, skinBoneBaseName=skinBoneBaseName)
     854
     855        return returnSkinBones
     856
     857    def create_skin_bone_from_bip_for_unreal(self, inBoneArray, skipNub=True, mesh=False, link=True, skinBoneBaseName=""):
     858        """
     859        언리얼 엔진용 바이페드 객체에서 스킨 뼈대 생성.
     860
     861        Args:
     862            inBoneArray: 바이페드 객체 배열
     863            skipNub: Nub 뼈대 건너뛰기 (기본값: True)
     864            mesh: 메시 스냅샷 사용 (기본값: False)
     865            link: 원본 뼈대에 연결 (기본값: True)
     866            skinBoneBaseName: 스킨 뼈대 기본 이름 (기본값: "b")
     867
     868        Returns:
     869            생성된 스킨 뼈대 배열 또는 False (실패 시)
     870        """
     871        genBones = self.create_skin_bone_from_bip(inBoneArray, skipNub=skipNub, mesh=mesh, link=link, skinBoneBaseName=skinBoneBaseName)
     872        if len(genBones) == 0:
     873            return False
     874
     875        # 언리얼 엔진용으로 특정 뼈대 회전
     876        for item in genBones:
     877            if rt.matchPattern(item.name, pattern="*Pelvis*"):
     878                self.anim.rotate_local(item, 180, 0, 0)
     879            if rt.matchPattern(item.name, pattern="*Spine*"):
     880                self.anim.rotate_local(item, 180, 0, 0)
     881            if rt.matchPattern(item.name, pattern="*Neck*"):
     882                self.anim.rotate_local(item, 180, 0, 0)
     883            if rt.matchPattern(item.name, pattern="*Head*"):
     884                self.anim.rotate_local(item, 180, 0, 0)
     885
     886        return genBones
     887
     888    def gen_missing_bip_bones_for_ue5manny(self, inBoneArray):
     889        returnBones = []
     890        spine3 = None
     891        neck = None
     892
     893        handL = None
     894        handR = None
     895
     896        fingerNames = ["index", "middle", "ring", "pinky"]
     897        knuckleName = "metacarpal"
     898        lKnuckleDistance = []
     899        rKnuckleDistance = []
     900
     901        lFingers = []
     902        rFingers = []
     903
     904        for item in inBoneArray:
     905            if rt.matchPattern(item.name, pattern="*spine 03"):
     906                spine3 = item
     907            if rt.matchPattern(item.name, pattern="*neck 01"):
     908                neck = item
     909            if rt.matchPattern(item.name, pattern="*hand*l"):
     910                handL = item
     911            if rt.matchPattern(item.name, pattern="*hand*r"):
     912                handR = item
     913
     914            for fingerName in fingerNames:
     915                if rt.matchPattern(item.name, pattern="*"+fingerName+"*01*l"):
     916                    lFingers.append(item)
     917                if rt.matchPattern(item.name, pattern="*"+fingerName+"*01*r"):
     918                    rFingers.append(item)
     919            for finger in lFingers:
     920                fingerDistance = rt.distance(finger, handL)
     921                lKnuckleDistance.append(fingerDistance)
     922            for finger in rFingers:
     923                fingerDistance = rt.distance(finger, handR)
     924                rKnuckleDistance.append(fingerDistance)
     925
     926        filteringChar = self.name._get_filtering_char(inBoneArray[-1].name)
     927        isLower = inBoneArray[-1].name[0].islower()
     928        spineName = self.name.get_name_part_value_by_description("Base", "Biped") + filteringChar + "Spine"
     929
     930        spine4 = self.create_nub_bone(spineName, 2)
     931        spine5 = self.create_nub_bone(spineName, 2)
     932
     933        spine4.name = self.name.replace_name_part("Index", spine4.name, "4")
     934        spine4.name = self.name.remove_name_part("Nub", spine4.name)
     935        spine5.name = self.name.replace_name_part("Index", spine5.name, "5")
     936        spine5.name = self.name.remove_name_part("Nub", spine5.name)
     937        if isLower:
     938            spine4.name = spine4.name.lower()
     939            spine5.name = spine5.name.lower()
     940
     941        spineDistance = rt.distance(spine3, neck)/3.0
     942        rt.setProperty(spine4, "transform", spine3.transform)
     943        rt.setProperty(spine5, "transform", spine3.transform)
     944        self.anim.move_local(spine4, spineDistance, 0, 0)
     945        self.anim.move_local(spine5, spineDistance * 2, 0, 0)
     946
     947        returnBones.append(spine4)
     948        returnBones.append(spine5)
     949
     950        for i, finger in enumerate(lFingers):
     951            knuckleBoneName = self.name.add_suffix_to_real_name(finger.name, filteringChar+knuckleName)
     952            knuckleBoneName = self.name.remove_name_part("Index", knuckleBoneName)
     953
     954            knuckleBone = self.create_nub_bone(knuckleBoneName, 2)
     955            knuckleBone.name = self.name.remove_name_part("Nub", knuckleBone.name)
     956            if isLower:
     957                knuckleBone.name = knuckleBone.name.lower()
     958
     959            knuckleBone.transform = finger.transform
     960            lookAtConst = self.const.assign_lookat(knuckleBone, handL)
     961            lookAtConst.upnode_world = False
     962            lookAtConst.pickUpNode = handL
     963            lookAtConst.lookat_vector_length = 0.0
     964            lookAtConst.target_axisFlip = True
     965            self.const.collapse(knuckleBone)
     966            self.anim.move_local(knuckleBone, -lKnuckleDistance[i]*0.8, 0, 0)
     967
     968            returnBones.append(knuckleBone)
     969
     970        for i, finger in enumerate(rFingers):
     971            knuckleBoneName = self.name.add_suffix_to_real_name(finger.name, filteringChar+knuckleName)
     972            knuckleBoneName = self.name.remove_name_part("Index", knuckleBoneName)
     973
     974            knuckleBone = self.create_nub_bone(knuckleBoneName, 2)
     975            knuckleBone.name = self.name.remove_name_part("Nub", knuckleBone.name)
     976            if isLower:
     977                knuckleBone.name = knuckleBone.name.lower()
     978
     979            knuckleBone.transform = finger.transform
     980            lookAtConst = self.const.assign_lookat(knuckleBone, handR)
     981            lookAtConst.upnode_world = False
     982            lookAtConst.pickUpNode = handR
     983            lookAtConst.lookat_vector_length = 0.0
     984            lookAtConst.target_axisFlip = True
     985            self.const.collapse(knuckleBone)
     986            self.anim.move_local(knuckleBone, -rKnuckleDistance[i]*0.8, 0, 0)
     987
     988            returnBones.append(knuckleBone)
     989
     990        return returnBones
     991
     992    def relink_missing_bip_bones_for_ue5manny(self, inBipArray, inMissingBoneArray):
     993        returnBones = []
     994
     995        spine3 = None
     996
     997        handL = None
     998        handR = None
     999
    1000        knuckleName = "metacarpal"
    1001
    1002        for item in inBipArray:
    1003            if rt.matchPattern(item.name, pattern="*spine 03"):
    1004                spine3 = item
    1005            if rt.matchPattern(item.name, pattern="*hand*l"):
    1006                handL = item
    1007            if rt.matchPattern(item.name, pattern="*hand*r"):
    1008                handR = item
    1009
    1010        for item in inMissingBoneArray:
    1011            if rt.matchPattern(item.name, pattern="*spine*"):
    1012                item.parent = spine3
    1013            if rt.matchPattern(item.name, pattern=f"*{knuckleName}*l"):
    1014                item.parent = handL
    1015            if rt.matchPattern(item.name, pattern=f"*{knuckleName}*r"):
    1016                item.parent = handR
    1017
    1018        returnBones.append(inBipArray)
    1019        returnBones.append(inMissingBoneArray)
    1020        return returnBones
    1021
    1022    def relink_missing_skin_bones_for_ue5manny(self, inSkinArray):
    1023        returnBones = []
    1024        spine3 = None
    1025        spine4 = None
    1026        spine5 = None
    1027
    1028        neck = None
    1029        clavicleL = None
    1030        clavicleR = None
    1031
    1032        handL = None
    1033        handR = None
    1034
    1035        fingerNames = ["index", "middle", "ring", "pinky"]
    1036        knuckleName = "metacarpal"
    1037
    1038        lFingers = []
    1039        rFingers = []
    1040
    1041        for item in inSkinArray:
    1042            if rt.matchPattern(item.name, pattern="*spine*03"):
    1043                spine3 = item
    1044            if rt.matchPattern(item.name, pattern="*neck*01"):
    1045                neck = item
    1046            if rt.matchPattern(item.name, pattern="*clavicle*l"):
    1047                clavicleL = item
    1048            if rt.matchPattern(item.name, pattern="*clavicle*r"):
    1049                clavicleR = item
    1050
    1051            if rt.matchPattern(item.name, pattern="*hand*l"):
    1052                handL = item
    1053            if rt.matchPattern(item.name, pattern="*hand*r"):
    1054                handR = item
    1055
    1056            for fingerName in fingerNames:
    1057                if rt.matchPattern(item.name, pattern="*"+fingerName+"*01*l"):
    1058                    lFingers.append(item)
    1059                if rt.matchPattern(item.name, pattern="*"+fingerName+"*01*r"):
    1060                    rFingers.append(item)
    1061
    1062        for item in inSkinArray:
    1063            if rt.matchPattern(item.name, pattern="*spine*04"):
    1064                spine4 = item
    1065                item.parent = spine3
    1066
    1067            if rt.matchPattern(item.name, pattern="*spine*05"):
    1068                spine5 = item
    1069                item.parent = spine4
    1070                neck.parent = spine5
    1071                clavicleL.parent = spine5
    1072                clavicleR.parent = spine5
    1073
    1074            if rt.matchPattern(item.name, pattern=f"*{knuckleName}*l"):
    1075                item.parent = handL
    1076            if rt.matchPattern(item.name, pattern=f"*{knuckleName}*r"):
    1077                item.parent = handR
    1078
    1079        filteringChar = self.name._get_filtering_char(inSkinArray[-1].name)
    1080
    1081        for item in lFingers:
    1082            fingerNamePattern = self.name.add_suffix_to_real_name(item.name, filteringChar+knuckleName)
    1083            fingerNamePattern = self.name.remove_name_part("Index", fingerNamePattern)
    1084            for knuckle in inSkinArray:
    1085                if rt.matchPattern(knuckle.name, pattern=fingerNamePattern):
    1086                    item.parent = knuckle
    1087                    break
    1088
    1089        for item in rFingers:
    1090            fingerNamePattern = self.name.add_suffix_to_real_name(item.name, filteringChar+knuckleName)
    1091            fingerNamePattern = self.name.remove_name_part("Index", fingerNamePattern)
    1092            for knuckle in inSkinArray:
    1093                if rt.matchPattern(knuckle.name, pattern=fingerNamePattern):
    1094                    item.parent = knuckle
    1095                    break
    1096
    1097        return returnBones
    1098
    1099    def create_skin_bone_from_bip_for_ue5manny(self, inBoneArray, skipNub=True, mesh=False, link=True, isHuman=False, skinBoneBaseName=""):
    1100        targetBones = [item for item in inBoneArray
    1101                      if (rt.classOf(item) == rt.Biped_Object)
    1102                      and (not rt.matchPattern(item.name, pattern="*Twist*"))
    1103                      and (item != item.controller.rootNode)]
    1104
    1105        missingBipBones = []
    1106
    1107        if isHuman:
    1108            missingBipBones = self.gen_missing_bip_bones_for_ue5manny(targetBones)
    1109            self.relink_missing_bip_bones_for_ue5manny(targetBones, missingBipBones)
    1110
    1111        for item in missingBipBones:
    1112            targetBones.append(item)
    1113
    1114        sortedBipBones = self.sort_bones_as_hierarchy(targetBones)
    1115
    1116        skinBones = self.create_skin_bone(sortedBipBones, skipNub=skipNub, mesh=mesh, link=False, skinBoneBaseName=skinBoneBaseName)
    1117        if len(skinBones) == 0:
    1118            return False
    1119
    1120        for item in skinBones:
    1121            if rt.matchPattern(item.name, pattern="*pelvis*"):
    1122                self.anim.rotate_local(item, 180, 0, 0, dontAffectChildren=True)
    1123            if rt.matchPattern(item.name, pattern="*spine*"):
    1124                self.anim.rotate_local(item, 180, 0, 0, dontAffectChildren=True)
    1125            if rt.matchPattern(item.name, pattern="*neck*"):
    1126                self.anim.rotate_local(item, 180, 0, 0, dontAffectChildren=True)
    1127            if rt.matchPattern(item.name, pattern="*head*"):
    1128                self.anim.rotate_local(item, 180, 0, 0, dontAffectChildren=True)
    1129            if rt.matchPattern(item.name, pattern="*thigh*l"):
    1130                self.anim.rotate_local(item, 0, 0, 180, dontAffectChildren=True)
    1131            if rt.matchPattern(item.name, pattern="*calf*l"):
    1132                self.anim.rotate_local(item, 0, 0, 180, dontAffectChildren=True)
    1133            if rt.matchPattern(item.name, pattern="*foot*l"):
    1134                self.anim.rotate_local(item, 0, 0, 180, dontAffectChildren=True)
    1135            if rt.matchPattern(item.name, pattern="*ball*r"):
    1136                self.anim.rotate_local(item, 0, 0, 180, dontAffectChildren=True)
    1137
    1138            if rt.matchPattern(item.name, pattern="*clavicle*r"):
    1139                self.anim.rotate_local(item, 0, 0, -180, dontAffectChildren=True)
    1140            if rt.matchPattern(item.name, pattern="*upperarm*r"):
    1141                self.anim.rotate_local(item, 0, 0, -180, dontAffectChildren=True)
    1142            if rt.matchPattern(item.name, pattern="*lowerarm*r"):
    1143                self.anim.rotate_local(item, 0, 0, -180, dontAffectChildren=True)
    1144            if rt.matchPattern(item.name, pattern="*hand*r"):
    1145                self.anim.rotate_local(item, 0, 0, -180, dontAffectChildren=True)
    1146
    1147            if rt.matchPattern(item.name, pattern="*thumb*r"):
    1148                self.anim.rotate_local(item, 0, 0, 180, dontAffectChildren=True)
    1149            if rt.matchPattern(item.name, pattern="*index*r"):
    1150                self.anim.rotate_local(item, 0, 0, 180, dontAffectChildren=True)
    1151            if rt.matchPattern(item.name, pattern="*middle*r"):
    1152                self.anim.rotate_local(item, 0, 0, 180, dontAffectChildren=True)
    1153            if rt.matchPattern(item.name, pattern="*ring*r"):
    1154                self.anim.rotate_local(item, 0, 0, 180, dontAffectChildren=True)
    1155            if rt.matchPattern(item.name, pattern="*pinky*r"):
    1156                self.anim.rotate_local(item, 0, 0, 180, dontAffectChildren=True)
    1157
    1158            if rt.matchPattern(item.name, pattern="*metacarpal*"):
    1159                tempArray = self.name._split_to_array(item.name)
    1160                item.name = self.name._combine(tempArray, inFilChar="_")
    1161                item.name = self.name.remove_name_part("Base", item.name)
    1162
    1163            self.anim.save_xform(item)
    1164
    1165        self.relink_missing_skin_bones_for_ue5manny(skinBones)
    1166
    1167        self.link_skin_bones(skinBones, sortedBipBones)
    1168        for item in skinBones:
    1169            self.anim.save_xform(item)
    1170
    1171        return skinBones
    1172
    1173    def set_bone_on(self, inBone):
    1174        """
    1175        뼈대 활성화.
    1176
    1177        Args:
    1178            inBone: 활성화할 뼈대 객체
    1179        """
    1180        if rt.classOf(inBone) == rt.BoneGeometry:
    1181            inBone.boneEnable = True
    1182
    1183    def set_bone_off(self, inBone):
    1184        """
    1185        뼈대 비활성화.
    1186
    1187        Args:
    1188            inBone: 비활성화할 뼈대 객체
    1189        """
    1190        if rt.classOf(inBone) == rt.BoneGeometry:
    1191            inBone.boneEnable = False
    1192
    1193    def set_bone_on_selection(self):
    1194        """
    1195        선택된 모든 뼈대 활성화.
    1196        """
    1197        selArray = list(rt.getCurrentSelection())
    1198        for item in selArray:
    1199            self.set_bone_on(item)
    1200
    1201    def set_bone_off_selection(self):
    1202        """
    1203        선택된 모든 뼈대 비활성화.
    1204        """
    1205        selArray = list(rt.getCurrentSelection())
    1206        for item in selArray:
    1207            self.set_bone_off(item)
    1208
    1209    def set_freeze_length_on(self, inBone):
    1210        """
    1211        뼈대 길이 고정 활성화.
    1212
    1213        Args:
    1214            inBone: 길이를 고정할 뼈대 객체
    1215        """
    1216        if rt.classOf(inBone) == rt.BoneGeometry:
    1217            inBone.boneFreezeLength = True
    1218
    1219    def set_freeze_length_off(self, inBone):
    1220        """
    1221        뼈대 길이 고정 비활성화.
    1222
    1223        Args:
    1224            inBone: 길이 고정을 해제할 뼈대 객체
    1225        """
    1226        if rt.classOf(inBone) == rt.BoneGeometry:
    1227            inBone.boneFreezeLength = False
    1228
    1229    def set_freeze_length_on_selection(self):
    1230        """
    1231        선택된 모든 뼈대의 길이 고정 활성화.
    1232        """
    1233        selArray = list(rt.getCurrentSelection())
    1234        for item in selArray:
    1235            self.set_freeze_length_on(item)
    1236
    1237    def set_freeze_length_off_selection(self):
    1238        """
    1239        선택된 모든 뼈대의 길이 고정 비활성화.
    1240        """
    1241        selArray = list(rt.getCurrentSelection())
    1242        for item in selArray:
    1243            self.set_freeze_length_off(item)

뼈대(Bone) 관련 기능을 제공하는 클래스. MAXScript의 _Bone 구조체 개념을 Python으로 재구현한 클래스이며, 3ds Max의 기능들을 pymxs API를 통해 제어합니다.

Bone( nameService=None, animService=None, helperService=None, constraintService=None) View Source

    26    def __init__(self, nameService=None, animService=None, helperService=None, constraintService=None):
    27        """
    28        클래스 초기화.
    29
    30        Args:
    31            nameService: 이름 처리 서비스 (제공되지 않으면 새로 생성)
    32            animService: 애니메이션 서비스 (제공되지 않으면 새로 생성)
    33            helperService: 헬퍼 객체 서비스 (제공되지 않으면 새로 생성)
    34            constraintService: 제약 서비스 (제공되지 않으면 새로 생성)
    35        """
    36        self.name = nameService if nameService else Name()
    37        self.anim = animService if animService else Anim()
    38        self.helper = helperService if helperService else Helper(nameService=self.name)
    39        self.const = constraintService if constraintService else Constraint(nameService=self.name, helperService=self.helper)

클래스 초기화.

Args: nameService: 이름 처리 서비스 (제공되지 않으면 새로 생성) animService: 애니메이션 서비스 (제공되지 않으면 새로 생성) helperService: 헬퍼 객체 서비스 (제공되지 않으면 새로 생성) constraintService: 제약 서비스 (제공되지 않으면 새로 생성)

name
anim
helper
const
def remove_ik(self, inBone): View Source

    41    def remove_ik(self, inBone):
    42        """
    43        뼈대에서 IK 체인을 제거.
    44
    45        Args:
    46            inBone: IK 체인을 제거할 뼈대 객체
    47        """
    48        # pos 또는 rotation 속성이 없는 경우에만 IK 체인 제거
    49        if (not rt.isProperty(inBone, "pos")) or (not rt.isProperty(inBone, "rotation")):
    50            rt.HDIKSys.RemoveChain(inBone)

뼈대에서 IK 체인을 제거.

Args: inBone: IK 체인을 제거할 뼈대 객체

def get_bone_assemblyHead(self, inBone): View Source

    52    def get_bone_assemblyHead(self, inBone):
    53        """
    54        뼈대 어셈블리의 헤드를 가져옴.
    55
    56        Args:
    57            inBone: 대상 뼈대 객체
    58
    59        Returns:
    60            어셈블리 헤드 또는 None
    61        """
    62        tempBone = inBone
    63        while tempBone is not None:
    64            if tempBone.assemblyHead:
    65                return tempBone
    66            if not tempBone.assemblyMember:
    67                break
    68            tempBone = tempBone.parent
    69
    70        return None

뼈대 어셈블리의 헤드를 가져옴.

Args: inBone: 대상 뼈대 객체

Returns: 어셈블리 헤드 또는 None

def put_child_into_bone_assembly(self, inBone): View Source

    72    def put_child_into_bone_assembly(self, inBone):
    73        """
    74        자식 뼈대를 어셈블리에 추가.
    75
    76        Args:
    77            inBone: 어셈블리에 추가할 자식 뼈대
    78        """
    79        if inBone.parent is not None and inBone.parent.assemblyMember:
    80            inBone.assemblyMember = True
    81            inBone.assemblyMemberOpen = True

자식 뼈대를 어셈블리에 추가.

Args: inBone: 어셈블리에 추가할 자식 뼈대

def sort_bones_as_hierarchy(self, inBoneArray): View Source

     83    def sort_bones_as_hierarchy(self, inBoneArray):
     84        """
     85        뼈대 배열을 계층 구조에 따라 정렬.
     86
     87        Args:
     88            inBoneArray: 정렬할 뼈대 객체 배열
     89
     90        Returns:
     91            계층 구조에 따라 정렬된 뼈대 배열
     92        """
     93        # BoneLevel 구조체 정의 (Python 클래스로 구현)
     94        @dataclass
     95        class BoneLevel:
     96            index: int
     97            level: int
     98
     99        # 뼈대 구조체 배열 초기화
    100        bones = []
    101
    102        # 뼈대 구조체 배열 채우기. 계층 수준을 0으로 초기화
    103        for i in range(len(inBoneArray)):
    104            bones.append(BoneLevel(i, 0))
    105
    106        # 뼈대 배열의 각 뼈대에 대한 계층 수준 계산
    107        # 계층 수준은 현재 뼈대와 루트 노드 사이의 조상 수
    108        for i in range(len(bones)):
    109            node = inBoneArray[bones[i].index]
    110            n = 0
    111            while node is not None:
    112                n += 1
    113                node = node.parent
    114            bones[i].level = n
    115
    116        # 계층 수준에 따라 뼈대 배열 정렬
    117        bones.sort(key=lambda x: x.level)
    118
    119        # 정렬된 뼈대를 저장할 새 배열 준비
    120        returnBonesArray = []
    121        for i in range(len(inBoneArray)):
    122            returnBonesArray.append(inBoneArray[bones[i].index])
    123
    124        return returnBonesArray

뼈대 배열을 계층 구조에 따라 정렬.

Args: inBoneArray: 정렬할 뼈대 객체 배열

Returns: 계층 구조에 따라 정렬된 뼈대 배열

def correct_negative_stretch(self, bone, ask=True): View Source

    126    def correct_negative_stretch(self, bone, ask=True):
    127        """
    128        뼈대의 음수 스케일 보정.
    129
    130        Args:
    131            bone: 보정할 뼈대 객체
    132            ask: 사용자에게 확인 요청 여부 (기본값: True)
    133
    134        Returns:
    135            None
    136        """
    137        axisIndex = 0
    138
    139        # 뼈대 축에 따라 인덱스 설정
    140        if bone.boneAxis == rt.Name("X"):
    141            axisIndex = 0
    142        elif bone.boneAxis == rt.Name("Y"):
    143            axisIndex = 1
    144        elif bone.boneAxis == rt.Name("Z"):
    145            axisIndex = 2
    146
    147        ooscale = bone.objectOffsetScale
    148
    149        # 음수 스케일 보정
    150        if (ooscale[axisIndex] < 0) and ((not ask) or rt.queryBox("Correct negative scale?", title=bone.Name)):
    151            ooscale[axisIndex] = -ooscale[axisIndex]
    152            axisIndex = axisIndex + 2
    153            if axisIndex > 2:
    154                axisIndex = axisIndex - 3
    155            ooscale[axisIndex] = -ooscale[axisIndex]
    156            bone.objectOffsetScale = ooscale

뼈대의 음수 스케일 보정.

Args: bone: 보정할 뼈대 객체 ask: 사용자에게 확인 요청 여부 (기본값: True)

Returns: None

def reset_scale_of_selected_bones(self, ask=True): View Source

    158    def reset_scale_of_selected_bones(self, ask=True):
    159        """
    160        선택된 뼈대들의 스케일 초기화.
    161
    162        Args:
    163            ask: 음수 스케일 보정 확인 요청 여부 (기본값: True)
    164
    165        Returns:
    166            None
    167        """
    168        # 선택된 객체 중 BoneGeometry 타입만 수집
    169        bones = [item for item in rt.selection if rt.classOf(item) == rt.BoneGeometry]
    170
    171        # 계층 구조에 따라 뼈대 정렬
    172        bones = self.sort_bones_as_hierarchy(rt.selection)
    173
    174        # 뼈대 배열의 모든 뼈대에 대해 스케일 초기화
    175        for i in range(len(bones)):
    176            rt.ResetScale(bones[i])
    177            if ask:
    178                self.correct_negative_stretch(bones[i], False)

선택된 뼈대들의 스케일 초기화.

Args: ask: 음수 스케일 보정 확인 요청 여부 (기본값: True)

Returns: None

def is_nub_bone(self, inputBone): View Source

    180    def is_nub_bone(self, inputBone):
    181        """
    182        뼈대가 Nub 뼈대인지 확인 (부모 및 자식이 없는 단일 뼈대).
    183
    184        Args:
    185            inputBone: 확인할 뼈대 객체
    186
    187        Returns:
    188            True: Nub 뼈대인 경우
    189            False: 그 외의 경우
    190        """
    191        if rt.classOf(inputBone) == rt.BoneGeometry:
    192            if inputBone.parent is None and inputBone.children.count == 0:
    193                return True
    194            else:
    195                return False
    196        return False

뼈대가 Nub 뼈대인지 확인 (부모 및 자식이 없는 단일 뼈대).

Args: inputBone: 확인할 뼈대 객체

Returns: True: Nub 뼈대인 경우 False: 그 외의 경우

def is_end_bone(self, inputBone): View Source

    198    def is_end_bone(self, inputBone):
    199        """
    200        뼈대가 End 뼈대인지 확인 (부모는 있지만 자식이 없는 뼈대).
    201
    202        Args:
    203            inputBone: 확인할 뼈대 객체
    204
    205        Returns:
    206            True: End 뼈대인 경우
    207            False: 그 외의 경우
    208        """
    209        if rt.classOf(inputBone) == rt.BoneGeometry:
    210            if inputBone.parent is not None and inputBone.children.count == 0:
    211                return True
    212            else:
    213                return False
    214        return False

뼈대가 End 뼈대인지 확인 (부모는 있지만 자식이 없는 뼈대).

Args: inputBone: 확인할 뼈대 객체

Returns: True: End 뼈대인 경우 False: 그 외의 경우

def create_nub_bone(self, inName, inSize): View Source

    216    def create_nub_bone(self, inName, inSize):
    217        """
    218        Nub 뼈대 생성.
    219
    220        Args:
    221            inName: 뼈대 이름
    222            inSize: 뼈대 크기
    223
    224        Returns:
    225            생성된 Nub 뼈대
    226        """
    227        nubBone = None
    228
    229        # 화면 갱신 중지 상태에서 뼈대 생성
    230        rt.disableSceneRedraw()
    231
    232        # 뼈대 생성 및 속성 설정
    233        nubBone = rt.BoneSys.createBone(rt.Point3(0, 0, 0), rt.Point3(1, 0, 0), rt.Point3(0, 0, 1))
    234
    235        nubBone.width = inSize
    236        nubBone.height = inSize
    237        nubBone.taper = 90
    238        nubBone.length = inSize
    239        nubBone.frontfin = False
    240        nubBone.backfin = False
    241        nubBone.sidefins = False
    242        nubBone.name = self.name.remove_name_part("Index", inName)
    243        nubBone.name = self.name.replace_name_part("Nub", nubBone.name, self.name.get_name_part_value_by_description("Nub", "Nub"))
    244
    245        # 화면 갱신 재개
    246        rt.enableSceneRedraw()
    247        rt.redrawViews()
    248
    249        return nubBone

Nub 뼈대 생성.

Args: inName: 뼈대 이름 inSize: 뼈대 크기

Returns: 생성된 Nub 뼈대

def create_nub_bone_on_obj(self, inObj, inSize=1): View Source

    251    def create_nub_bone_on_obj(self, inObj, inSize=1):
    252        """
    253        객체 위치에 Nub 뼈대 생성.
    254
    255        Args:
    256            inObj: 위치를 참조할 객체
    257            inSize: 뼈대 크기 (기본값: 1)
    258
    259        Returns:
    260            생성된 Nub 뼈대
    261        """
    262        boneName = self.name.get_string(inObj.name)
    263        newBone = self.create_nub_bone(boneName, inSize)
    264        newBone.transform = inObj.transform
    265
    266        return newBone

객체 위치에 Nub 뼈대 생성.

Args: inObj: 위치를 참조할 객체 inSize: 뼈대 크기 (기본값: 1)

Returns: 생성된 Nub 뼈대

def create_end_bone(self, inBone): View Source

    268    def create_end_bone(self, inBone):
    269        """
    270        뼈대의 끝에 End 뼈대 생성.
    271
    272        Args:
    273            inBone: 부모가 될 뼈대 객체
    274
    275        Returns:
    276            생성된 End 뼈대
    277        """
    278        parentBone = inBone
    279        parentTrans = parentBone.transform
    280        parentPos = parentTrans.translation
    281        boneName = self.name.get_string(parentBone.name)
    282        newBone = self.create_nub_bone(boneName, parentBone.width)
    283
    284        newBone.transform = parentTrans
    285
    286        # 로컬 좌표계에서 이동
    287        self.anim.move_local(newBone, parentBone.length, 0, 0)
    288
    289        newBone.parent = parentBone
    290        self.put_child_into_bone_assembly(newBone)
    291
    292        # 뼈대 속성 설정
    293        newBone.width = parentBone.width
    294        newBone.height = parentBone.height
    295        newBone.frontfin = False
    296        newBone.backfin = False
    297        newBone.sidefins = False
    298        newBone.taper = 90
    299        newBone.length = (parentBone.width + parentBone.height) / 2
    300        newBone.wirecolor = parentBone.wirecolor
    301
    302        return newBone

뼈대의 끝에 End 뼈대 생성.

Args: inBone: 부모가 될 뼈대 객체

Returns: 생성된 End 뼈대

def create_bone( self, inPointArray, inName, end=True, delPoint=False, parent=False, size=2, normals=None): View Source

    304    def create_bone(self, inPointArray, inName, end=True, delPoint=False, parent=False, size=2, normals=None):
    305        """
    306        포인트 배열을 따라 뼈대 체인 생성.
    307
    308        Args:
    309            inPointArray: 뼈대 위치를 정의하는 포인트 배열
    310            inName: 뼈대 기본 이름
    311            end: End 뼈대 생성 여부 (기본값: True)
    312            delPoint: 포인트 삭제 여부 (기본값: False)
    313            parent: 부모 Nub 포인트 생성 여부 (기본값: False)
    314            size: 뼈대 크기 (기본값: 2)
    315            normals: 법선 벡터 배열 (기본값: None)
    316
    317        Returns:
    318            생성된 뼈대 배열 또는 False (실패 시)
    319        """
    320        if normals is None:
    321            normals = []
    322
    323        tempBone = None
    324        newBone = None
    325
    326        returnBoneArray = []
    327
    328        if len(inPointArray) != 1:
    329            for i in range(len(inPointArray) - 1):
    330                boneNum = i
    331
    332                if len(normals) == len(inPointArray):
    333                    xDir = rt.normalize(inPointArray[i+1].transform.position - inPointArray[i].transform.position)
    334                    zDir = rt.normalize(rt.cross(xDir, normals[i]))
    335                    newBone = rt.BoneSys.createBone(inPointArray[i].transform.position, inPointArray[i+1].transform.position, zDir)
    336                else:
    337                    newBone = rt.BoneSys.createBone(inPointArray[i].transform.position, inPointArray[i+1].transform.position, rt.Point3(0, -1, 0))
    338
    339                newBone.boneFreezeLength = True
    340                newBone.name = self.name.replace_name_part("Index", inName, str(boneNum))
    341                newBone.height = size
    342                newBone.width = size
    343                newBone.frontfin = False
    344                newBone.backfin = False
    345                newBone.sidefins = False
    346
    347                returnBoneArray.append(newBone)
    348
    349                if tempBone is not None:
    350                    tempTm = rt.copy(newBone.transform * rt.Inverse(tempBone.transform))
    351                    localRot = rt.quatToEuler(tempTm.rotation).x
    352
    353                    self.anim.rotate_local(newBone, -localRot, 0, 0)
    354
    355                newBone.parent = tempBone
    356                tempBone = newBone
    357
    358            if delPoint:
    359                for i in range(len(inPointArray)):
    360                    if (rt.classOf(inPointArray[i]) == rt.Dummy) or (rt.classOf(inPointArray[i]) == rt.ExposeTm) or (rt.classOf(inPointArray[i]) == rt.Point):
    361                        rt.delete(inPointArray[i])
    362
    363            if parent:
    364                parentNubPointName = self.name.replace_type(inName, self.name.get_parent_str())
    365                parentNubPoint = self.helper.create_point(parentNubPointName, size=size, boxToggle=True, crossToggle=True)
    366                parentNubPoint.transform = returnBoneArray[0].transform
    367                returnBoneArray[0].parent = parentNubPoint
    368
    369            rt.select(newBone)
    370
    371            if end:
    372                endBone = self.create_end_bone(newBone)
    373                returnBoneArray.append(endBone)
    374
    375                rt.clearSelection()
    376
    377                return returnBoneArray
    378            else:
    379                return returnBoneArray
    380        else:
    381            return False

포인트 배열을 따라 뼈대 체인 생성.

Args: inPointArray: 뼈대 위치를 정의하는 포인트 배열 inName: 뼈대 기본 이름 end: End 뼈대 생성 여부 (기본값: True) delPoint: 포인트 삭제 여부 (기본값: False) parent: 부모 Nub 포인트 생성 여부 (기본값: False) size: 뼈대 크기 (기본값: 2) normals: 법선 벡터 배열 (기본값: None)

Returns: 생성된 뼈대 배열 또는 False (실패 시)

def create_simple_bone(self, inLength, inName, end=True, size=1): View Source

    383    def create_simple_bone(self, inLength, inName, end=True, size=1):
    384        """
    385        간단한 뼈대 생성 (시작점과 끝점 지정).
    386
    387        Args:
    388            inLength: 뼈대 길이
    389            inName: 뼈대 이름
    390            end: End 뼈대 생성 여부 (기본값: True)
    391            size: 뼈대 크기 (기본값: 1)
    392
    393        Returns:
    394            생성된 뼈대 배열
    395        """
    396        startPoint = self.helper.create_point("tempStart")
    397        endPoint = self.helper.create_point("tempEnd", pos=(inLength, 0, 0))
    398        returnBoneArray = self.create_bone([startPoint, endPoint], inName, end=end, delPoint=True, size=size)
    399
    400        return returnBoneArray

간단한 뼈대 생성 (시작점과 끝점 지정).

Args: inLength: 뼈대 길이 inName: 뼈대 이름 end: End 뼈대 생성 여부 (기본값: True) size: 뼈대 크기 (기본값: 1)

Returns: 생성된 뼈대 배열

def create_stretch_bone(self, inPointArray, inName, size=2): View Source

    402    def create_stretch_bone(self, inPointArray, inName, size=2):
    403        """
    404        스트레치 뼈대 생성 (포인트를 따라 움직이는 뼈대).
    405
    406        Args:
    407            inPointArray: 뼈대 위치를 정의하는 포인트 배열
    408            inName: 뼈대 기본 이름
    409            size: 뼈대 크기 (기본값: 2)
    410
    411        Returns:
    412            생성된 스트레치 뼈대 배열
    413        """
    414        tempBone = []
    415        tempBone = self.create_bone(inPointArray, inName, size=size)
    416
    417        for i in range(len(tempBone) - 1):
    418            self.const.assign_pos_const(tempBone[i], inPointArray[i])
    419            self.const.assign_lookat(tempBone[i], inPointArray[i+1])
    420        self.const.assign_pos_const(tempBone[-1], inPointArray[-1])
    421
    422        return tempBone

스트레치 뼈대 생성 (포인트를 따라 움직이는 뼈대).

Args: inPointArray: 뼈대 위치를 정의하는 포인트 배열 inName: 뼈대 기본 이름 size: 뼈대 크기 (기본값: 2)

Returns: 생성된 스트레치 뼈대 배열

def create_simple_stretch_bone(self, inStart, inEnd, inName, squash=False, size=1): View Source

    424    def create_simple_stretch_bone(self, inStart, inEnd, inName, squash=False, size=1):
    425        """
    426        간단한 스트레치 뼈대 생성 (시작점과 끝점 지정).
    427
    428        Args:
    429            inStart: 시작 포인트
    430            inEnd: 끝 포인트
    431            inName: 뼈대 이름
    432            squash: 스쿼시 효과 적용 여부 (기본값: False)
    433            size: 뼈대 크기 (기본값: 1)
    434
    435        Returns:
    436            생성된 스트레치 뼈대 배열
    437        """
    438        returnArray = []
    439        returnArray = self.create_stretch_bone([inStart, inEnd], inName, size=size)
    440        if squash:
    441            returnArray[0].boneScaleType = rt.Name("squash")
    442
    443        return returnArray

간단한 스트레치 뼈대 생성 (시작점과 끝점 지정).

Args: inStart: 시작 포인트 inEnd: 끝 포인트 inName: 뼈대 이름 squash: 스쿼시 효과 적용 여부 (기본값: False) size: 뼈대 크기 (기본값: 1)

Returns: 생성된 스트레치 뼈대 배열

def get_bone_shape(self, inBone): View Source

    445    def get_bone_shape(self, inBone):
    446        """
    447        뼈대의 형태 속성 가져오기.
    448
    449        Args:
    450            inBone: 속성을 가져올 뼈대 객체
    451
    452        Returns:
    453            뼈대 형태 속성 배열
    454        """
    455        returnArray = []
    456        if rt.classOf(inBone) == rt.BoneGeometry:
    457            returnArray = [None] * 16  # 빈 배열 초기화
    458            returnArray[0] = inBone.width
    459            returnArray[1] = inBone.height
    460            returnArray[2] = inBone.taper
    461            returnArray[3] = inBone.length
    462            returnArray[4] = inBone.sidefins
    463            returnArray[5] = inBone.sidefinssize
    464            returnArray[6] = inBone.sidefinsstarttaper
    465            returnArray[7] = inBone.sidefinsendtaper
    466            returnArray[8] = inBone.frontfin
    467            returnArray[9] = inBone.frontfinsize
    468            returnArray[10] = inBone.frontfinstarttaper
    469            returnArray[11] = inBone.frontfinendtaper
    470            returnArray[12] = inBone.backfin
    471            returnArray[13] = inBone.backfinsize
    472            returnArray[14] = inBone.backfinstarttaper
    473            returnArray[15] = inBone.backfinendtaper
    474
    475        return returnArray

뼈대의 형태 속성 가져오기.

Args: inBone: 속성을 가져올 뼈대 객체

Returns: 뼈대 형태 속성 배열

def pasete_bone_shape(self, targetBone, shapeArray): View Source

    477    def pasete_bone_shape(self, targetBone, shapeArray):
    478        """
    479        뼈대에 형태 속성 적용.
    480
    481        Args:
    482            targetBone: 속성을 적용할 뼈대 객체
    483            shapeArray: 적용할 뼈대 형태 속성 배열
    484
    485        Returns:
    486            True: 성공
    487            False: 실패
    488        """
    489        if rt.classOf(targetBone) == rt.BoneGeometry:
    490            targetBone.width = shapeArray[0]
    491            targetBone.height = shapeArray[1]
    492            targetBone.taper = shapeArray[2]
    493            #targetBone.length = shapeArray[3]  # 길이는 변경하지 않음
    494            targetBone.sidefins = shapeArray[4]
    495            targetBone.sidefinssize = shapeArray[5]
    496            targetBone.sidefinsstarttaper = shapeArray[6]
    497            targetBone.sidefinsendtaper = shapeArray[7]
    498            targetBone.frontfin = shapeArray[8]
    499            targetBone.frontfinsize = shapeArray[9]
    500            targetBone.frontfinstarttaper = shapeArray[10]
    501            targetBone.frontfinendtaper = shapeArray[11]
    502            targetBone.backfin = shapeArray[12]
    503            targetBone.backfinsize = shapeArray[13]
    504            targetBone.backfinstarttaper = shapeArray[14]
    505            targetBone.backfinendtaper = shapeArray[15]
    506
    507            if self.is_end_bone(targetBone):
    508                targetBone.taper = 90
    509                targetBone.length = (targetBone.width + targetBone.height) / 2
    510                targetBone.frontfin = False
    511                targetBone.backfin = False
    512                targetBone.sidefins = False
    513
    514            return True
    515        return False

뼈대에 형태 속성 적용.

Args: targetBone: 속성을 적용할 뼈대 객체 shapeArray: 적용할 뼈대 형태 속성 배열

Returns: True: 성공 False: 실패

def set_fin_on( self, inBone, side=True, front=True, back=False, inSize=2.0, inTaper=0.0): View Source

    517    def set_fin_on(self, inBone, side=True, front=True, back=False, inSize=2.0, inTaper=0.0):
    518        """
    519        뼈대의 핀(fin) 설정 활성화.
    520
    521        Args:
    522            inBone: 핀을 설정할 뼈대 객체
    523            side: 측면 핀 활성화 여부 (기본값: True)
    524            front: 전면 핀 활성화 여부 (기본값: True)
    525            back: 후면 핀 활성화 여부 (기본값: False)
    526            inSize: 핀 크기 (기본값: 2.0)
    527            inTaper: 핀 테이퍼 (기본값: 0.0)
    528        """
    529        if rt.classOf(inBone) == rt.BoneGeometry:
    530            if not self.is_end_bone(inBone):
    531                inBone.frontfin = front
    532                inBone.frontfinsize = inSize
    533                inBone.frontfinstarttaper = inTaper
    534                inBone.frontfinendtaper = inTaper
    535
    536                inBone.sidefins = side
    537                inBone.sidefinssize = inSize
    538                inBone.sidefinsstarttaper = inTaper
    539                inBone.sidefinsendtaper = inTaper
    540
    541                inBone.backfin = back
    542                inBone.backfinsize = inSize
    543                inBone.backfinstarttaper = inTaper
    544                inBone.backfinendtaper = inTaper

뼈대의 핀(fin) 설정 활성화.

Args: inBone: 핀을 설정할 뼈대 객체 side: 측면 핀 활성화 여부 (기본값: True) front: 전면 핀 활성화 여부 (기본값: True) back: 후면 핀 활성화 여부 (기본값: False) inSize: 핀 크기 (기본값: 2.0) inTaper: 핀 테이퍼 (기본값: 0.0)

def set_fin_off(self, inBone): View Source

    546    def set_fin_off(self, inBone):
    547        """
    548        뼈대의 모든 핀(fin) 비활성화.
    549
    550        Args:
    551            inBone: 핀을 비활성화할 뼈대 객체
    552        """
    553        if rt.classOf(inBone) == rt.BoneGeometry:
    554            inBone.frontfin = False
    555            inBone.sidefins = False
    556            inBone.backfin = False

뼈대의 모든 핀(fin) 비활성화.

Args: inBone: 핀을 비활성화할 뼈대 객체

def set_bone_size(self, inBone, inSize): View Source

    558    def set_bone_size(self, inBone, inSize):
    559        """
    560        뼈대 크기 설정.
    561
    562        Args:
    563            inBone: 크기를 설정할 뼈대 객체
    564            inSize: 설정할 크기
    565        """
    566        if rt.classOf(inBone) == rt.BoneGeometry:
    567            inBone.width = inSize
    568            inBone.height = inSize
    569
    570            if self.is_end_bone(inBone) or self.is_nub_bone(inBone):
    571                inBone.taper = 90
    572                inBone.length = inSize

뼈대 크기 설정.

Args: inBone: 크기를 설정할 뼈대 객체 inSize: 설정할 크기

def set_bone_taper(self, inBone, inTaper): View Source

    574    def set_bone_taper(self, inBone, inTaper):
    575        """
    576        뼈대 테이퍼 설정.
    577
    578        Args:
    579            inBone: 테이퍼를 설정할 뼈대 객체
    580            inTaper: 설정할 테이퍼 값
    581        """
    582        if rt.classOf(inBone) == rt.BoneGeometry:
    583            if not self.is_end_bone(inBone):
    584                inBone.taper = inTaper

뼈대 테이퍼 설정.

Args: inBone: 테이퍼를 설정할 뼈대 객체 inTaper: 설정할 테이퍼 값

def delete_bones_safely(self, inBoneArray): View Source

    586    def delete_bones_safely(self, inBoneArray):
    587        """
    588        뼈대 배열을 안전하게 삭제.
    589
    590        Args:
    591            inBoneArray: 삭제할 뼈대 배열
    592        """
    593        if len(inBoneArray) > 0:
    594            for targetBone in inBoneArray:
    595                self.const.collapse(targetBone)
    596                targetBone.parent = None
    597                rt.delete(targetBone)
    598
    599            inBoneArray.clear()

뼈대 배열을 안전하게 삭제.

Args: inBoneArray: 삭제할 뼈대 배열

def select_first_children(self, inObj): View Source

    601    def select_first_children(self, inObj):
    602        """
    603        객체의 첫 번째 자식들을 재귀적으로 선택.
    604
    605        Args:
    606            inObj: 시작 객체
    607
    608        Returns:
    609            True: 자식이 있는 경우
    610            False: 자식이 없는 경우
    611        """
    612        rt.selectmore(inObj)
    613
    614        for i in range(inObj.children.count):
    615            if self.select_first_children(inObj.children[i]):
    616                if inObj.children.count == 0 or inObj.children[0] is None:
    617                    return True
    618            else:
    619                return False

객체의 첫 번째 자식들을 재귀적으로 선택.

Args: inObj: 시작 객체

Returns: True: 자식이 있는 경우 False: 자식이 없는 경우

def get_every_children(self, inObj): View Source

    621    def get_every_children(self, inObj):
    622        """
    623        객체의 모든 자식들을 가져옴.
    624
    625        Args:
    626            inObj: 시작 객체
    627
    628        Returns:
    629            자식 객체 배열
    630        """
    631        children = []
    632
    633        if inObj.children.count != 0 and inObj.children[0] is not None:
    634            for i in range(inObj.children.count):
    635                children.append(inObj.children[i])
    636                children.extend(self.get_every_children(inObj.children[i]))
    637
    638        return children

객체의 모든 자식들을 가져옴.

Args: inObj: 시작 객체

Returns: 자식 객체 배열

def select_every_children(self, inObj, includeSelf=False): View Source

    640    def select_every_children(self, inObj, includeSelf=False):
    641        """
    642        객체의 모든 자식들을 선택.
    643
    644        Args:
    645            inObj: 시작 객체
    646            includeSelf: 자신도 포함할지 여부 (기본값: False)
    647
    648        Returns:
    649            선택된 자식 객체 배열
    650        """
    651        children = self.get_every_children(inObj)
    652
    653        # 자신도 포함하는 경우
    654        if includeSelf:
    655            children.insert(0, inObj)
    656
    657        rt.select(children)

객체의 모든 자식들을 선택.

Args: inObj: 시작 객체 includeSelf: 자신도 포함할지 여부 (기본값: False)

Returns: 선택된 자식 객체 배열

def get_bone_end_position(self, inBone): View Source

    659    def get_bone_end_position(self, inBone):
    660        """
    661        뼈대 끝 위치 가져오기.
    662
    663        Args:
    664            inBone: 대상 뼈대 객체
    665
    666        Returns:
    667            뼈대 끝 위치 좌표
    668        """
    669        if rt.classOf(inBone) == rt.BoneGeometry:
    670            return rt.Point3(inBone.length, 0, 0) * inBone.objectTransform
    671        else:
    672            return inBone.transform.translation

뼈대 끝 위치 가져오기.

Args: inBone: 대상 뼈대 객체

Returns: 뼈대 끝 위치 좌표

def link_skin_bone(self, inSkinBone, inOriBone): View Source

    674    def link_skin_bone(self, inSkinBone, inOriBone):
    675        """
    676        스킨 뼈대를 원본 뼈대에 연결.
    677
    678        Args:
    679            inSkinBone: 연결할 스킨 뼈대
    680            inOriBone: 원본 뼈대
    681        """
    682        self.anim.save_xform(inSkinBone)
    683        self.anim.set_xform(inSkinBone, space="World")
    684
    685        self.anim.save_xform(inOriBone)
    686
    687        rt.setPropertyController(inSkinBone.controller, "Scale", rt.scaleXYZ())
    688
    689        linkConst = rt.link_constraint()
    690        linkConst.addTarget(inOriBone, 0)
    691
    692        inSkinBone.controller = linkConst
    693
    694        self.anim.set_xform(inSkinBone, space="World")

스킨 뼈대를 원본 뼈대에 연결.

Args: inSkinBone: 연결할 스킨 뼈대 inOriBone: 원본 뼈대

def link_skin_bones(self, inSkinBoneArray, inOriBoneArray): View Source

    696    def link_skin_bones(self, inSkinBoneArray, inOriBoneArray):
    697        """
    698        스킨 뼈대 배열을 원본 뼈대 배열에 연결.
    699
    700        Args:
    701            inSkinBoneArray: 연결할 스킨 뼈대 배열
    702            inOriBoneArray: 원본 뼈대 배열
    703
    704        Returns:
    705            True: 성공
    706            False: 실패
    707        """
    708        if len(inSkinBoneArray) != len(inOriBoneArray):
    709            print("Error: Skin bone array and original bone array must have the same length.")
    710            return False
    711
    712        skinBoneDict = {}
    713        oriBoneDict = {}
    714
    715        # 스킨 뼈대 딕셔너리 생성 (이름과 패턴화된 이름을 함께 저장)
    716        for item in inSkinBoneArray:
    717            # 아이템 저장
    718            skinBoneDict[item.name] = item
    719            # 언더스코어를 별표로 변환한 패턴 생성
    720            namePattern = self.name.remove_name_part("Base", item.name)
    721            namePattern = namePattern.replace("_", "*")
    722            skinBoneDict[item.name + "_Pattern"] = namePattern
    723
    724        # 원본 뼈대 딕셔너리 생성 (이름과 패턴화된 이름을 함께 저장)
    725        for item in inOriBoneArray:
    726            # 아이템 저장
    727            oriBoneDict[item.name] = item
    728            # 공백을 별표로 변환한 패턴 생성
    729            namePattern = self.name.remove_name_part("Base", item.name)
    730            namePattern = namePattern.replace(" ", "*")
    731            oriBoneDict[item.name + "_Pattern"] = namePattern
    732
    733        # 정렬된 배열 생성
    734        sortedSkinBoneArray = []
    735        sortedOriBoneArray = []
    736
    737        # 같은 패턴을 가진 뼈대들을 찾아 매칭
    738        for skinName, skinBone in [(k, v) for k, v in skinBoneDict.items() if not k.endswith("_Pattern")]:
    739            skinPattern = skinBoneDict[skinName + "_Pattern"]
    740
    741            for oriName, oriBone in [(k, v) for k, v in oriBoneDict.items() if not k.endswith("_Pattern")]:
    742                oriPattern = oriBoneDict[oriName + "_Pattern"]
    743
    744                if rt.matchPattern(skinName, pattern=oriPattern):
    745                    sortedSkinBoneArray.append(skinBone)
    746                    sortedOriBoneArray.append(oriBone)
    747                    break
    748        # 링크 연결 수행
    749        for i in range(len(sortedSkinBoneArray)):
    750            self.link_skin_bone(sortedSkinBoneArray[i], sortedOriBoneArray[i])
    751
    752        return True

스킨 뼈대 배열을 원본 뼈대 배열에 연결.

Args: inSkinBoneArray: 연결할 스킨 뼈대 배열 inOriBoneArray: 원본 뼈대 배열

Returns: True: 성공 False: 실패

def create_skin_bone( self, inBoneArray, skipNub=True, mesh=True, link=True, skinBoneBaseName=''): View Source

    754    def create_skin_bone(self, inBoneArray, skipNub=True, mesh=True, link=True, skinBoneBaseName=""):
    755        """
    756        스킨 뼈대 생성.
    757
    758        Args:
    759            inBoneArray: 원본 뼈대 배열
    760            skipNub: Nub 뼈대 건너뛰기 (기본값: True)
    761            mesh: 메시 스냅샷 사용 (기본값: True)
    762            link: 원본 뼈대에 연결 (기본값: True)
    763            skinBoneBaseName: 스킨 뼈대 기본 이름 (기본값: "b")
    764
    765        Returns:
    766            생성된 스킨 뼈대 배열
    767        """
    768        bones = []
    769        skinBoneFilteringChar = "_"
    770        skinBonePushAmount = -0.02
    771        returnBones = []
    772
    773        definedSkinBoneBaseName = self.name.get_name_part_value_by_description("Base", "SkinBone")
    774
    775        for i in range(len(inBoneArray)):
    776            skinBoneName = self.name.replace_name_part("Base", inBoneArray[i].name, definedSkinBoneBaseName)
    777            skinBoneName = self.name.replace_filtering_char(skinBoneName, skinBoneFilteringChar)
    778
    779            skinBone = self.create_nub_bone(f"{definedSkinBoneBaseName}_TempSkin", 2)
    780            skinBone.name = skinBoneName
    781            skinBone.wireColor = rt.Color(255, 88, 199)
    782            skinBone.transform = inBoneArray[i].transform
    783
    784            if mesh:
    785                snapShotObj = rt.snapshot(inBoneArray[i])
    786                rt.addModifier(snapShotObj, rt.Push())
    787                snapShotObj.modifiers[rt.Name("Push")].Push_Value = skinBonePushAmount
    788                rt.collapseStack(snapShotObj)
    789
    790                rt.addModifier(skinBone, rt.Edit_Poly())
    791                rt.execute("max modify mode")
    792                rt.modPanel.setCurrentObject(skinBone.modifiers[rt.Name("Edit_Poly")])
    793                skinBone.modifiers[rt.Name("Edit_Poly")].Attach(snapShotObj, editPolyNode=skinBone)
    794
    795            skinBone.boneEnable = True
    796            skinBone.renderable = False
    797            skinBone.boneScaleType = rt.Name("None")
    798
    799            bones.append(skinBone)
    800
    801        for i in range(len(inBoneArray)):
    802            oriParentObj = inBoneArray[i].parent
    803            if oriParentObj is not None:
    804                skinBoneParentObjName = self.name.replace_name_part("Base", oriParentObj.name, definedSkinBoneBaseName)
    805                skinBoneParentObjName = self.name.replace_filtering_char(skinBoneParentObjName, skinBoneFilteringChar)
    806                bones[i].parent = rt.getNodeByName(skinBoneParentObjName)
    807            else:
    808                bones[i].parent = None
    809
    810        for item in bones:
    811            item.showLinks = True
    812            item.showLinksOnly = True
    813
    814        for item in bones:
    815            item.name = self.name.replace_name_part("Base", item.name, skinBoneBaseName)
    816
    817        if link:
    818            self.link_skin_bones(bones, inBoneArray)
    819
    820        if skipNub:
    821            for item in bones:
    822                if not rt.matchPattern(item.name, pattern=("*" + self.name.get_name_part_value_by_description("Nub", "Nub"))):
    823                    returnBones.append(item)
    824                else:
    825                    rt.delete(item)
    826        else:
    827            returnBones = bones.copy()
    828
    829        bones.clear()
    830
    831        return returnBones

스킨 뼈대 생성.

Args: inBoneArray: 원본 뼈대 배열 skipNub: Nub 뼈대 건너뛰기 (기본값: True) mesh: 메시 스냅샷 사용 (기본값: True) link: 원본 뼈대에 연결 (기본값: True) skinBoneBaseName: 스킨 뼈대 기본 이름 (기본값: "b")

Returns: 생성된 스킨 뼈대 배열

def create_skin_bone_from_bip( self, inBoneArray, skipNub=True, mesh=False, link=True, skinBoneBaseName=''): View Source

    833    def create_skin_bone_from_bip(self, inBoneArray, skipNub=True, mesh=False, link=True, skinBoneBaseName=""):
    834        """
    835        바이페드 객체에서 스킨 뼈대 생성.
    836
    837        Args:
    838            inBoneArray: 바이페드 객체 배열
    839            skipNub: Nub 뼈대 건너뛰기 (기본값: True)
    840            mesh: 메시 스냅샷 사용 (기본값: False)
    841            link: 원본 뼈대에 연결 (기본값: True)
    842            skinBoneBaseName: 스킨 뼈대 기본 이름 (기본값: "")
    843
    844        Returns:
    845            생성된 스킨 뼈대 배열
    846        """
    847        # 바이페드 객체만 필터링, Twist 뼈대 제외, 루트 노드 제외
    848        targetBones = [item for item in inBoneArray
    849                      if (rt.classOf(item) == rt.Biped_Object)
    850                      and (not rt.matchPattern(item.name, pattern="*Twist*"))
    851                      and (item != item.controller.rootNode)]
    852
    853        returnSkinBones = self.create_skin_bone(targetBones, skipNub=skipNub, mesh=mesh, link=link, skinBoneBaseName=skinBoneBaseName)
    854
    855        return returnSkinBones

바이페드 객체에서 스킨 뼈대 생성.

Args: inBoneArray: 바이페드 객체 배열 skipNub: Nub 뼈대 건너뛰기 (기본값: True) mesh: 메시 스냅샷 사용 (기본값: False) link: 원본 뼈대에 연결 (기본값: True) skinBoneBaseName: 스킨 뼈대 기본 이름 (기본값: "")

Returns: 생성된 스킨 뼈대 배열

def create_skin_bone_from_bip_for_unreal( self, inBoneArray, skipNub=True, mesh=False, link=True, skinBoneBaseName=''): View Source

    857    def create_skin_bone_from_bip_for_unreal(self, inBoneArray, skipNub=True, mesh=False, link=True, skinBoneBaseName=""):
    858        """
    859        언리얼 엔진용 바이페드 객체에서 스킨 뼈대 생성.
    860
    861        Args:
    862            inBoneArray: 바이페드 객체 배열
    863            skipNub: Nub 뼈대 건너뛰기 (기본값: True)
    864            mesh: 메시 스냅샷 사용 (기본값: False)
    865            link: 원본 뼈대에 연결 (기본값: True)
    866            skinBoneBaseName: 스킨 뼈대 기본 이름 (기본값: "b")
    867
    868        Returns:
    869            생성된 스킨 뼈대 배열 또는 False (실패 시)
    870        """
    871        genBones = self.create_skin_bone_from_bip(inBoneArray, skipNub=skipNub, mesh=mesh, link=link, skinBoneBaseName=skinBoneBaseName)
    872        if len(genBones) == 0:
    873            return False
    874
    875        # 언리얼 엔진용으로 특정 뼈대 회전
    876        for item in genBones:
    877            if rt.matchPattern(item.name, pattern="*Pelvis*"):
    878                self.anim.rotate_local(item, 180, 0, 0)
    879            if rt.matchPattern(item.name, pattern="*Spine*"):
    880                self.anim.rotate_local(item, 180, 0, 0)
    881            if rt.matchPattern(item.name, pattern="*Neck*"):
    882                self.anim.rotate_local(item, 180, 0, 0)
    883            if rt.matchPattern(item.name, pattern="*Head*"):
    884                self.anim.rotate_local(item, 180, 0, 0)
    885
    886        return genBones

언리얼 엔진용 바이페드 객체에서 스킨 뼈대 생성.

Args: inBoneArray: 바이페드 객체 배열 skipNub: Nub 뼈대 건너뛰기 (기본값: True) mesh: 메시 스냅샷 사용 (기본값: False) link: 원본 뼈대에 연결 (기본값: True) skinBoneBaseName: 스킨 뼈대 기본 이름 (기본값: "b")

Returns: 생성된 스킨 뼈대 배열 또는 False (실패 시)

def gen_missing_bip_bones_for_ue5manny(self, inBoneArray): View Source

    888    def gen_missing_bip_bones_for_ue5manny(self, inBoneArray):
    889        returnBones = []
    890        spine3 = None
    891        neck = None
    892
    893        handL = None
    894        handR = None
    895
    896        fingerNames = ["index", "middle", "ring", "pinky"]
    897        knuckleName = "metacarpal"
    898        lKnuckleDistance = []
    899        rKnuckleDistance = []
    900
    901        lFingers = []
    902        rFingers = []
    903
    904        for item in inBoneArray:
    905            if rt.matchPattern(item.name, pattern="*spine 03"):
    906                spine3 = item
    907            if rt.matchPattern(item.name, pattern="*neck 01"):
    908                neck = item
    909            if rt.matchPattern(item.name, pattern="*hand*l"):
    910                handL = item
    911            if rt.matchPattern(item.name, pattern="*hand*r"):
    912                handR = item
    913
    914            for fingerName in fingerNames:
    915                if rt.matchPattern(item.name, pattern="*"+fingerName+"*01*l"):
    916                    lFingers.append(item)
    917                if rt.matchPattern(item.name, pattern="*"+fingerName+"*01*r"):
    918                    rFingers.append(item)
    919            for finger in lFingers:
    920                fingerDistance = rt.distance(finger, handL)
    921                lKnuckleDistance.append(fingerDistance)
    922            for finger in rFingers:
    923                fingerDistance = rt.distance(finger, handR)
    924                rKnuckleDistance.append(fingerDistance)
    925
    926        filteringChar = self.name._get_filtering_char(inBoneArray[-1].name)
    927        isLower = inBoneArray[-1].name[0].islower()
    928        spineName = self.name.get_name_part_value_by_description("Base", "Biped") + filteringChar + "Spine"
    929
    930        spine4 = self.create_nub_bone(spineName, 2)
    931        spine5 = self.create_nub_bone(spineName, 2)
    932
    933        spine4.name = self.name.replace_name_part("Index", spine4.name, "4")
    934        spine4.name = self.name.remove_name_part("Nub", spine4.name)
    935        spine5.name = self.name.replace_name_part("Index", spine5.name, "5")
    936        spine5.name = self.name.remove_name_part("Nub", spine5.name)
    937        if isLower:
    938            spine4.name = spine4.name.lower()
    939            spine5.name = spine5.name.lower()
    940
    941        spineDistance = rt.distance(spine3, neck)/3.0
    942        rt.setProperty(spine4, "transform", spine3.transform)
    943        rt.setProperty(spine5, "transform", spine3.transform)
    944        self.anim.move_local(spine4, spineDistance, 0, 0)
    945        self.anim.move_local(spine5, spineDistance * 2, 0, 0)
    946
    947        returnBones.append(spine4)
    948        returnBones.append(spine5)
    949
    950        for i, finger in enumerate(lFingers):
    951            knuckleBoneName = self.name.add_suffix_to_real_name(finger.name, filteringChar+knuckleName)
    952            knuckleBoneName = self.name.remove_name_part("Index", knuckleBoneName)
    953
    954            knuckleBone = self.create_nub_bone(knuckleBoneName, 2)
    955            knuckleBone.name = self.name.remove_name_part("Nub", knuckleBone.name)
    956            if isLower:
    957                knuckleBone.name = knuckleBone.name.lower()
    958
    959            knuckleBone.transform = finger.transform
    960            lookAtConst = self.const.assign_lookat(knuckleBone, handL)
    961            lookAtConst.upnode_world = False
    962            lookAtConst.pickUpNode = handL
    963            lookAtConst.lookat_vector_length = 0.0
    964            lookAtConst.target_axisFlip = True
    965            self.const.collapse(knuckleBone)
    966            self.anim.move_local(knuckleBone, -lKnuckleDistance[i]*0.8, 0, 0)
    967
    968            returnBones.append(knuckleBone)
    969
    970        for i, finger in enumerate(rFingers):
    971            knuckleBoneName = self.name.add_suffix_to_real_name(finger.name, filteringChar+knuckleName)
    972            knuckleBoneName = self.name.remove_name_part("Index", knuckleBoneName)
    973
    974            knuckleBone = self.create_nub_bone(knuckleBoneName, 2)
    975            knuckleBone.name = self.name.remove_name_part("Nub", knuckleBone.name)
    976            if isLower:
    977                knuckleBone.name = knuckleBone.name.lower()
    978
    979            knuckleBone.transform = finger.transform
    980            lookAtConst = self.const.assign_lookat(knuckleBone, handR)
    981            lookAtConst.upnode_world = False
    982            lookAtConst.pickUpNode = handR
    983            lookAtConst.lookat_vector_length = 0.0
    984            lookAtConst.target_axisFlip = True
    985            self.const.collapse(knuckleBone)
    986            self.anim.move_local(knuckleBone, -rKnuckleDistance[i]*0.8, 0, 0)
    987
    988            returnBones.append(knuckleBone)
    989
    990        return returnBones

def relink_missing_bip_bones_for_ue5manny(self, inBipArray, inMissingBoneArray): View Source

     992    def relink_missing_bip_bones_for_ue5manny(self, inBipArray, inMissingBoneArray):
     993        returnBones = []
     994
     995        spine3 = None
     996
     997        handL = None
     998        handR = None
     999
    1000        knuckleName = "metacarpal"
    1001
    1002        for item in inBipArray:
    1003            if rt.matchPattern(item.name, pattern="*spine 03"):
    1004                spine3 = item
    1005            if rt.matchPattern(item.name, pattern="*hand*l"):
    1006                handL = item
    1007            if rt.matchPattern(item.name, pattern="*hand*r"):
    1008                handR = item
    1009
    1010        for item in inMissingBoneArray:
    1011            if rt.matchPattern(item.name, pattern="*spine*"):
    1012                item.parent = spine3
    1013            if rt.matchPattern(item.name, pattern=f"*{knuckleName}*l"):
    1014                item.parent = handL
    1015            if rt.matchPattern(item.name, pattern=f"*{knuckleName}*r"):
    1016                item.parent = handR
    1017
    1018        returnBones.append(inBipArray)
    1019        returnBones.append(inMissingBoneArray)
    1020        return returnBones

def relink_missing_skin_bones_for_ue5manny(self, inSkinArray): View Source

    1022    def relink_missing_skin_bones_for_ue5manny(self, inSkinArray):
    1023        returnBones = []
    1024        spine3 = None
    1025        spine4 = None
    1026        spine5 = None
    1027
    1028        neck = None
    1029        clavicleL = None
    1030        clavicleR = None
    1031
    1032        handL = None
    1033        handR = None
    1034
    1035        fingerNames = ["index", "middle", "ring", "pinky"]
    1036        knuckleName = "metacarpal"
    1037
    1038        lFingers = []
    1039        rFingers = []
    1040
    1041        for item in inSkinArray:
    1042            if rt.matchPattern(item.name, pattern="*spine*03"):
    1043                spine3 = item
    1044            if rt.matchPattern(item.name, pattern="*neck*01"):
    1045                neck = item
    1046            if rt.matchPattern(item.name, pattern="*clavicle*l"):
    1047                clavicleL = item
    1048            if rt.matchPattern(item.name, pattern="*clavicle*r"):
    1049                clavicleR = item
    1050
    1051            if rt.matchPattern(item.name, pattern="*hand*l"):
    1052                handL = item
    1053            if rt.matchPattern(item.name, pattern="*hand*r"):
    1054                handR = item
    1055
    1056            for fingerName in fingerNames:
    1057                if rt.matchPattern(item.name, pattern="*"+fingerName+"*01*l"):
    1058                    lFingers.append(item)
    1059                if rt.matchPattern(item.name, pattern="*"+fingerName+"*01*r"):
    1060                    rFingers.append(item)
    1061
    1062        for item in inSkinArray:
    1063            if rt.matchPattern(item.name, pattern="*spine*04"):
    1064                spine4 = item
    1065                item.parent = spine3
    1066
    1067            if rt.matchPattern(item.name, pattern="*spine*05"):
    1068                spine5 = item
    1069                item.parent = spine4
    1070                neck.parent = spine5
    1071                clavicleL.parent = spine5
    1072                clavicleR.parent = spine5
    1073
    1074            if rt.matchPattern(item.name, pattern=f"*{knuckleName}*l"):
    1075                item.parent = handL
    1076            if rt.matchPattern(item.name, pattern=f"*{knuckleName}*r"):
    1077                item.parent = handR
    1078
    1079        filteringChar = self.name._get_filtering_char(inSkinArray[-1].name)
    1080
    1081        for item in lFingers:
    1082            fingerNamePattern = self.name.add_suffix_to_real_name(item.name, filteringChar+knuckleName)
    1083            fingerNamePattern = self.name.remove_name_part("Index", fingerNamePattern)
    1084            for knuckle in inSkinArray:
    1085                if rt.matchPattern(knuckle.name, pattern=fingerNamePattern):
    1086                    item.parent = knuckle
    1087                    break
    1088
    1089        for item in rFingers:
    1090            fingerNamePattern = self.name.add_suffix_to_real_name(item.name, filteringChar+knuckleName)
    1091            fingerNamePattern = self.name.remove_name_part("Index", fingerNamePattern)
    1092            for knuckle in inSkinArray:
    1093                if rt.matchPattern(knuckle.name, pattern=fingerNamePattern):
    1094                    item.parent = knuckle
    1095                    break
    1096
    1097        return returnBones

def create_skin_bone_from_bip_for_ue5manny( self, inBoneArray, skipNub=True, mesh=False, link=True, isHuman=False, skinBoneBaseName=''): View Source

    1099    def create_skin_bone_from_bip_for_ue5manny(self, inBoneArray, skipNub=True, mesh=False, link=True, isHuman=False, skinBoneBaseName=""):
    1100        targetBones = [item for item in inBoneArray
    1101                      if (rt.classOf(item) == rt.Biped_Object)
    1102                      and (not rt.matchPattern(item.name, pattern="*Twist*"))
    1103                      and (item != item.controller.rootNode)]
    1104
    1105        missingBipBones = []
    1106
    1107        if isHuman:
    1108            missingBipBones = self.gen_missing_bip_bones_for_ue5manny(targetBones)
    1109            self.relink_missing_bip_bones_for_ue5manny(targetBones, missingBipBones)
    1110
    1111        for item in missingBipBones:
    1112            targetBones.append(item)
    1113
    1114        sortedBipBones = self.sort_bones_as_hierarchy(targetBones)
    1115
    1116        skinBones = self.create_skin_bone(sortedBipBones, skipNub=skipNub, mesh=mesh, link=False, skinBoneBaseName=skinBoneBaseName)
    1117        if len(skinBones) == 0:
    1118            return False
    1119
    1120        for item in skinBones:
    1121            if rt.matchPattern(item.name, pattern="*pelvis*"):
    1122                self.anim.rotate_local(item, 180, 0, 0, dontAffectChildren=True)
    1123            if rt.matchPattern(item.name, pattern="*spine*"):
    1124                self.anim.rotate_local(item, 180, 0, 0, dontAffectChildren=True)
    1125            if rt.matchPattern(item.name, pattern="*neck*"):
    1126                self.anim.rotate_local(item, 180, 0, 0, dontAffectChildren=True)
    1127            if rt.matchPattern(item.name, pattern="*head*"):
    1128                self.anim.rotate_local(item, 180, 0, 0, dontAffectChildren=True)
    1129            if rt.matchPattern(item.name, pattern="*thigh*l"):
    1130                self.anim.rotate_local(item, 0, 0, 180, dontAffectChildren=True)
    1131            if rt.matchPattern(item.name, pattern="*calf*l"):
    1132                self.anim.rotate_local(item, 0, 0, 180, dontAffectChildren=True)
    1133            if rt.matchPattern(item.name, pattern="*foot*l"):
    1134                self.anim.rotate_local(item, 0, 0, 180, dontAffectChildren=True)
    1135            if rt.matchPattern(item.name, pattern="*ball*r"):
    1136                self.anim.rotate_local(item, 0, 0, 180, dontAffectChildren=True)
    1137
    1138            if rt.matchPattern(item.name, pattern="*clavicle*r"):
    1139                self.anim.rotate_local(item, 0, 0, -180, dontAffectChildren=True)
    1140            if rt.matchPattern(item.name, pattern="*upperarm*r"):
    1141                self.anim.rotate_local(item, 0, 0, -180, dontAffectChildren=True)
    1142            if rt.matchPattern(item.name, pattern="*lowerarm*r"):
    1143                self.anim.rotate_local(item, 0, 0, -180, dontAffectChildren=True)
    1144            if rt.matchPattern(item.name, pattern="*hand*r"):
    1145                self.anim.rotate_local(item, 0, 0, -180, dontAffectChildren=True)
    1146
    1147            if rt.matchPattern(item.name, pattern="*thumb*r"):
    1148                self.anim.rotate_local(item, 0, 0, 180, dontAffectChildren=True)
    1149            if rt.matchPattern(item.name, pattern="*index*r"):
    1150                self.anim.rotate_local(item, 0, 0, 180, dontAffectChildren=True)
    1151            if rt.matchPattern(item.name, pattern="*middle*r"):
    1152                self.anim.rotate_local(item, 0, 0, 180, dontAffectChildren=True)
    1153            if rt.matchPattern(item.name, pattern="*ring*r"):
    1154                self.anim.rotate_local(item, 0, 0, 180, dontAffectChildren=True)
    1155            if rt.matchPattern(item.name, pattern="*pinky*r"):
    1156                self.anim.rotate_local(item, 0, 0, 180, dontAffectChildren=True)
    1157
    1158            if rt.matchPattern(item.name, pattern="*metacarpal*"):
    1159                tempArray = self.name._split_to_array(item.name)
    1160                item.name = self.name._combine(tempArray, inFilChar="_")
    1161                item.name = self.name.remove_name_part("Base", item.name)
    1162
    1163            self.anim.save_xform(item)
    1164
    1165        self.relink_missing_skin_bones_for_ue5manny(skinBones)
    1166
    1167        self.link_skin_bones(skinBones, sortedBipBones)
    1168        for item in skinBones:
    1169            self.anim.save_xform(item)
    1170
    1171        return skinBones

def set_bone_on(self, inBone): View Source

    1173    def set_bone_on(self, inBone):
    1174        """
    1175        뼈대 활성화.
    1176
    1177        Args:
    1178            inBone: 활성화할 뼈대 객체
    1179        """
    1180        if rt.classOf(inBone) == rt.BoneGeometry:
    1181            inBone.boneEnable = True

뼈대 활성화.

Args: inBone: 활성화할 뼈대 객체

def set_bone_off(self, inBone): View Source

    1183    def set_bone_off(self, inBone):
    1184        """
    1185        뼈대 비활성화.
    1186
    1187        Args:
    1188            inBone: 비활성화할 뼈대 객체
    1189        """
    1190        if rt.classOf(inBone) == rt.BoneGeometry:
    1191            inBone.boneEnable = False

뼈대 비활성화.

Args: inBone: 비활성화할 뼈대 객체

def set_bone_on_selection(self): View Source

    1193    def set_bone_on_selection(self):
    1194        """
    1195        선택된 모든 뼈대 활성화.
    1196        """
    1197        selArray = list(rt.getCurrentSelection())
    1198        for item in selArray:
    1199            self.set_bone_on(item)

선택된 모든 뼈대 활성화.

def set_bone_off_selection(self): View Source

    1201    def set_bone_off_selection(self):
    1202        """
    1203        선택된 모든 뼈대 비활성화.
    1204        """
    1205        selArray = list(rt.getCurrentSelection())
    1206        for item in selArray:
    1207            self.set_bone_off(item)

선택된 모든 뼈대 비활성화.

def set_freeze_length_on(self, inBone): View Source

    1209    def set_freeze_length_on(self, inBone):
    1210        """
    1211        뼈대 길이 고정 활성화.
    1212
    1213        Args:
    1214            inBone: 길이를 고정할 뼈대 객체
    1215        """
    1216        if rt.classOf(inBone) == rt.BoneGeometry:
    1217            inBone.boneFreezeLength = True

뼈대 길이 고정 활성화.

Args: inBone: 길이를 고정할 뼈대 객체

def set_freeze_length_off(self, inBone): View Source

    1219    def set_freeze_length_off(self, inBone):
    1220        """
    1221        뼈대 길이 고정 비활성화.
    1222
    1223        Args:
    1224            inBone: 길이 고정을 해제할 뼈대 객체
    1225        """
    1226        if rt.classOf(inBone) == rt.BoneGeometry:
    1227            inBone.boneFreezeLength = False

뼈대 길이 고정 비활성화.

Args: inBone: 길이 고정을 해제할 뼈대 객체

def set_freeze_length_on_selection(self): View Source

    1229    def set_freeze_length_on_selection(self):
    1230        """
    1231        선택된 모든 뼈대의 길이 고정 활성화.
    1232        """
    1233        selArray = list(rt.getCurrentSelection())
    1234        for item in selArray:
    1235            self.set_freeze_length_on(item)

선택된 모든 뼈대의 길이 고정 활성화.

def set_freeze_length_off_selection(self): View Source

    1237    def set_freeze_length_off_selection(self):
    1238        """
    1239        선택된 모든 뼈대의 길이 고정 비활성화.
    1240        """
    1241        selArray = list(rt.getCurrentSelection())
    1242        for item in selArray:
    1243            self.set_freeze_length_off(item)

선택된 모든 뼈대의 길이 고정 비활성화.

class Mirror: View Source

     17class Mirror:
     18    """
     19    객체 미러링 관련 기능을 제공하는 클래스.
     20    MAXScript의 _Mirror 구조체 개념을 Python으로 재구현한 클래스이며,
     21    3ds Max의 기능들을 pymxs API를 통해 제어합니다.
     22    """
     23
     24    def __init__(self, nameService=None, boneService=None):
     25        """
     26        클래스 초기화
     27
     28        Args:
     29            nameService: Name 서비스 인스턴스 (제공되지 않으면 새로 생성)
     30            boneService: Bone 서비스 인스턴스 (제공되지 않으면 새로 생성)
     31        """
     32        self.name = nameService if nameService else Name()
     33        self.bone = boneService if boneService else Bone(nameService=self.name) # Pass the potentially newly created nameService
     34
     35    def mirror_matrix(self, mAxis="x", mFlip="x", tm=None, pivotTM=None):
     36        """
     37        미러링 행렬 생성
     38
     39        Args:
     40            mAxis: 미러링 축 (기본값: "x")
     41            mFlip: 뒤집는 축 (기본값: "x")
     42            tm: 변환 행렬 (기본값: 단위 행렬)
     43            pivotTM: 피벗 변환 행렬 (기본값: 단위 행렬)
     44
     45        Returns:
     46            미러링된 변환 행렬
     47        """
     48        def fetch_reflection(a):
     49            """
     50            반사 벡터 값 반환
     51
     52            Args:
     53                a: 축 식별자 ("x", "y", "z")
     54
     55            Returns:
     56                해당 축에 대한 반사 벡터
     57            """
     58            if a == "x":
     59                return [-1, 1, 1]  # YZ 평면에 대한 반사
     60            elif a == "y":
     61                return [1, -1, 1]  # ZX 평면에 대한 반사
     62            elif a == "z":
     63                return [1, 1, -1]  # XY 평면에 대한 반사
     64            else:
     65                return [1, 1, 1]  # 반사 없음
     66
     67        # 기본값 설정
     68        if tm is None:
     69            tm = rt.matrix3(1)
     70        if pivotTM is None:
     71            pivotTM = rt.matrix3(1)
     72
     73        # 반사 행렬 생성
     74        a_reflection = rt.scalematrix(rt.Point3(*fetch_reflection(mAxis)))
     75        f_reflection = rt.scalematrix(rt.Point3(*fetch_reflection(mFlip)))
     76
     77        # 미러링된 변환 행렬 계산: fReflection * tm * aReflection * pivotTm
     78        return f_reflection * tm * a_reflection * pivotTM
     79
     80    def apply_mirror(self, inObj, axis=1, flip=2, pivotObj=None, cloneStatus=2, negative=False):
     81        """
     82        객체에 미러링 적용
     83
     84        Args:
     85            inObj: 미러링할 객체
     86            axis: 미러링 축 인덱스 (1=x, 2=y, 3=z, 기본값: 1)
     87            flip: 뒤집기 축 인덱스 (1=x, 2=y, 3=z, 4=none, 기본값: 2)
     88            pivotObj: 피벗 객체 (기본값: None)
     89            cloneStatus: 복제 상태 (1=원본 변경, 2=복제본 생성, 3=스냅샷, 기본값: 2)
     90            negative: 음수 좌표계 사용 여부 (기본값: False)
     91
     92        Returns:
     93            미러링된 객체 (복제본 또는 원본)
     94        """
     95        axisArray = ["x", "y", "z", "none"]
     96        copyObj = rt.copy(inObj)
     97        objTM = inObj.transform
     98        pivotTM = rt.matrix3(1)
     99        mirrorIndexAxis = axis
    100        flipAxisIndex = flip
    101        copyObjName = self.name.gen_mirroring_name(inObj.name)
    102
    103        # 피벗 객체가 지정된 경우 피벗 변환 행렬 사용
    104        if pivotObj is not None:
    105            pivotTM = pivotObj.transform
    106
    107        # negative가 True인 경우 뒤집기 없음으로 설정
    108        if negative:
    109            flipAxisIndex = 4
    110
    111        # 복제본 초기 설정
    112        copyObj.name = copyObjName
    113        copyObj.parent = None
    114        copyObj.wirecolor = inObj.wirecolor
    115
    116        # 복제 상태에 따른 처리
    117        if cloneStatus == 1:  # 원본 변경
    118            rt.delete(copyObj)
    119            copyObj = None
    120            inObj.transform = self.mirror_matrix(
    121                mAxis=axisArray[mirrorIndexAxis-1],
    122                mFlip=axisArray[flipAxisIndex-1],
    123                tm=objTM,
    124                pivotTM=pivotTM
    125            )
    126            copyObj = inObj
    127        elif cloneStatus == 2:  # 복제본 생성
    128            copyObj.transform = self.mirror_matrix(
    129                mAxis=axisArray[mirrorIndexAxis-1],
    130                mFlip=axisArray[flipAxisIndex-1],
    131                tm=objTM,
    132                pivotTM=pivotTM
    133            )
    134        elif cloneStatus == 3:  # 스냅샷 생성
    135            rt.delete(copyObj)
    136            copyObj = None
    137            copyObj = rt.snapShot(inObj)
    138            copyObj.transform = self.mirror_matrix(
    139                mAxis=axisArray[mirrorIndexAxis-1],
    140                mFlip=axisArray[flipAxisIndex-1],
    141                tm=objTM,
    142                pivotTM=pivotTM
    143            )
    144
    145        return copyObj
    146
    147    def mirror_object(self, inObjArray, mAxis=1, pivotObj=None, cloneStatus=2):
    148        """
    149        객체 배열을 음수 좌표계를 사용하여 미러링
    150
    151        Args:
    152            inObjArray: 미러링할 객체 배열
    153            mAxis: 미러링 축 (기본값: 1)
    154            pivotObj: 피벗 객체 (기본값: None)
    155            cloneStatus: 복제 상태 (기본값: 2)
    156
    157        Returns:
    158            미러링된 객체 배열
    159        """
    160        returnArray = []
    161
    162        for item in inObjArray:
    163            mirroredObj = self.apply_mirror(
    164                item,
    165                axis=mAxis,
    166                pivotObj=pivotObj,
    167                cloneStatus=cloneStatus,
    168                negative=True
    169            )
    170            returnArray.append(mirroredObj)
    171
    172        return returnArray
    173
    174    def mirror_without_negative(self, inMirrorObjArray, mAxis=1, pivotObj=None, cloneStatus=2):
    175        """
    176        객체 배열을 양수 좌표계를 사용하여 미러링
    177
    178        Args:
    179            inMirrorObjArray: 미러링할 객체 배열
    180            mAxis: 미러링 축 인덱스 (1-6, 기본값: 1)
    181            pivotObj: 피벗 객체 (기본값: None)
    182            cloneStatus: 복제 상태 (기본값: 2)
    183
    184        Returns:
    185            미러링된 객체 배열
    186        """
    187        # 미러링 축과 뒤집기 축 매핑
    188        # 1=XY, 2=XZ, 3=YX, 4=YZ, 5=ZX, 6=ZY
    189        axisIndex = 1
    190        flipIndex = 1
    191
    192        # 미러링 축 인덱스에 따른 매핑
    193        if mAxis == 1:
    194            axisIndex = 1  # x
    195            flipIndex = 2  # y
    196        elif mAxis == 2:
    197            axisIndex = 1  # x
    198            flipIndex = 3  # z
    199        elif mAxis == 3:
    200            axisIndex = 2  # y
    201            flipIndex = 1  # x
    202        elif mAxis == 4:
    203            axisIndex = 2  # y
    204            flipIndex = 3  # z
    205        elif mAxis == 5:
    206            axisIndex = 3  # z
    207            flipIndex = 1  # x
    208        elif mAxis == 6:
    209            axisIndex = 3  # z
    210            flipIndex = 2  # y
    211        else:
    212            axisIndex = 1  # x
    213            flipIndex = 1  # x
    214
    215        # 미러링 적용
    216        returnArray = []
    217        for item in inMirrorObjArray:
    218            mirroredObj = self.apply_mirror(
    219                item,
    220                axis=axisIndex,
    221                flip=flipIndex,
    222                pivotObj=pivotObj,
    223                cloneStatus=cloneStatus,
    224                negative=False
    225            )
    226            returnArray.append(mirroredObj)
    227
    228        return returnArray
    229
    230    def mirror_bone(self, inBoneArray, mAxis=1, flipZ=False, offset=0.0):
    231        """
    232        뼈대 객체를 미러링
    233
    234        Args:
    235            inBoneArray: 미러링할 뼈대 배열
    236            mAxis: 미러링 축 (1=x, 2=y, 3=z, 기본값: 1)
    237            flipZ: Z축 뒤집기 여부 (기본값: False)
    238            offset: 미러링 오프셋 (기본값: 0.0)
    239
    240        Returns:
    241            미러링된 뼈대 배열
    242        """
    243        # 계층 구조에 따라 뼈대 정렬
    244        bones = self.bone.sort_bones_as_hierarchy(inBoneArray)
    245
    246        # 미러링 축 팩터 설정
    247        axisFactor = [1, 1, 1]
    248        if mAxis == 1:
    249            axisFactor = [-1, 1, 1]  # x축 미러링
    250        elif mAxis == 2:
    251            axisFactor = [1, -1, 1]  # y축 미러링
    252        elif mAxis == 3:
    253            axisFactor = [1, 1, -1]  # z축 미러링
    254
    255        # 새 뼈대와 부모 정보 저장 배열 준비
    256        parents = []
    257        created = []
    258
    259        # 시작점 위치 (미러링 중심) 설정
    260        root = bones[0].transform.translation
    261
    262        # 정렬된 뼈대 순서대로 처리
    263        for i in range(len(bones)):
    264            original = bones[i]
    265            if rt.classOf(original) != rt.BoneGeometry:  # 실제 뼈대가 아닌 경우
    266                parents.append(None)  # 부모 없음 표시
    267                continue
    268
    269            # 원본 뼈대의 시작점, 끝점, Z축 방향 가져오기
    270            boneStart = original.pos
    271            boneEnd = self.bone.get_bone_end_position(original)
    272            boneZ = original.dir
    273
    274            # 미러링 적용
    275            for k in range(3):  # x, y, z 좌표
    276                if axisFactor[k] < 0:
    277                    boneStart[k] = 2.0 * root[k] - boneStart[k] + offset
    278                    boneEnd[k] = 2.0 * root[k] - boneEnd[k] + offset
    279                    boneZ[k] = -boneZ[k]
    280
    281            # Z축 뒤집기 옵션 적용
    282            if flipZ:
    283                boneZ = -boneZ
    284
    285            # 새 뼈대 생성
    286            reflection = rt.bonesys.createbone(boneStart, boneEnd, boneZ)
    287
    288            # 원본 뼈대의 속성을 복사
    289            reflection.backfin = original.backfin
    290            reflection.backfinendtaper = original.backfinendtaper
    291            reflection.backfinsize = original.backfinsize
    292            reflection.backfinstarttaper = original.backfinstarttaper
    293            reflection.frontfin = original.frontfin
    294            reflection.frontfinendtaper = original.frontfinendtaper
    295            reflection.frontfinsize = original.frontfinsize
    296            reflection.frontfinstarttaper = original.frontfinstarttaper
    297            reflection.height = original.height
    298
    299            # 이름 생성 (좌우/앞뒤 방향이 있는 경우 미러링된 이름 생성)
    300            if self.name.has_Side(original.name) or self.name.has_FrontBack(original.name):
    301                reflection.name = self.name.gen_mirroring_name(original.name, axis=mAxis)
    302            else:
    303                reflection.name = self.name.add_suffix_to_real_name(original.name, "Mirrored")
    304
    305            reflection.sidefins = original.sidefins
    306            reflection.sidefinsendtaper = original.sidefinsendtaper
    307            reflection.sidefinssize = original.sidefinssize
    308            reflection.sidefinsstarttaper = original.sidefinsstarttaper
    309            reflection.taper = original.taper
    310            reflection.width = original.width
    311            reflection.wirecolor = original.wirecolor
    312
    313            created.append(reflection)
    314            parents.append(reflection)
    315
    316        # 계층 구조 연결 (자식부터 상위로)
    317        for i in range(len(created)-1, 0, -1):
    318            pIndex = bones.index(bones[i].parent) if bones[i].parent in bones else 0
    319            if pIndex != 0:
    320                created[i].parent = parents[pIndex]
    321
    322        # 루트 뼈대의 부모 설정
    323        created[0].parent = bones[0].parent
    324
    325        # 부모가 없는 뼈대는 위치 조정
    326        for i in range(len(created)):
    327            if created[i].parent is None:
    328                created[i].position = rt.Point3(
    329                    bones[i].position.x * axisFactor[0],
    330                    bones[i].position.y * axisFactor[1],
    331                    bones[i].position.z * axisFactor[2]
    332                )
    333
    334        return created
    335
    336    def mirror_geo(self, inMirrorObjArray, mAxis=1, pivotObj=None, cloneStatus=2):
    337        """
    338        지오메트리 객체 미러링 (폴리곤 노멀 방향 조정 포함)
    339
    340        Args:
    341            inMirrorObjArray: 미러링할 객체 배열
    342            mAxis: 미러링 축 (기본값: 1)
    343            pivotObj: 피벗 객체 (기본값: None)
    344            cloneStatus: 복제 상태 (기본값: 2)
    345
    346        Returns:
    347            미러링된 객체 배열
    348        """
    349        # 객체 미러링
    350        mirroredArray = self.mirror_object(
    351            inMirrorObjArray,
    352            mAxis=mAxis,
    353            pivotObj=pivotObj,
    354            cloneStatus=cloneStatus
    355        )
    356
    357        # 리셋 대상, 비리셋 대상 분류
    358        resetXformArray = []
    359        nonResetXformArray = []
    360        returnArray = []
    361
    362        # 객체 타입에 따라 분류
    363        for item in mirroredArray:
    364            caseIndex = 0
    365            if rt.classOf(item) == rt.Editable_Poly:
    366                caseIndex += 1
    367            if rt.classOf(item) == rt.Editable_mesh:
    368                caseIndex += 1
    369            if item.modifiers.count > 0:
    370                caseIndex += 1
    371
    372            if caseIndex == 1:  # 폴리곤, 메시 또는 모디파이어가 있는 경우
    373                resetXformArray.append(item)
    374            else:
    375                nonResetXformArray.append(item)
    376
    377        # 리셋 대상 객체에 XForm 리셋 및 노멀 방향 뒤집기 적용
    378        for item in resetXformArray:
    379            rt.ResetXForm(item)
    380            tempNormalMod = rt.normalModifier()
    381            tempNormalMod.flip = True
    382            rt.addModifier(item, tempNormalMod)
    383            rt.collapseStack(item)
    384
    385        # 처리된 객체들 합치기
    386        returnArray.extend(resetXformArray)
    387        returnArray.extend(nonResetXformArray)
    388
    389        return returnArray

객체 미러링 관련 기능을 제공하는 클래스. MAXScript의 _Mirror 구조체 개념을 Python으로 재구현한 클래스이며, 3ds Max의 기능들을 pymxs API를 통해 제어합니다.

Mirror(nameService=None, boneService=None) View Source

    24    def __init__(self, nameService=None, boneService=None):
    25        """
    26        클래스 초기화
    27
    28        Args:
    29            nameService: Name 서비스 인스턴스 (제공되지 않으면 새로 생성)
    30            boneService: Bone 서비스 인스턴스 (제공되지 않으면 새로 생성)
    31        """
    32        self.name = nameService if nameService else Name()
    33        self.bone = boneService if boneService else Bone(nameService=self.name) # Pass the potentially newly created nameService

클래스 초기화

Args: nameService: Name 서비스 인스턴스 (제공되지 않으면 새로 생성) boneService: Bone 서비스 인스턴스 (제공되지 않으면 새로 생성)

name
bone
def mirror_matrix(self, mAxis='x', mFlip='x', tm=None, pivotTM=None): View Source

    35    def mirror_matrix(self, mAxis="x", mFlip="x", tm=None, pivotTM=None):
    36        """
    37        미러링 행렬 생성
    38
    39        Args:
    40            mAxis: 미러링 축 (기본값: "x")
    41            mFlip: 뒤집는 축 (기본값: "x")
    42            tm: 변환 행렬 (기본값: 단위 행렬)
    43            pivotTM: 피벗 변환 행렬 (기본값: 단위 행렬)
    44
    45        Returns:
    46            미러링된 변환 행렬
    47        """
    48        def fetch_reflection(a):
    49            """
    50            반사 벡터 값 반환
    51
    52            Args:
    53                a: 축 식별자 ("x", "y", "z")
    54
    55            Returns:
    56                해당 축에 대한 반사 벡터
    57            """
    58            if a == "x":
    59                return [-1, 1, 1]  # YZ 평면에 대한 반사
    60            elif a == "y":
    61                return [1, -1, 1]  # ZX 평면에 대한 반사
    62            elif a == "z":
    63                return [1, 1, -1]  # XY 평면에 대한 반사
    64            else:
    65                return [1, 1, 1]  # 반사 없음
    66
    67        # 기본값 설정
    68        if tm is None:
    69            tm = rt.matrix3(1)
    70        if pivotTM is None:
    71            pivotTM = rt.matrix3(1)
    72
    73        # 반사 행렬 생성
    74        a_reflection = rt.scalematrix(rt.Point3(*fetch_reflection(mAxis)))
    75        f_reflection = rt.scalematrix(rt.Point3(*fetch_reflection(mFlip)))
    76
    77        # 미러링된 변환 행렬 계산: fReflection * tm * aReflection * pivotTm
    78        return f_reflection * tm * a_reflection * pivotTM

미러링 행렬 생성

Args: mAxis: 미러링 축 (기본값: "x") mFlip: 뒤집는 축 (기본값: "x") tm: 변환 행렬 (기본값: 단위 행렬) pivotTM: 피벗 변환 행렬 (기본값: 단위 행렬)

Returns: 미러링된 변환 행렬

def apply_mirror( self, inObj, axis=1, flip=2, pivotObj=None, cloneStatus=2, negative=False): View Source

     80    def apply_mirror(self, inObj, axis=1, flip=2, pivotObj=None, cloneStatus=2, negative=False):
     81        """
     82        객체에 미러링 적용
     83
     84        Args:
     85            inObj: 미러링할 객체
     86            axis: 미러링 축 인덱스 (1=x, 2=y, 3=z, 기본값: 1)
     87            flip: 뒤집기 축 인덱스 (1=x, 2=y, 3=z, 4=none, 기본값: 2)
     88            pivotObj: 피벗 객체 (기본값: None)
     89            cloneStatus: 복제 상태 (1=원본 변경, 2=복제본 생성, 3=스냅샷, 기본값: 2)
     90            negative: 음수 좌표계 사용 여부 (기본값: False)
     91
     92        Returns:
     93            미러링된 객체 (복제본 또는 원본)
     94        """
     95        axisArray = ["x", "y", "z", "none"]
     96        copyObj = rt.copy(inObj)
     97        objTM = inObj.transform
     98        pivotTM = rt.matrix3(1)
     99        mirrorIndexAxis = axis
    100        flipAxisIndex = flip
    101        copyObjName = self.name.gen_mirroring_name(inObj.name)
    102
    103        # 피벗 객체가 지정된 경우 피벗 변환 행렬 사용
    104        if pivotObj is not None:
    105            pivotTM = pivotObj.transform
    106
    107        # negative가 True인 경우 뒤집기 없음으로 설정
    108        if negative:
    109            flipAxisIndex = 4
    110
    111        # 복제본 초기 설정
    112        copyObj.name = copyObjName
    113        copyObj.parent = None
    114        copyObj.wirecolor = inObj.wirecolor
    115
    116        # 복제 상태에 따른 처리
    117        if cloneStatus == 1:  # 원본 변경
    118            rt.delete(copyObj)
    119            copyObj = None
    120            inObj.transform = self.mirror_matrix(
    121                mAxis=axisArray[mirrorIndexAxis-1],
    122                mFlip=axisArray[flipAxisIndex-1],
    123                tm=objTM,
    124                pivotTM=pivotTM
    125            )
    126            copyObj = inObj
    127        elif cloneStatus == 2:  # 복제본 생성
    128            copyObj.transform = self.mirror_matrix(
    129                mAxis=axisArray[mirrorIndexAxis-1],
    130                mFlip=axisArray[flipAxisIndex-1],
    131                tm=objTM,
    132                pivotTM=pivotTM
    133            )
    134        elif cloneStatus == 3:  # 스냅샷 생성
    135            rt.delete(copyObj)
    136            copyObj = None
    137            copyObj = rt.snapShot(inObj)
    138            copyObj.transform = self.mirror_matrix(
    139                mAxis=axisArray[mirrorIndexAxis-1],
    140                mFlip=axisArray[flipAxisIndex-1],
    141                tm=objTM,
    142                pivotTM=pivotTM
    143            )
    144
    145        return copyObj

객체에 미러링 적용

Args: inObj: 미러링할 객체 axis: 미러링 축 인덱스 (1=x, 2=y, 3=z, 기본값: 1) flip: 뒤집기 축 인덱스 (1=x, 2=y, 3=z, 4=none, 기본값: 2) pivotObj: 피벗 객체 (기본값: None) cloneStatus: 복제 상태 (1=원본 변경, 2=복제본 생성, 3=스냅샷, 기본값: 2) negative: 음수 좌표계 사용 여부 (기본값: False)

Returns: 미러링된 객체 (복제본 또는 원본)

def mirror_object(self, inObjArray, mAxis=1, pivotObj=None, cloneStatus=2): View Source

    147    def mirror_object(self, inObjArray, mAxis=1, pivotObj=None, cloneStatus=2):
    148        """
    149        객체 배열을 음수 좌표계를 사용하여 미러링
    150
    151        Args:
    152            inObjArray: 미러링할 객체 배열
    153            mAxis: 미러링 축 (기본값: 1)
    154            pivotObj: 피벗 객체 (기본값: None)
    155            cloneStatus: 복제 상태 (기본값: 2)
    156
    157        Returns:
    158            미러링된 객체 배열
    159        """
    160        returnArray = []
    161
    162        for item in inObjArray:
    163            mirroredObj = self.apply_mirror(
    164                item,
    165                axis=mAxis,
    166                pivotObj=pivotObj,
    167                cloneStatus=cloneStatus,
    168                negative=True
    169            )
    170            returnArray.append(mirroredObj)
    171
    172        return returnArray

객체 배열을 음수 좌표계를 사용하여 미러링

Args: inObjArray: 미러링할 객체 배열 mAxis: 미러링 축 (기본값: 1) pivotObj: 피벗 객체 (기본값: None) cloneStatus: 복제 상태 (기본값: 2)

Returns: 미러링된 객체 배열

def mirror_without_negative(self, inMirrorObjArray, mAxis=1, pivotObj=None, cloneStatus=2): View Source

    174    def mirror_without_negative(self, inMirrorObjArray, mAxis=1, pivotObj=None, cloneStatus=2):
    175        """
    176        객체 배열을 양수 좌표계를 사용하여 미러링
    177
    178        Args:
    179            inMirrorObjArray: 미러링할 객체 배열
    180            mAxis: 미러링 축 인덱스 (1-6, 기본값: 1)
    181            pivotObj: 피벗 객체 (기본값: None)
    182            cloneStatus: 복제 상태 (기본값: 2)
    183
    184        Returns:
    185            미러링된 객체 배열
    186        """
    187        # 미러링 축과 뒤집기 축 매핑
    188        # 1=XY, 2=XZ, 3=YX, 4=YZ, 5=ZX, 6=ZY
    189        axisIndex = 1
    190        flipIndex = 1
    191
    192        # 미러링 축 인덱스에 따른 매핑
    193        if mAxis == 1:
    194            axisIndex = 1  # x
    195            flipIndex = 2  # y
    196        elif mAxis == 2:
    197            axisIndex = 1  # x
    198            flipIndex = 3  # z
    199        elif mAxis == 3:
    200            axisIndex = 2  # y
    201            flipIndex = 1  # x
    202        elif mAxis == 4:
    203            axisIndex = 2  # y
    204            flipIndex = 3  # z
    205        elif mAxis == 5:
    206            axisIndex = 3  # z
    207            flipIndex = 1  # x
    208        elif mAxis == 6:
    209            axisIndex = 3  # z
    210            flipIndex = 2  # y
    211        else:
    212            axisIndex = 1  # x
    213            flipIndex = 1  # x
    214
    215        # 미러링 적용
    216        returnArray = []
    217        for item in inMirrorObjArray:
    218            mirroredObj = self.apply_mirror(
    219                item,
    220                axis=axisIndex,
    221                flip=flipIndex,
    222                pivotObj=pivotObj,
    223                cloneStatus=cloneStatus,
    224                negative=False
    225            )
    226            returnArray.append(mirroredObj)
    227
    228        return returnArray

객체 배열을 양수 좌표계를 사용하여 미러링

Args: inMirrorObjArray: 미러링할 객체 배열 mAxis: 미러링 축 인덱스 (1-6, 기본값: 1) pivotObj: 피벗 객체 (기본값: None) cloneStatus: 복제 상태 (기본값: 2)

Returns: 미러링된 객체 배열

def mirror_bone(self, inBoneArray, mAxis=1, flipZ=False, offset=0.0): View Source

    230    def mirror_bone(self, inBoneArray, mAxis=1, flipZ=False, offset=0.0):
    231        """
    232        뼈대 객체를 미러링
    233
    234        Args:
    235            inBoneArray: 미러링할 뼈대 배열
    236            mAxis: 미러링 축 (1=x, 2=y, 3=z, 기본값: 1)
    237            flipZ: Z축 뒤집기 여부 (기본값: False)
    238            offset: 미러링 오프셋 (기본값: 0.0)
    239
    240        Returns:
    241            미러링된 뼈대 배열
    242        """
    243        # 계층 구조에 따라 뼈대 정렬
    244        bones = self.bone.sort_bones_as_hierarchy(inBoneArray)
    245
    246        # 미러링 축 팩터 설정
    247        axisFactor = [1, 1, 1]
    248        if mAxis == 1:
    249            axisFactor = [-1, 1, 1]  # x축 미러링
    250        elif mAxis == 2:
    251            axisFactor = [1, -1, 1]  # y축 미러링
    252        elif mAxis == 3:
    253            axisFactor = [1, 1, -1]  # z축 미러링
    254
    255        # 새 뼈대와 부모 정보 저장 배열 준비
    256        parents = []
    257        created = []
    258
    259        # 시작점 위치 (미러링 중심) 설정
    260        root = bones[0].transform.translation
    261
    262        # 정렬된 뼈대 순서대로 처리
    263        for i in range(len(bones)):
    264            original = bones[i]
    265            if rt.classOf(original) != rt.BoneGeometry:  # 실제 뼈대가 아닌 경우
    266                parents.append(None)  # 부모 없음 표시
    267                continue
    268
    269            # 원본 뼈대의 시작점, 끝점, Z축 방향 가져오기
    270            boneStart = original.pos
    271            boneEnd = self.bone.get_bone_end_position(original)
    272            boneZ = original.dir
    273
    274            # 미러링 적용
    275            for k in range(3):  # x, y, z 좌표
    276                if axisFactor[k] < 0:
    277                    boneStart[k] = 2.0 * root[k] - boneStart[k] + offset
    278                    boneEnd[k] = 2.0 * root[k] - boneEnd[k] + offset
    279                    boneZ[k] = -boneZ[k]
    280
    281            # Z축 뒤집기 옵션 적용
    282            if flipZ:
    283                boneZ = -boneZ
    284
    285            # 새 뼈대 생성
    286            reflection = rt.bonesys.createbone(boneStart, boneEnd, boneZ)
    287
    288            # 원본 뼈대의 속성을 복사
    289            reflection.backfin = original.backfin
    290            reflection.backfinendtaper = original.backfinendtaper
    291            reflection.backfinsize = original.backfinsize
    292            reflection.backfinstarttaper = original.backfinstarttaper
    293            reflection.frontfin = original.frontfin
    294            reflection.frontfinendtaper = original.frontfinendtaper
    295            reflection.frontfinsize = original.frontfinsize
    296            reflection.frontfinstarttaper = original.frontfinstarttaper
    297            reflection.height = original.height
    298
    299            # 이름 생성 (좌우/앞뒤 방향이 있는 경우 미러링된 이름 생성)
    300            if self.name.has_Side(original.name) or self.name.has_FrontBack(original.name):
    301                reflection.name = self.name.gen_mirroring_name(original.name, axis=mAxis)
    302            else:
    303                reflection.name = self.name.add_suffix_to_real_name(original.name, "Mirrored")
    304
    305            reflection.sidefins = original.sidefins
    306            reflection.sidefinsendtaper = original.sidefinsendtaper
    307            reflection.sidefinssize = original.sidefinssize
    308            reflection.sidefinsstarttaper = original.sidefinsstarttaper
    309            reflection.taper = original.taper
    310            reflection.width = original.width
    311            reflection.wirecolor = original.wirecolor
    312
    313            created.append(reflection)
    314            parents.append(reflection)
    315
    316        # 계층 구조 연결 (자식부터 상위로)
    317        for i in range(len(created)-1, 0, -1):
    318            pIndex = bones.index(bones[i].parent) if bones[i].parent in bones else 0
    319            if pIndex != 0:
    320                created[i].parent = parents[pIndex]
    321
    322        # 루트 뼈대의 부모 설정
    323        created[0].parent = bones[0].parent
    324
    325        # 부모가 없는 뼈대는 위치 조정
    326        for i in range(len(created)):
    327            if created[i].parent is None:
    328                created[i].position = rt.Point3(
    329                    bones[i].position.x * axisFactor[0],
    330                    bones[i].position.y * axisFactor[1],
    331                    bones[i].position.z * axisFactor[2]
    332                )
    333
    334        return created

뼈대 객체를 미러링

Args: inBoneArray: 미러링할 뼈대 배열 mAxis: 미러링 축 (1=x, 2=y, 3=z, 기본값: 1) flipZ: Z축 뒤집기 여부 (기본값: False) offset: 미러링 오프셋 (기본값: 0.0)

Returns: 미러링된 뼈대 배열

def mirror_geo(self, inMirrorObjArray, mAxis=1, pivotObj=None, cloneStatus=2): View Source

    336    def mirror_geo(self, inMirrorObjArray, mAxis=1, pivotObj=None, cloneStatus=2):
    337        """
    338        지오메트리 객체 미러링 (폴리곤 노멀 방향 조정 포함)
    339
    340        Args:
    341            inMirrorObjArray: 미러링할 객체 배열
    342            mAxis: 미러링 축 (기본값: 1)
    343            pivotObj: 피벗 객체 (기본값: None)
    344            cloneStatus: 복제 상태 (기본값: 2)
    345
    346        Returns:
    347            미러링된 객체 배열
    348        """
    349        # 객체 미러링
    350        mirroredArray = self.mirror_object(
    351            inMirrorObjArray,
    352            mAxis=mAxis,
    353            pivotObj=pivotObj,
    354            cloneStatus=cloneStatus
    355        )
    356
    357        # 리셋 대상, 비리셋 대상 분류
    358        resetXformArray = []
    359        nonResetXformArray = []
    360        returnArray = []
    361
    362        # 객체 타입에 따라 분류
    363        for item in mirroredArray:
    364            caseIndex = 0
    365            if rt.classOf(item) == rt.Editable_Poly:
    366                caseIndex += 1
    367            if rt.classOf(item) == rt.Editable_mesh:
    368                caseIndex += 1
    369            if item.modifiers.count > 0:
    370                caseIndex += 1
    371
    372            if caseIndex == 1:  # 폴리곤, 메시 또는 모디파이어가 있는 경우
    373                resetXformArray.append(item)
    374            else:
    375                nonResetXformArray.append(item)
    376
    377        # 리셋 대상 객체에 XForm 리셋 및 노멀 방향 뒤집기 적용
    378        for item in resetXformArray:
    379            rt.ResetXForm(item)
    380            tempNormalMod = rt.normalModifier()
    381            tempNormalMod.flip = True
    382            rt.addModifier(item, tempNormalMod)
    383            rt.collapseStack(item)
    384
    385        # 처리된 객체들 합치기
    386        returnArray.extend(resetXformArray)
    387        returnArray.extend(nonResetXformArray)
    388
    389        return returnArray

지오메트리 객체 미러링 (폴리곤 노멀 방향 조정 포함)

Args: inMirrorObjArray: 미러링할 객체 배열 mAxis: 미러링 축 (기본값: 1) pivotObj: 피벗 객체 (기본값: None) cloneStatus: 복제 상태 (기본값: 2)

Returns: 미러링된 객체 배열

class Layer: View Source

     13class Layer:
     14    """
     15    레이어 관련 기능을 위한 클래스
     16    MAXScript의 _Layer 구조체를 Python 클래스로 변환
     17
     18    pymxs 모듈을 통해 3ds Max의 레이어 관리 기능을 제어합니다.
     19    """
     20
     21    def __init__(self):
     22        """
     23        초기화 함수
     24        """
     25        pass
     26
     27    def reset_layer(self):
     28        """
     29        모든 레이어를 초기화하고 기본 레이어로 객체 이동
     30
     31        Returns:
     32            None
     33        """
     34        # 기본 레이어(0번 레이어) 가져오기
     35        defaultLayer = rt.layerManager.getLayer(0)
     36        layerNameArray = []
     37        defaultLayer.current = True
     38
     39        # 레이어가 1개 이상 존재하면
     40        if rt.LayerManager.count > 1:
     41            # 모든 레이어 순회하며 객체들을 기본 레이어로 이동
     42            for i in range(1, rt.layerManager.count):
     43                ilayer = rt.layerManager.getLayer(i)
     44                layerName = ilayer.name
     45                layerNameArray.append(layerName)
     46
     47                layer = rt.ILayerManager.getLayerObject(i)
     48                layerNodes = rt.refs.dependents(layer)
     49
     50                # 레이어의 모든 노드를 기본 레이어로 이동
     51                for item in layerNodes:
     52                    if rt.isValidNode(item):
     53                        defaultLayer.addNode(item)
     54
     55            # 모든 레이어 삭제
     56            for item in layerNameArray:
     57                rt.LayerManager.deleteLayerByName(item)
     58
     59    def get_nodes_from_layer(self, inLayerNum):
     60        """
     61        레이어 번호로 해당 레이어의 노드들을 가져옴
     62
     63        Args:
     64            inLayerNum: 레이어 번호
     65
     66        Returns:
     67            레이어에 포함된 노드 배열 또는 빈 배열
     68        """
     69        returnVal = []
     70        layer = rt.ILayerManager.getLayerObject(inLayerNum)
     71        if layer is not None:
     72            layerNodes = rt.refs.dependents(layer)
     73            for item in layerNodes:
     74                if rt.isValidNode(item):
     75                    returnVal.append(item)
     76
     77        return returnVal
     78
     79    def get_layer_number(self, inLayerName):
     80        """
     81        레이어 이름으로 레이어 번호를 찾음
     82
     83        Args:
     84            inLayerName: 레이어 이름
     85
     86        Returns:
     87            레이어 번호 또는 False (없는 경우)
     88        """
     89        # 모든 레이어를 순회하며 이름 비교
     90        for i in range(rt.LayerManager.count):
     91            layer = rt.layerManager.getLayer(i)
     92            if layer.name == inLayerName:
     93                return i
     94
     95        return False
     96
     97    def get_nodes_by_layername(self, inLayerName):
     98        """
     99        레이어 이름으로 해당 레이어의 노드들을 가져옴
    100
    101        Args:
    102            inLayerName: 레이어 이름
    103
    104        Returns:
    105            레이어에 포함된 노드 배열
    106        """
    107        return self.get_nodes_from_layer(self.get_layer_number(inLayerName))
    108
    109    def del_empty_layer(self, showLog=False):
    110        """
    111        빈 레이어 삭제
    112
    113        Args:
    114            showLog: 삭제 결과 메시지 표시 여부
    115
    116        Returns:
    117            None
    118        """
    119        deleted_layer_count = 0
    120        deflayer = rt.layermanager.getlayer(0)
    121        deflayer.current = True
    122
    123        # 모든 레이어를 역순으로 순회 (삭제 시 인덱스 변경 문제 방지)
    124        for i in range(rt.Layermanager.count-1, 0, -1):
    125            layer = rt.layermanager.getLayer(i)
    126            thisLayerName = layer.name
    127            nodes = self.get_nodes_from_layer(i)
    128
    129            # 노드가 없는 레이어 삭제
    130            if len(nodes) == 0:
    131                rt.LayerManager.deleteLayerbyname(thisLayerName)
    132                deleted_layer_count += 1
    133
    134        # 로그 표시 옵션이 활성화되어 있고 삭제된 레이어가 있는 경우
    135        if showLog and deleted_layer_count != 0:
    136            print(f"Number of layers removed = {deleted_layer_count}")
    137
    138    def create_layer_from_array(self, inArray, inLayerName):
    139        """
    140        객체 배열로 새 레이어 생성
    141
    142        Args:
    143            inArray: 레이어에 추가할 객체 배열
    144            inLayerName: 생성할 레이어 이름
    145
    146        Returns:
    147            생성된 레이어
    148        """
    149        new_layer = None
    150        layer_index = self.get_layer_number(inLayerName)
    151
    152        # 레이어가 없으면 새로 생성, 있으면 기존 레이어 사용
    153        if layer_index is False:
    154            new_layer = rt.LayerManager.newLayer()
    155            new_layer.setName(inLayerName)
    156        else:
    157            new_layer = rt.layerManager.getLayer(layer_index)
    158
    159        # 모든 객체를 레이어에 추가
    160        for item in inArray:
    161            new_layer.addNode(item)
    162
    163        return new_layer
    164
    165    def delete_layer(self, inLayerName, forceDelete=False):
    166        """
    167        레이어 삭제
    168
    169        Args:
    170            inLayerName: 삭제할 레이어 이름
    171            forceDelete: 레이어 내 객체도 함께 삭제할지 여부 (False면 기본 레이어로 이동)
    172
    173        Returns:
    174            성공 여부
    175        """
    176        return_val = False
    177        deflayer = rt.layermanager.getlayer(0)
    178        deflayer.current = True
    179
    180        # 레이어의 모든 노드 가져오기
    181        nodes = self.get_nodes_by_layername(inLayerName)
    182
    183        if len(nodes) > 0:
    184            if forceDelete:
    185                # 강제 삭제 옵션이 켜져 있으면 객체도 함께 삭제
    186                rt.delete(nodes)
    187                nodes = rt.Array()
    188            else:
    189                # 아니면 기본 레이어로 이동
    190                for item in nodes:
    191                    deflayer.addNode(item)
    192
    193        # 레이어 삭제
    194        return_val = rt.LayerManager.deleteLayerbyname(inLayerName)
    195
    196        return return_val
    197
    198    def set_parent_layer(self, inLayerName, inParentName):
    199        """
    200        레이어 부모 설정
    201
    202        Args:
    203            inLayerName: 자식 레이어 이름
    204            inParentName: 부모 레이어 이름
    205
    206        Returns:
    207            성공 여부
    208        """
    209        returnVal = False
    210
    211        # 타겟 레이어와 부모 레이어 가져오기
    212        targetLayer = rt.layermanager.getlayer(self.get_layer_number(inLayerName))
    213        parentLayer = rt.layermanager.getlayer(self.get_layer_number(inParentName))
    214
    215        # 두 레이어가 모두 존재하면 부모 설정
    216        if targetLayer is not None and parentLayer is not None:
    217            targetLayer.setParent(parentLayer)
    218            returnVal = True
    219
    220        return returnVal
    221
    222    def rename_layer_from_index(self, inLayerIndex, searchFor, replaceWith):
    223        """
    224        레이어 이름의 특정 부분을 교체
    225
    226        Args:
    227            inLayerIndex: 레이어 인덱스
    228            searchFor: 검색할 문자열
    229            replaceWith: 교체할 문자열
    230
    231        Returns:
    232            None
    233        """
    234        targetLayer = rt.LayerManager.getLayer(inLayerIndex)
    235        layerName = targetLayer.name
    236
    237        # 문자열 찾기
    238        find_at = layerName.find(searchFor)
    239
    240        # 찾은 경우 교체
    241        if find_at != -1:
    242            new_name = layerName.replace(searchFor, replaceWith)
    243            targetLayer.setName(new_name)
    244
    245    def is_valid_layer(self, inLayerName=None, inLayerIndex=None):
    246        """
    247        유효한 레이어인지 확인
    248
    249        Args:
    250            inLayerName: 레이어 이름 (선택)
    251            inLayerIndex: 레이어 인덱스 (선택)
    252
    253        Returns:
    254            유효 여부
    255        """
    256        layer = None
    257
    258        # 이름으로 확인
    259        if inLayerName is not None:
    260            layer = rt.LayerManager.getLayerFromName(inLayerName)
    261        # 인덱스로 확인
    262        elif inLayerIndex is not None:
    263            layer = rt.LayerManager.getLayer(inLayerIndex)
    264
    265        # 레이어가 있으면 True, 없으면 False
    266        return layer is not None

레이어 관련 기능을 위한 클래스 MAXScript의 _Layer 구조체를 Python 클래스로 변환

pymxs 모듈을 통해 3ds Max의 레이어 관리 기능을 제어합니다.

Layer() View Source

    21    def __init__(self):
    22        """
    23        초기화 함수
    24        """
    25        pass

초기화 함수

def reset_layer(self): View Source

    27    def reset_layer(self):
    28        """
    29        모든 레이어를 초기화하고 기본 레이어로 객체 이동
    30
    31        Returns:
    32            None
    33        """
    34        # 기본 레이어(0번 레이어) 가져오기
    35        defaultLayer = rt.layerManager.getLayer(0)
    36        layerNameArray = []
    37        defaultLayer.current = True
    38
    39        # 레이어가 1개 이상 존재하면
    40        if rt.LayerManager.count > 1:
    41            # 모든 레이어 순회하며 객체들을 기본 레이어로 이동
    42            for i in range(1, rt.layerManager.count):
    43                ilayer = rt.layerManager.getLayer(i)
    44                layerName = ilayer.name
    45                layerNameArray.append(layerName)
    46
    47                layer = rt.ILayerManager.getLayerObject(i)
    48                layerNodes = rt.refs.dependents(layer)
    49
    50                # 레이어의 모든 노드를 기본 레이어로 이동
    51                for item in layerNodes:
    52                    if rt.isValidNode(item):
    53                        defaultLayer.addNode(item)
    54
    55            # 모든 레이어 삭제
    56            for item in layerNameArray:
    57                rt.LayerManager.deleteLayerByName(item)

모든 레이어를 초기화하고 기본 레이어로 객체 이동

Returns: None

def get_nodes_from_layer(self, inLayerNum): View Source

    59    def get_nodes_from_layer(self, inLayerNum):
    60        """
    61        레이어 번호로 해당 레이어의 노드들을 가져옴
    62
    63        Args:
    64            inLayerNum: 레이어 번호
    65
    66        Returns:
    67            레이어에 포함된 노드 배열 또는 빈 배열
    68        """
    69        returnVal = []
    70        layer = rt.ILayerManager.getLayerObject(inLayerNum)
    71        if layer is not None:
    72            layerNodes = rt.refs.dependents(layer)
    73            for item in layerNodes:
    74                if rt.isValidNode(item):
    75                    returnVal.append(item)
    76
    77        return returnVal

레이어 번호로 해당 레이어의 노드들을 가져옴

Args: inLayerNum: 레이어 번호

Returns: 레이어에 포함된 노드 배열 또는 빈 배열

def get_layer_number(self, inLayerName): View Source

    79    def get_layer_number(self, inLayerName):
    80        """
    81        레이어 이름으로 레이어 번호를 찾음
    82
    83        Args:
    84            inLayerName: 레이어 이름
    85
    86        Returns:
    87            레이어 번호 또는 False (없는 경우)
    88        """
    89        # 모든 레이어를 순회하며 이름 비교
    90        for i in range(rt.LayerManager.count):
    91            layer = rt.layerManager.getLayer(i)
    92            if layer.name == inLayerName:
    93                return i
    94
    95        return False

레이어 이름으로 레이어 번호를 찾음

Args: inLayerName: 레이어 이름

Returns: 레이어 번호 또는 False (없는 경우)

def get_nodes_by_layername(self, inLayerName): View Source

     97    def get_nodes_by_layername(self, inLayerName):
     98        """
     99        레이어 이름으로 해당 레이어의 노드들을 가져옴
    100
    101        Args:
    102            inLayerName: 레이어 이름
    103
    104        Returns:
    105            레이어에 포함된 노드 배열
    106        """
    107        return self.get_nodes_from_layer(self.get_layer_number(inLayerName))

레이어 이름으로 해당 레이어의 노드들을 가져옴

Args: inLayerName: 레이어 이름

Returns: 레이어에 포함된 노드 배열

def del_empty_layer(self, showLog=False): View Source

    109    def del_empty_layer(self, showLog=False):
    110        """
    111        빈 레이어 삭제
    112
    113        Args:
    114            showLog: 삭제 결과 메시지 표시 여부
    115
    116        Returns:
    117            None
    118        """
    119        deleted_layer_count = 0
    120        deflayer = rt.layermanager.getlayer(0)
    121        deflayer.current = True
    122
    123        # 모든 레이어를 역순으로 순회 (삭제 시 인덱스 변경 문제 방지)
    124        for i in range(rt.Layermanager.count-1, 0, -1):
    125            layer = rt.layermanager.getLayer(i)
    126            thisLayerName = layer.name
    127            nodes = self.get_nodes_from_layer(i)
    128
    129            # 노드가 없는 레이어 삭제
    130            if len(nodes) == 0:
    131                rt.LayerManager.deleteLayerbyname(thisLayerName)
    132                deleted_layer_count += 1
    133
    134        # 로그 표시 옵션이 활성화되어 있고 삭제된 레이어가 있는 경우
    135        if showLog and deleted_layer_count != 0:
    136            print(f"Number of layers removed = {deleted_layer_count}")

빈 레이어 삭제

Args: showLog: 삭제 결과 메시지 표시 여부

Returns: None

def create_layer_from_array(self, inArray, inLayerName): View Source

    138    def create_layer_from_array(self, inArray, inLayerName):
    139        """
    140        객체 배열로 새 레이어 생성
    141
    142        Args:
    143            inArray: 레이어에 추가할 객체 배열
    144            inLayerName: 생성할 레이어 이름
    145
    146        Returns:
    147            생성된 레이어
    148        """
    149        new_layer = None
    150        layer_index = self.get_layer_number(inLayerName)
    151
    152        # 레이어가 없으면 새로 생성, 있으면 기존 레이어 사용
    153        if layer_index is False:
    154            new_layer = rt.LayerManager.newLayer()
    155            new_layer.setName(inLayerName)
    156        else:
    157            new_layer = rt.layerManager.getLayer(layer_index)
    158
    159        # 모든 객체를 레이어에 추가
    160        for item in inArray:
    161            new_layer.addNode(item)
    162
    163        return new_layer

객체 배열로 새 레이어 생성

Args: inArray: 레이어에 추가할 객체 배열 inLayerName: 생성할 레이어 이름

Returns: 생성된 레이어

def delete_layer(self, inLayerName, forceDelete=False): View Source

    165    def delete_layer(self, inLayerName, forceDelete=False):
    166        """
    167        레이어 삭제
    168
    169        Args:
    170            inLayerName: 삭제할 레이어 이름
    171            forceDelete: 레이어 내 객체도 함께 삭제할지 여부 (False면 기본 레이어로 이동)
    172
    173        Returns:
    174            성공 여부
    175        """
    176        return_val = False
    177        deflayer = rt.layermanager.getlayer(0)
    178        deflayer.current = True
    179
    180        # 레이어의 모든 노드 가져오기
    181        nodes = self.get_nodes_by_layername(inLayerName)
    182
    183        if len(nodes) > 0:
    184            if forceDelete:
    185                # 강제 삭제 옵션이 켜져 있으면 객체도 함께 삭제
    186                rt.delete(nodes)
    187                nodes = rt.Array()
    188            else:
    189                # 아니면 기본 레이어로 이동
    190                for item in nodes:
    191                    deflayer.addNode(item)
    192
    193        # 레이어 삭제
    194        return_val = rt.LayerManager.deleteLayerbyname(inLayerName)
    195
    196        return return_val

레이어 삭제

Args: inLayerName: 삭제할 레이어 이름 forceDelete: 레이어 내 객체도 함께 삭제할지 여부 (False면 기본 레이어로 이동)

Returns: 성공 여부

def set_parent_layer(self, inLayerName, inParentName): View Source

    198    def set_parent_layer(self, inLayerName, inParentName):
    199        """
    200        레이어 부모 설정
    201
    202        Args:
    203            inLayerName: 자식 레이어 이름
    204            inParentName: 부모 레이어 이름
    205
    206        Returns:
    207            성공 여부
    208        """
    209        returnVal = False
    210
    211        # 타겟 레이어와 부모 레이어 가져오기
    212        targetLayer = rt.layermanager.getlayer(self.get_layer_number(inLayerName))
    213        parentLayer = rt.layermanager.getlayer(self.get_layer_number(inParentName))
    214
    215        # 두 레이어가 모두 존재하면 부모 설정
    216        if targetLayer is not None and parentLayer is not None:
    217            targetLayer.setParent(parentLayer)
    218            returnVal = True
    219
    220        return returnVal

레이어 부모 설정

Args: inLayerName: 자식 레이어 이름 inParentName: 부모 레이어 이름

Returns: 성공 여부

def rename_layer_from_index(self, inLayerIndex, searchFor, replaceWith): View Source

    222    def rename_layer_from_index(self, inLayerIndex, searchFor, replaceWith):
    223        """
    224        레이어 이름의 특정 부분을 교체
    225
    226        Args:
    227            inLayerIndex: 레이어 인덱스
    228            searchFor: 검색할 문자열
    229            replaceWith: 교체할 문자열
    230
    231        Returns:
    232            None
    233        """
    234        targetLayer = rt.LayerManager.getLayer(inLayerIndex)
    235        layerName = targetLayer.name
    236
    237        # 문자열 찾기
    238        find_at = layerName.find(searchFor)
    239
    240        # 찾은 경우 교체
    241        if find_at != -1:
    242            new_name = layerName.replace(searchFor, replaceWith)
    243            targetLayer.setName(new_name)

레이어 이름의 특정 부분을 교체

Args: inLayerIndex: 레이어 인덱스 searchFor: 검색할 문자열 replaceWith: 교체할 문자열

Returns: None

def is_valid_layer(self, inLayerName=None, inLayerIndex=None): View Source

    245    def is_valid_layer(self, inLayerName=None, inLayerIndex=None):
    246        """
    247        유효한 레이어인지 확인
    248
    249        Args:
    250            inLayerName: 레이어 이름 (선택)
    251            inLayerIndex: 레이어 인덱스 (선택)
    252
    253        Returns:
    254            유효 여부
    255        """
    256        layer = None
    257
    258        # 이름으로 확인
    259        if inLayerName is not None:
    260            layer = rt.LayerManager.getLayerFromName(inLayerName)
    261        # 인덱스로 확인
    262        elif inLayerIndex is not None:
    263            layer = rt.LayerManager.getLayer(inLayerIndex)
    264
    265        # 레이어가 있으면 True, 없으면 False
    266        return layer is not None

유효한 레이어인지 확인

Args: inLayerName: 레이어 이름 (선택) inLayerIndex: 레이어 인덱스 (선택)

Returns: 유효 여부

class Align: View Source

     13class Align:
     14    """
     15    객체 정렬 관련 기능을 제공하는 클래스.
     16    MAXScript의 _Align 구조체 개념을 Python으로 재구현한 클래스이며, 3ds Max의 기능들을 pymxs API를 통해 제어합니다.
     17    """
     18
     19    def __init__(self):
     20        """클래스 초기화 (현재 특별한 초기화 동작은 없음)"""
     21        pass
     22
     23    def align_to_last_sel_center(self):
     24        """
     25        선택된 객체들을 마지막 선택된 객체의 중심점으로 정렬.
     26
     27        모든 객체의 트랜스폼은 마지막 선택된 객체의 트랜스폼을 가지며,
     28        위치는 마지막 선택된 객체의 중심점(center)으로 설정됩니다.
     29        """
     30        selection_count = rt.selection.count
     31
     32        if selection_count > 1:
     33            for i in range(selection_count):
     34                rt.setProperty(rt.selection[i], "transform", rt.selection[selection_count-1].transform)
     35                rt.setProperty(rt.selection[i], "position", rt.selection[selection_count-1].center)
     36
     37    def align_to_last_sel(self):
     38        """
     39        선택된 객체들을 마지막 선택된 객체의 트랜스폼으로 정렬.
     40
     41        모든 객체의 트랜스폼은 마지막 선택된 객체의 트랜스폼을 가지게 됩니다.
     42        """
     43        selection_count = rt.selection.count
     44
     45        if selection_count > 1:
     46            for i in range(selection_count):
     47                # 인덱스가 0부터 시작하는 Python과 달리 MAXScript는 1부터 시작하므로 i+1 사용
     48                rt.selection[i].transform = rt.selection[selection_count-1].transform
     49
     50    def align_to_last_sel_pos(self):
     51        """
     52        선택된 객체들을 마지막 선택된 객체의 위치로 정렬 (회전은 유지).
     53
     54        위치는 마지막 선택된 객체를 따르고,
     55        회전은 원래 객체의 회전을 유지합니다.
     56        """
     57        selection_count = rt.selection.count
     58
     59        if selection_count > 1:
     60            for i in range(selection_count):
     61                # 임시 포인트 객체 생성
     62                pos_dum_point = rt.Point()
     63                # 위치와 회전 제약 컨트롤러 생성
     64                pos_const = rt.Position_Constraint()
     65                rot_const = rt.Orientation_Constraint()
     66
     67                # 포인트에 컨트롤러 할당
     68                rt.setPropertyController(pos_dum_point.controller, "Position", pos_const)
     69                rt.setPropertyController(pos_dum_point.controller, "Rotation", rot_const)
     70
     71                # 위치는 마지막 선택된 객체 기준, 회전은 현재 처리 중인 객체 기준
     72                pos_const.appendTarget(rt.selection[selection_count-1], 100.0)
     73                rot_const.appendTarget(rt.selection[i], 100.0)
     74
     75                # 계산된 변환 행렬을 객체에 적용
     76                rt.setProperty(rt.selection[i], "transform", pos_dum_point.transform)
     77
     78                # 임시 객체 삭제
     79                rt.delete(pos_dum_point)
     80
     81    def align_to_last_sel_rot(self):
     82        """
     83        선택된 객체들을 마지막 선택된 객체의 회전으로 정렬 (위치는 유지).
     84
     85        회전은 마지막 선택된 객체를 따르고,
     86        위치는 원래 객체의 위치를 유지합니다.
     87        """
     88        selection_count = rt.selection.count
     89
     90        if selection_count > 1:
     91            for i in range(selection_count):
     92                # 인덱스가 0부터 시작하는 Python과 달리 MAXScript는 1부터 시작하므로 i+1 사용
     93                # 임시 포인트 객체 생성
     94                rot_dum_point = rt.Point()
     95                # 위치와 회전 제약 컨트롤러 생성
     96                pos_const = rt.Position_Constraint()
     97                rot_const = rt.Orientation_Constraint()
     98
     99                # 포인트에 컨트롤러 할당
    100                rot_dum_point.position.controller = pos_const
    101                rot_dum_point.rotation.controller = rot_const
    102                rt.setPropertyController(rot_dum_point.controller, "Position", pos_const)
    103                rt.setPropertyController(rot_dum_point.controller, "Rotation", rot_const)
    104
    105                # 위치는 현재 처리 중인 객체 기준, 회전은 마지막 선택된 객체 기준
    106                pos_const.appendTarget(rt.selection[i], 100.0)
    107                rot_const.appendTarget(rt.selection[selection_count-1], 100.0)
    108
    109                # 계산된 변환 행렬을 객체에 적용
    110                rt.setProperty(rt.selection[i], "transform", rot_dum_point.transform)
    111
    112                # 임시 객체 삭제
    113                rt.delete(rot_dum_point)

객체 정렬 관련 기능을 제공하는 클래스. MAXScript의 _Align 구조체 개념을 Python으로 재구현한 클래스이며, 3ds Max의 기능들을 pymxs API를 통해 제어합니다.

Align() View Source

    19    def __init__(self):
    20        """클래스 초기화 (현재 특별한 초기화 동작은 없음)"""
    21        pass

클래스 초기화 (현재 특별한 초기화 동작은 없음)

def align_to_last_sel_center(self): View Source

    23    def align_to_last_sel_center(self):
    24        """
    25        선택된 객체들을 마지막 선택된 객체의 중심점으로 정렬.
    26
    27        모든 객체의 트랜스폼은 마지막 선택된 객체의 트랜스폼을 가지며,
    28        위치는 마지막 선택된 객체의 중심점(center)으로 설정됩니다.
    29        """
    30        selection_count = rt.selection.count
    31
    32        if selection_count > 1:
    33            for i in range(selection_count):
    34                rt.setProperty(rt.selection[i], "transform", rt.selection[selection_count-1].transform)
    35                rt.setProperty(rt.selection[i], "position", rt.selection[selection_count-1].center)

선택된 객체들을 마지막 선택된 객체의 중심점으로 정렬.

모든 객체의 트랜스폼은 마지막 선택된 객체의 트랜스폼을 가지며, 위치는 마지막 선택된 객체의 중심점(center)으로 설정됩니다.

def align_to_last_sel(self): View Source

    37    def align_to_last_sel(self):
    38        """
    39        선택된 객체들을 마지막 선택된 객체의 트랜스폼으로 정렬.
    40
    41        모든 객체의 트랜스폼은 마지막 선택된 객체의 트랜스폼을 가지게 됩니다.
    42        """
    43        selection_count = rt.selection.count
    44
    45        if selection_count > 1:
    46            for i in range(selection_count):
    47                # 인덱스가 0부터 시작하는 Python과 달리 MAXScript는 1부터 시작하므로 i+1 사용
    48                rt.selection[i].transform = rt.selection[selection_count-1].transform

선택된 객체들을 마지막 선택된 객체의 트랜스폼으로 정렬.

모든 객체의 트랜스폼은 마지막 선택된 객체의 트랜스폼을 가지게 됩니다.

def align_to_last_sel_pos(self): View Source

    50    def align_to_last_sel_pos(self):
    51        """
    52        선택된 객체들을 마지막 선택된 객체의 위치로 정렬 (회전은 유지).
    53
    54        위치는 마지막 선택된 객체를 따르고,
    55        회전은 원래 객체의 회전을 유지합니다.
    56        """
    57        selection_count = rt.selection.count
    58
    59        if selection_count > 1:
    60            for i in range(selection_count):
    61                # 임시 포인트 객체 생성
    62                pos_dum_point = rt.Point()
    63                # 위치와 회전 제약 컨트롤러 생성
    64                pos_const = rt.Position_Constraint()
    65                rot_const = rt.Orientation_Constraint()
    66
    67                # 포인트에 컨트롤러 할당
    68                rt.setPropertyController(pos_dum_point.controller, "Position", pos_const)
    69                rt.setPropertyController(pos_dum_point.controller, "Rotation", rot_const)
    70
    71                # 위치는 마지막 선택된 객체 기준, 회전은 현재 처리 중인 객체 기준
    72                pos_const.appendTarget(rt.selection[selection_count-1], 100.0)
    73                rot_const.appendTarget(rt.selection[i], 100.0)
    74
    75                # 계산된 변환 행렬을 객체에 적용
    76                rt.setProperty(rt.selection[i], "transform", pos_dum_point.transform)
    77
    78                # 임시 객체 삭제
    79                rt.delete(pos_dum_point)

선택된 객체들을 마지막 선택된 객체의 위치로 정렬 (회전은 유지).

위치는 마지막 선택된 객체를 따르고, 회전은 원래 객체의 회전을 유지합니다.

def align_to_last_sel_rot(self): View Source

     81    def align_to_last_sel_rot(self):
     82        """
     83        선택된 객체들을 마지막 선택된 객체의 회전으로 정렬 (위치는 유지).
     84
     85        회전은 마지막 선택된 객체를 따르고,
     86        위치는 원래 객체의 위치를 유지합니다.
     87        """
     88        selection_count = rt.selection.count
     89
     90        if selection_count > 1:
     91            for i in range(selection_count):
     92                # 인덱스가 0부터 시작하는 Python과 달리 MAXScript는 1부터 시작하므로 i+1 사용
     93                # 임시 포인트 객체 생성
     94                rot_dum_point = rt.Point()
     95                # 위치와 회전 제약 컨트롤러 생성
     96                pos_const = rt.Position_Constraint()
     97                rot_const = rt.Orientation_Constraint()
     98
     99                # 포인트에 컨트롤러 할당
    100                rot_dum_point.position.controller = pos_const
    101                rot_dum_point.rotation.controller = rot_const
    102                rt.setPropertyController(rot_dum_point.controller, "Position", pos_const)
    103                rt.setPropertyController(rot_dum_point.controller, "Rotation", rot_const)
    104
    105                # 위치는 현재 처리 중인 객체 기준, 회전은 마지막 선택된 객체 기준
    106                pos_const.appendTarget(rt.selection[i], 100.0)
    107                rot_const.appendTarget(rt.selection[selection_count-1], 100.0)
    108
    109                # 계산된 변환 행렬을 객체에 적용
    110                rt.setProperty(rt.selection[i], "transform", rot_dum_point.transform)
    111
    112                # 임시 객체 삭제
    113                rt.delete(rot_dum_point)

선택된 객체들을 마지막 선택된 객체의 회전으로 정렬 (위치는 유지).

회전은 마지막 선택된 객체를 따르고, 위치는 원래 객체의 위치를 유지합니다.

class Select: View Source

     17class Select:
     18    """
     19    객체 선택 관련 기능을 제공하는 클래스.
     20    MAXScript의 _Select 구조체 개념을 Python으로 재구현한 클래스이며,
     21    3ds Max의 기능들을 pymxs API를 통해 제어합니다.
     22    """
     23
     24    def __init__(self, nameService=None, boneService=None):
     25        """
     26        클래스 초기화
     27
     28        Args:
     29            nameService: Name 서비스 인스턴스 (제공되지 않으면 새로 생성)
     30            boneService: Bone 서비스 인스턴스 (제공되지 않으면 새로 생성)
     31        """
     32        self.name = nameService if nameService else Name()
     33        self.bone = boneService if boneService else Bone(nameService=self.name) # Pass the potentially newly created nameService
     34
     35    def set_selectionSet_to_all(self):
     36        """
     37        모든 유형의 객체를 선택하도록 필터 설정
     38        """
     39        rt.SetSelectFilter(1)
     40
     41    def set_selectionSet_to_bone(self):
     42        """
     43        뼈대 객체만 선택하도록 필터 설정
     44        """
     45        rt.SetSelectFilter(8)
     46
     47    def reset_selectionSet(self):
     48        """
     49        선택 필터를 기본값으로 재설정
     50        """
     51        rt.SetSelectFilter(1)
     52
     53    def set_selectionSet_to_helper(self):
     54        """
     55        헬퍼 객체만 선택하도록 필터 설정
     56        """
     57        rt.SetSelectFilter(6)
     58
     59    def set_selectionSet_to_point(self):
     60        """
     61        포인트 객체만 선택하도록 필터 설정
     62        """
     63        rt.SetSelectFilter(10)
     64
     65    def set_selectionSet_to_spline(self):
     66        """
     67        스플라인 객체만 선택하도록 필터 설정
     68        """
     69        rt.SetSelectFilter(3)
     70
     71    def set_selectionSet_to_mesh(self):
     72        """
     73        메시 객체만 선택하도록 필터 설정
     74        """
     75        rt.SetSelectFilter(2)
     76
     77    def filter_bip(self):
     78        """
     79        현재 선택 항목에서 Biped 객체만 필터링하여 선택
     80        """
     81        sel_array = rt.getCurrentSelection()
     82        if len(sel_array) > 0:
     83            filtered_sel = [item for item in sel_array if rt.classOf(item) == rt.Biped_Object]
     84            rt.clearSelection()
     85            rt.select(filtered_sel)
     86
     87    def filter_bone(self):
     88        """
     89        현재 선택 항목에서 뼈대 객체만 필터링하여 선택
     90        """
     91        sel_array = rt.getCurrentSelection()
     92        if len(sel_array) > 0:
     93            filtered_sel = [item for item in sel_array if rt.classOf(item) == rt.BoneGeometry]
     94            rt.clearSelection()
     95            rt.select(filtered_sel)
     96
     97    def filter_helper(self):
     98        """
     99        현재 선택 항목에서 헬퍼 객체(Point, IK_Chain)만 필터링하여 선택
    100        """
    101        sel_array = rt.getCurrentSelection()
    102        if len(sel_array) > 0:
    103            filtered_sel = [item for item in sel_array if rt.classOf(item) == rt.Point or rt.classOf(item) == rt.IK_Chain_Object]
    104            rt.clearSelection()
    105            rt.select(filtered_sel)
    106
    107    def filter_expTm(self):
    108        """
    109        현재 선택 항목에서 ExposeTm 객체만 필터링하여 선택
    110        """
    111        sel_array = rt.getCurrentSelection()
    112        if len(sel_array) > 0:
    113            filtered_sel = [item for item in sel_array if rt.classOf(item) == rt.ExposeTm]
    114            rt.clearSelection()
    115            rt.select(filtered_sel)
    116
    117    def filter_spline(self):
    118        """
    119        현재 선택 항목에서 스플라인 객체만 필터링하여 선택
    120        """
    121        sel_array = rt.getCurrentSelection()
    122        if len(sel_array) > 0:
    123            filtered_sel = [item for item in sel_array if rt.superClassOf(item) == rt.shape]
    124            rt.clearSelection()
    125            rt.select(filtered_sel)
    126
    127    def select_children(self, inObj, includeSelf=False):
    128        """
    129        객체의 모든 자식을 선택
    130
    131        Args:
    132            in_obj: 부모 객체
    133            include_self: 자신도 포함할지 여부 (기본값: False)
    134
    135        Returns:
    136            선택된 자식 객체 리스트
    137        """
    138        children = self.bone.select_every_children(inObj=inObj, includeSelf=includeSelf)
    139
    140        return children
    141
    142    def distinguish_hierachy_objects(self, inArray):
    143        """
    144        계층이 있는 객체와 없는 객체 구분
    145
    146        Args:
    147            inArray: 검사할 객체 배열
    148
    149        Returns:
    150            [계층이 없는 객체 배열, 계층이 있는 객체 배열]
    151        """
    152        return_array = [[], []]  # 첫 번째는 독립 객체, 두 번째는 계층 객체
    153
    154        for item in inArray:
    155            if item.parent is None and item.children.count == 0:
    156                return_array[0].append(item)  # 부모와 자식이 없는 경우
    157            else:
    158                return_array[1].append(item)  # 부모나 자식이 있는 경우
    159
    160        return return_array
    161
    162    def get_nonLinked_objects(self, inArray):
    163        """
    164        링크(계층구조)가 없는 독립 객체만 반환
    165
    166        Args:
    167            inArray: 검사할 객체 배열
    168
    169        Returns:
    170            독립적인 객체 배열
    171        """
    172        return self.distinguish_hierachy_objects(inArray)[0]
    173
    174    def get_linked_objects(self, inArray):
    175        """
    176        링크(계층구조)가 있는 객체만 반환
    177
    178        Args:
    179            inArray: 검사할 객체 배열
    180
    181        Returns:
    182            계층 구조를 가진 객체 배열
    183        """
    184        return self.distinguish_hierachy_objects(inArray)[1]
    185
    186    def sort_by_hierachy(self, inArray):
    187        """
    188        객체를 계층 구조에 따라 정렬
    189
    190        Args:
    191            inArray: 정렬할 객체 배열
    192
    193        Returns:
    194            계층 순서대로 정렬된 객체 배열
    195        """
    196        return self.bone.sort_bones_as_hierarchy(inArray)
    197
    198    def sort_by_index(self, inArray):
    199        """
    200        객체를 이름에 포함된 인덱스 번호에 따라 정렬
    201
    202        Args:
    203            inArray: 정렬할 객체 배열
    204
    205        Returns:
    206            인덱스 순서대로 정렬된 객체 배열
    207        """
    208        if len(inArray) == 0:
    209            return []
    210
    211        nameArray = [item.name for item in inArray]
    212        sortedNameArray = self.name.sort_by_index(nameArray)
    213
    214        sortedArray = [item for item in inArray]
    215
    216        for i, sortedName in enumerate(sortedNameArray):
    217            foundIndex = nameArray.index(sortedName)
    218            sortedArray[i] = inArray[foundIndex]
    219
    220        return sortedArray
    221
    222    def sort_objects(self, inArray):
    223        """
    224        객체를 적절한 방법으로 정렬 (독립 객체와 계층 객체 모두 고려)
    225
    226        Args:
    227            inArray: 정렬할 객체 배열
    228
    229        Returns:
    230            정렬된 객체 배열
    231        """
    232        returnArray = []
    233
    234        # 독립 객체와 계층 객체 분류
    235        aloneObjArray = self.get_nonLinked_objects(inArray)
    236        hierachyObjArray = self.get_linked_objects(inArray)
    237
    238        # 각각의 방식으로 정렬
    239        sortedAloneObjArray = self.sort_by_index(aloneObjArray)
    240        sortedHierachyObjArray = self.sort_by_hierachy(hierachyObjArray)
    241
    242        # 첫 인덱스 비교를 위한 초기화
    243        firstIndexOfAloneObj = 10000
    244        firstIndexOfHierachyObj = 10000
    245        is_alone_importer = False
    246
    247        # 독립 객체의 첫 인덱스 확인
    248        if len(sortedAloneObjArray) > 0:
    249            index_digit = self.name.get_index_as_digit(sortedAloneObjArray[0].name)
    250            if index_digit is False:
    251                firstIndexOfAloneObj = 0
    252            else:
    253                firstIndexOfAloneObj = index_digit
    254
    255        # 계층 객체의 첫 인덱스 확인
    256        if len(sortedHierachyObjArray) > 0:
    257            index_digit = self.name.get_index_as_digit(sortedHierachyObjArray[0].name)
    258            if index_digit is False:
    259                firstIndexOfHierachyObj = 0
    260            else:
    261                firstIndexOfHierachyObj = index_digit
    262
    263        # 인덱스에 따라 순서 결정
    264        if firstIndexOfAloneObj < firstIndexOfHierachyObj:
    265            is_alone_importer = True
    266
    267        # 결정된 순서에 따라 배열 합치기
    268        if is_alone_importer:
    269            for item in sortedAloneObjArray:
    270                returnArray.append(item)
    271            for item in sortedHierachyObjArray:
    272                returnArray.append(item)
    273        else:
    274            for item in sortedHierachyObjArray:
    275                returnArray.append(item)
    276            for item in sortedAloneObjArray:
    277                returnArray.append(item)
    278
    279        return returnArray

객체 선택 관련 기능을 제공하는 클래스. MAXScript의 _Select 구조체 개념을 Python으로 재구현한 클래스이며, 3ds Max의 기능들을 pymxs API를 통해 제어합니다.

Select(nameService=None, boneService=None) View Source

    24    def __init__(self, nameService=None, boneService=None):
    25        """
    26        클래스 초기화
    27
    28        Args:
    29            nameService: Name 서비스 인스턴스 (제공되지 않으면 새로 생성)
    30            boneService: Bone 서비스 인스턴스 (제공되지 않으면 새로 생성)
    31        """
    32        self.name = nameService if nameService else Name()
    33        self.bone = boneService if boneService else Bone(nameService=self.name) # Pass the potentially newly created nameService

클래스 초기화

Args: nameService: Name 서비스 인스턴스 (제공되지 않으면 새로 생성) boneService: Bone 서비스 인스턴스 (제공되지 않으면 새로 생성)

name
bone
def set_selectionSet_to_all(self): View Source

    35    def set_selectionSet_to_all(self):
    36        """
    37        모든 유형의 객체를 선택하도록 필터 설정
    38        """
    39        rt.SetSelectFilter(1)

모든 유형의 객체를 선택하도록 필터 설정

def set_selectionSet_to_bone(self): View Source

    41    def set_selectionSet_to_bone(self):
    42        """
    43        뼈대 객체만 선택하도록 필터 설정
    44        """
    45        rt.SetSelectFilter(8)

뼈대 객체만 선택하도록 필터 설정

def reset_selectionSet(self): View Source

    47    def reset_selectionSet(self):
    48        """
    49        선택 필터를 기본값으로 재설정
    50        """
    51        rt.SetSelectFilter(1)

선택 필터를 기본값으로 재설정

def set_selectionSet_to_helper(self): View Source

    53    def set_selectionSet_to_helper(self):
    54        """
    55        헬퍼 객체만 선택하도록 필터 설정
    56        """
    57        rt.SetSelectFilter(6)

헬퍼 객체만 선택하도록 필터 설정

def set_selectionSet_to_point(self): View Source

    59    def set_selectionSet_to_point(self):
    60        """
    61        포인트 객체만 선택하도록 필터 설정
    62        """
    63        rt.SetSelectFilter(10)

포인트 객체만 선택하도록 필터 설정

def set_selectionSet_to_spline(self): View Source

    65    def set_selectionSet_to_spline(self):
    66        """
    67        스플라인 객체만 선택하도록 필터 설정
    68        """
    69        rt.SetSelectFilter(3)

스플라인 객체만 선택하도록 필터 설정

def set_selectionSet_to_mesh(self): View Source

    71    def set_selectionSet_to_mesh(self):
    72        """
    73        메시 객체만 선택하도록 필터 설정
    74        """
    75        rt.SetSelectFilter(2)

메시 객체만 선택하도록 필터 설정

def filter_bip(self): View Source

    77    def filter_bip(self):
    78        """
    79        현재 선택 항목에서 Biped 객체만 필터링하여 선택
    80        """
    81        sel_array = rt.getCurrentSelection()
    82        if len(sel_array) > 0:
    83            filtered_sel = [item for item in sel_array if rt.classOf(item) == rt.Biped_Object]
    84            rt.clearSelection()
    85            rt.select(filtered_sel)

현재 선택 항목에서 Biped 객체만 필터링하여 선택

def filter_bone(self): View Source

    87    def filter_bone(self):
    88        """
    89        현재 선택 항목에서 뼈대 객체만 필터링하여 선택
    90        """
    91        sel_array = rt.getCurrentSelection()
    92        if len(sel_array) > 0:
    93            filtered_sel = [item for item in sel_array if rt.classOf(item) == rt.BoneGeometry]
    94            rt.clearSelection()
    95            rt.select(filtered_sel)

현재 선택 항목에서 뼈대 객체만 필터링하여 선택

def filter_helper(self): View Source

     97    def filter_helper(self):
     98        """
     99        현재 선택 항목에서 헬퍼 객체(Point, IK_Chain)만 필터링하여 선택
    100        """
    101        sel_array = rt.getCurrentSelection()
    102        if len(sel_array) > 0:
    103            filtered_sel = [item for item in sel_array if rt.classOf(item) == rt.Point or rt.classOf(item) == rt.IK_Chain_Object]
    104            rt.clearSelection()
    105            rt.select(filtered_sel)

현재 선택 항목에서 헬퍼 객체(Point, IK_Chain)만 필터링하여 선택

def filter_expTm(self): View Source

    107    def filter_expTm(self):
    108        """
    109        현재 선택 항목에서 ExposeTm 객체만 필터링하여 선택
    110        """
    111        sel_array = rt.getCurrentSelection()
    112        if len(sel_array) > 0:
    113            filtered_sel = [item for item in sel_array if rt.classOf(item) == rt.ExposeTm]
    114            rt.clearSelection()
    115            rt.select(filtered_sel)

현재 선택 항목에서 ExposeTm 객체만 필터링하여 선택

def filter_spline(self): View Source

    117    def filter_spline(self):
    118        """
    119        현재 선택 항목에서 스플라인 객체만 필터링하여 선택
    120        """
    121        sel_array = rt.getCurrentSelection()
    122        if len(sel_array) > 0:
    123            filtered_sel = [item for item in sel_array if rt.superClassOf(item) == rt.shape]
    124            rt.clearSelection()
    125            rt.select(filtered_sel)

현재 선택 항목에서 스플라인 객체만 필터링하여 선택

def select_children(self, inObj, includeSelf=False): View Source

    127    def select_children(self, inObj, includeSelf=False):
    128        """
    129        객체의 모든 자식을 선택
    130
    131        Args:
    132            in_obj: 부모 객체
    133            include_self: 자신도 포함할지 여부 (기본값: False)
    134
    135        Returns:
    136            선택된 자식 객체 리스트
    137        """
    138        children = self.bone.select_every_children(inObj=inObj, includeSelf=includeSelf)
    139
    140        return children

객체의 모든 자식을 선택

Args: in_obj: 부모 객체 include_self: 자신도 포함할지 여부 (기본값: False)

Returns: 선택된 자식 객체 리스트

def distinguish_hierachy_objects(self, inArray): View Source

    142    def distinguish_hierachy_objects(self, inArray):
    143        """
    144        계층이 있는 객체와 없는 객체 구분
    145
    146        Args:
    147            inArray: 검사할 객체 배열
    148
    149        Returns:
    150            [계층이 없는 객체 배열, 계층이 있는 객체 배열]
    151        """
    152        return_array = [[], []]  # 첫 번째는 독립 객체, 두 번째는 계층 객체
    153
    154        for item in inArray:
    155            if item.parent is None and item.children.count == 0:
    156                return_array[0].append(item)  # 부모와 자식이 없는 경우
    157            else:
    158                return_array[1].append(item)  # 부모나 자식이 있는 경우
    159
    160        return return_array

계층이 있는 객체와 없는 객체 구분

Args: inArray: 검사할 객체 배열

Returns: [계층이 없는 객체 배열, 계층이 있는 객체 배열]

def get_nonLinked_objects(self, inArray): View Source

    162    def get_nonLinked_objects(self, inArray):
    163        """
    164        링크(계층구조)가 없는 독립 객체만 반환
    165
    166        Args:
    167            inArray: 검사할 객체 배열
    168
    169        Returns:
    170            독립적인 객체 배열
    171        """
    172        return self.distinguish_hierachy_objects(inArray)[0]

링크(계층구조)가 없는 독립 객체만 반환

Args: inArray: 검사할 객체 배열

Returns: 독립적인 객체 배열

def get_linked_objects(self, inArray): View Source

    174    def get_linked_objects(self, inArray):
    175        """
    176        링크(계층구조)가 있는 객체만 반환
    177
    178        Args:
    179            inArray: 검사할 객체 배열
    180
    181        Returns:
    182            계층 구조를 가진 객체 배열
    183        """
    184        return self.distinguish_hierachy_objects(inArray)[1]

링크(계층구조)가 있는 객체만 반환

Args: inArray: 검사할 객체 배열

Returns: 계층 구조를 가진 객체 배열

def sort_by_hierachy(self, inArray): View Source

    186    def sort_by_hierachy(self, inArray):
    187        """
    188        객체를 계층 구조에 따라 정렬
    189
    190        Args:
    191            inArray: 정렬할 객체 배열
    192
    193        Returns:
    194            계층 순서대로 정렬된 객체 배열
    195        """
    196        return self.bone.sort_bones_as_hierarchy(inArray)

객체를 계층 구조에 따라 정렬

Args: inArray: 정렬할 객체 배열

Returns: 계층 순서대로 정렬된 객체 배열

def sort_by_index(self, inArray): View Source

    198    def sort_by_index(self, inArray):
    199        """
    200        객체를 이름에 포함된 인덱스 번호에 따라 정렬
    201
    202        Args:
    203            inArray: 정렬할 객체 배열
    204
    205        Returns:
    206            인덱스 순서대로 정렬된 객체 배열
    207        """
    208        if len(inArray) == 0:
    209            return []
    210
    211        nameArray = [item.name for item in inArray]
    212        sortedNameArray = self.name.sort_by_index(nameArray)
    213
    214        sortedArray = [item for item in inArray]
    215
    216        for i, sortedName in enumerate(sortedNameArray):
    217            foundIndex = nameArray.index(sortedName)
    218            sortedArray[i] = inArray[foundIndex]
    219
    220        return sortedArray

객체를 이름에 포함된 인덱스 번호에 따라 정렬

Args: inArray: 정렬할 객체 배열

Returns: 인덱스 순서대로 정렬된 객체 배열

def sort_objects(self, inArray): View Source

    222    def sort_objects(self, inArray):
    223        """
    224        객체를 적절한 방법으로 정렬 (독립 객체와 계층 객체 모두 고려)
    225
    226        Args:
    227            inArray: 정렬할 객체 배열
    228
    229        Returns:
    230            정렬된 객체 배열
    231        """
    232        returnArray = []
    233
    234        # 독립 객체와 계층 객체 분류
    235        aloneObjArray = self.get_nonLinked_objects(inArray)
    236        hierachyObjArray = self.get_linked_objects(inArray)
    237
    238        # 각각의 방식으로 정렬
    239        sortedAloneObjArray = self.sort_by_index(aloneObjArray)
    240        sortedHierachyObjArray = self.sort_by_hierachy(hierachyObjArray)
    241
    242        # 첫 인덱스 비교를 위한 초기화
    243        firstIndexOfAloneObj = 10000
    244        firstIndexOfHierachyObj = 10000
    245        is_alone_importer = False
    246
    247        # 독립 객체의 첫 인덱스 확인
    248        if len(sortedAloneObjArray) > 0:
    249            index_digit = self.name.get_index_as_digit(sortedAloneObjArray[0].name)
    250            if index_digit is False:
    251                firstIndexOfAloneObj = 0
    252            else:
    253                firstIndexOfAloneObj = index_digit
    254
    255        # 계층 객체의 첫 인덱스 확인
    256        if len(sortedHierachyObjArray) > 0:
    257            index_digit = self.name.get_index_as_digit(sortedHierachyObjArray[0].name)
    258            if index_digit is False:
    259                firstIndexOfHierachyObj = 0
    260            else:
    261                firstIndexOfHierachyObj = index_digit
    262
    263        # 인덱스에 따라 순서 결정
    264        if firstIndexOfAloneObj < firstIndexOfHierachyObj:
    265            is_alone_importer = True
    266
    267        # 결정된 순서에 따라 배열 합치기
    268        if is_alone_importer:
    269            for item in sortedAloneObjArray:
    270                returnArray.append(item)
    271            for item in sortedHierachyObjArray:
    272                returnArray.append(item)
    273        else:
    274            for item in sortedHierachyObjArray:
    275                returnArray.append(item)
    276            for item in sortedAloneObjArray:
    277                returnArray.append(item)
    278
    279        return returnArray

객체를 적절한 방법으로 정렬 (독립 객체와 계층 객체 모두 고려)

Args: inArray: 정렬할 객체 배열

Returns: 정렬된 객체 배열

class Link: View Source

    12class Link:
    13    """
    14    객체 연결(링크) 관련 기능을 위한 클래스
    15    MAXScript의 _Link 구조체를 Python 클래스로 변환
    16
    17    pymxs 모듈을 통해 3ds Max의 객체 간 부모-자식 관계를 관리합니다.
    18    """
    19
    20    def __init__(self):
    21        """
    22        초기화 함수
    23        """
    24        pass
    25
    26    def link_to_last_sel(self):
    27        """
    28        선택된 객체들을 마지막 선택 객체에 링크(부모로 지정)
    29
    30        Returns:
    31            None
    32        """
    33        # 선택된 객체가 2개 이상인 경우에만 처리
    34        if rt.selection.count > 1:
    35            # 첫 번째부터 마지막 직전까지의 모든 객체를 마지막 객체에 링크
    36            for i in range(rt.selection.count - 1):
    37                rt.selection[i].parent = rt.selection[rt.selection.count - 1]
    38
    39    def link_to_first_sel(self):
    40        """
    41        선택된 객체들을 첫 번째 선택 객체에 링크(부모로 지정)
    42
    43        Returns:
    44            None
    45        """
    46        # 선택된 객체가 2개 이상인 경우에만 처리
    47        if rt.selection.count > 1:
    48            # 두 번째부터 마지막까지의 모든 객체를 첫 번째 객체에 링크
    49            for i in range(1, rt.selection.count):
    50                rt.selection[i].parent = rt.selection[0]
    51
    52    def unlink_selection(self):
    53        """
    54        선택된 모든 객체의 부모 관계 해제
    55
    56        Returns:
    57            None
    58        """
    59        # 선택된 객체가 있는 경우에만 처리
    60        if rt.selection.count > 0:
    61            # 모든 선택 객체의 부모 관계 해제
    62            for item in rt.selection:
    63                item.parent = None
    64
    65    def unlink_children(self):
    66        """
    67        선택된 객체의 모든 자식 객체의 부모 관계 해제
    68
    69        Returns:
    70            None
    71        """
    72        # 정확히 하나의 객체가 선택된 경우에만 처리
    73        if rt.selection.count == 1:
    74            # 선택된 객체의 모든 자식 객체의 부모 관계 해제
    75            selObjs = rt.getCurrentSelection()
    76            childrenObjs = selObjs[0].children
    77            targetChildren = [child for child in childrenObjs]
    78            for child in targetChildren:
    79                child.parent = None

객체 연결(링크) 관련 기능을 위한 클래스 MAXScript의 _Link 구조체를 Python 클래스로 변환

pymxs 모듈을 통해 3ds Max의 객체 간 부모-자식 관계를 관리합니다.

Link() View Source

    20    def __init__(self):
    21        """
    22        초기화 함수
    23        """
    24        pass

초기화 함수

def link_to_last_sel(self): View Source

    26    def link_to_last_sel(self):
    27        """
    28        선택된 객체들을 마지막 선택 객체에 링크(부모로 지정)
    29
    30        Returns:
    31            None
    32        """
    33        # 선택된 객체가 2개 이상인 경우에만 처리
    34        if rt.selection.count > 1:
    35            # 첫 번째부터 마지막 직전까지의 모든 객체를 마지막 객체에 링크
    36            for i in range(rt.selection.count - 1):
    37                rt.selection[i].parent = rt.selection[rt.selection.count - 1]

선택된 객체들을 마지막 선택 객체에 링크(부모로 지정)

Returns: None

def link_to_first_sel(self): View Source

    39    def link_to_first_sel(self):
    40        """
    41        선택된 객체들을 첫 번째 선택 객체에 링크(부모로 지정)
    42
    43        Returns:
    44            None
    45        """
    46        # 선택된 객체가 2개 이상인 경우에만 처리
    47        if rt.selection.count > 1:
    48            # 두 번째부터 마지막까지의 모든 객체를 첫 번째 객체에 링크
    49            for i in range(1, rt.selection.count):
    50                rt.selection[i].parent = rt.selection[0]

선택된 객체들을 첫 번째 선택 객체에 링크(부모로 지정)

Returns: None

def unlink_selection(self): View Source

    52    def unlink_selection(self):
    53        """
    54        선택된 모든 객체의 부모 관계 해제
    55
    56        Returns:
    57            None
    58        """
    59        # 선택된 객체가 있는 경우에만 처리
    60        if rt.selection.count > 0:
    61            # 모든 선택 객체의 부모 관계 해제
    62            for item in rt.selection:
    63                item.parent = None

선택된 모든 객체의 부모 관계 해제

Returns: None

def unlink_children(self): View Source

    65    def unlink_children(self):
    66        """
    67        선택된 객체의 모든 자식 객체의 부모 관계 해제
    68
    69        Returns:
    70            None
    71        """
    72        # 정확히 하나의 객체가 선택된 경우에만 처리
    73        if rt.selection.count == 1:
    74            # 선택된 객체의 모든 자식 객체의 부모 관계 해제
    75            selObjs = rt.getCurrentSelection()
    76            childrenObjs = selObjs[0].children
    77            targetChildren = [child for child in childrenObjs]
    78            for child in targetChildren:
    79                child.parent = None

선택된 객체의 모든 자식 객체의 부모 관계 해제

Returns: None

class Bip: View Source

     21class Bip:
     22    """
     23    Biped 객체 관련 기능을 제공하는 클래스.
     24    MAXScript의 _Bip 구조체 개념을 Python으로 재구현한 클래스이며,
     25    3ds Max의 기능들을 pymxs API를 통해 제어합니다.
     26    """
     27
     28    def __init__(self, animService=None, nameService=None, boneService=None):
     29        """
     30        클래스 초기화
     31
     32        Args:
     33            animService: Anim 서비스 인스턴스 (제공되지 않으면 새로 생성)
     34            nameService: Name 서비스 인스턴스 (제공되지 않으면 새로 생성)
     35            boneService: Bone 서비스 인스턴스 (제공되지 않으면 새로 생성)
     36        """
     37        self.anim = animService if animService else Anim()
     38        self.name = nameService if nameService else Name()
     39        self.bone = boneService if boneService else Bone(nameService=self.name, animService=self.anim) # Pass potentially new instances
     40
     41    def get_bips(self):
     42        """
     43        씬 내의 모든 Biped_Object를 찾음
     44
     45        Returns:
     46            Biped_Object 리스트
     47        """
     48        return [obj for obj in rt.objects if rt.isKindOf(obj, rt.Biped_Object)]
     49
     50    def get_coms_name(self):
     51        """
     52        씬 내 모든 Biped COM(Center of Mass)의 이름 리스트 반환
     53
     54        Returns:
     55            Biped COM 이름 리스트
     56        """
     57        bips = self.get_bips()
     58        bipComsName = []
     59
     60        for obj in bips:
     61            rootName = obj.controller.rootName
     62            if rootName not in bipComsName:
     63                bipComsName.append(rootName)
     64
     65        return bipComsName
     66
     67    def get_coms(self):
     68        """
     69        씬 내 모든 Biped COM(Center of Mass) 객체 리스트 반환
     70
     71        Returns:
     72            Biped COM 객체 리스트
     73        """
     74        bips = self.get_bips()
     75        bipComs = []
     76
     77        for obj in bips:
     78            rootNode = obj.controller.rootNode
     79            if rootNode not in bipComs:
     80                bipComs.append(rootNode)
     81
     82        return bipComs
     83
     84    def is_biped_object(self, inObj):
     85        """
     86        객체가 Biped 관련 객체인지 확인
     87
     88        Args:
     89            inObj: 확인할 객체
     90
     91        Returns:
     92            Biped 관련 객체이면 True, 아니면 False
     93        """
     94        return (rt.classOf(inObj.controller) == rt.BipSlave_control or
     95                rt.classOf(inObj.controller) == rt.Footsteps or
     96                rt.classOf(inObj.controller) == rt.Vertical_Horizontal_Turn)
     97
     98    def get_com(self, inBip):
     99        """
    100        Biped 객체의 COM(Center of Mass) 반환
    101
    102        Args:
    103            inBip: COM을 찾을 Biped 객체
    104
    105        Returns:
    106            Biped의 COM 객체 또는 None
    107        """
    108        if self.is_biped_object(inBip):
    109            return inBip.controller.rootNode
    110        return None
    111
    112    def get_all(self, inBip):
    113        """
    114        Biped와 관련된 모든 객체 반환
    115
    116        Args:
    117            inBip: 기준 Biped 객체
    118
    119        Returns:
    120            Biped 관련 모든 객체 리스트
    121        """
    122        returnVal = []
    123
    124        if self.is_biped_object(inBip):
    125            root = self.get_com(inBip)
    126            allNodes = [root]
    127            returnVal = [root]
    128
    129            for obj in allNodes:
    130                for child in obj.children:
    131                    if child not in allNodes:
    132                        allNodes.append(child)
    133                    if self.is_biped_object(child) and child not in returnVal:
    134                        returnVal.append(child)
    135
    136                if obj.parent is not None:
    137                    if obj.parent not in allNodes:
    138                        allNodes.append(obj.parent)
    139                    if self.is_biped_object(obj.parent) and obj.parent not in returnVal:
    140                        returnVal.append(obj.parent)
    141
    142        return returnVal
    143
    144    def get_nodes(self, inBip):
    145        """
    146        Biped의 실제 노드만 반환 (더미나 Footstep은 제외)
    147
    148        Args:
    149            inBip: 기준 Biped 객체
    150
    151        Returns:
    152            Biped의 노드 객체 리스트
    153        """
    154        returnVal = []
    155
    156        if self.is_biped_object(inBip):
    157            root = self.get_com(inBip)
    158            allNodes = [root]
    159            returnVal = [root]
    160
    161            for obj in allNodes:
    162                for child in obj.children:
    163                    if rt.classOf(child) != rt.Dummy and rt.classOf(child.controller) != rt.Footsteps:
    164                        if child not in allNodes:
    165                            allNodes.append(child)
    166                        if self.is_biped_object(child) and child not in returnVal:
    167                            returnVal.append(child)
    168
    169                if obj.parent is not None:
    170                    if rt.classOf(obj.parent) != rt.Dummy and rt.classOf(obj.parent.controller) != rt.Footsteps:
    171                        if obj.parent not in allNodes:
    172                            allNodes.append(obj.parent)
    173                        if self.is_biped_object(obj.parent) and obj.parent not in returnVal:
    174                            returnVal.append(obj.parent)
    175
    176        return returnVal
    177
    178    def get_dummy_and_footstep(self, inBip):
    179        """
    180        Biped의 더미와 Footstep 객체만 반환
    181
    182        Args:
    183            inBip: 기준 Biped 객체
    184
    185        Returns:
    186            더미와 Footstep 객체 리스트
    187        """
    188        returnVal = []
    189
    190        if self.is_biped_object(inBip):
    191            bipArray = self.get_all(inBip)
    192            returnVal = [item for item in bipArray if rt.classOf(item) == rt.Dummy or rt.classOf(item.controller) == rt.Footsteps]
    193
    194        return returnVal
    195
    196    def get_all_grouped_nodes(self, inBip):
    197        """
    198        Biped의 체인 이름으로 노드 반환
    199
    200        Args:
    201            inBip: 기준 Biped 객체
    202
    203        Returns:
    204            해당 체인에 속하는 Biped 노드 리스트
    205        """
    206        # Define node categories with their corresponding index numbers
    207        NODE_CATEGORIES = {
    208            1: "lArm",
    209            2: "rArm",
    210            3: "lFingers",
    211            4: "rFingers",
    212            5: "lLeg",
    213            6: "rLeg",
    214            7: "lToes",
    215            8: "rToes",
    216            9: "spine",
    217            10: "tail",
    218            11: "head",
    219            12: "pelvis",
    220            17: "neck",
    221            18: "pony1",
    222            19: "pony2",
    223            20: "prop1",
    224            21: "prop2",
    225            22: "prop3"
    226        }
    227
    228        # Initialize node collections dictionary
    229        nodes = {category: [] for category in NODE_CATEGORIES.values()}
    230
    231        com = inBip.controller.rootNode
    232        if rt.classOf(inBip) != rt.Biped_Object:
    233            return nodes
    234
    235        nn = rt.biped.maxNumNodes(com)
    236        nl = rt.biped.maxNumLinks(com)
    237
    238        # Collect nodes by category
    239        for i in range(1, nn + 1):
    240            if i not in NODE_CATEGORIES:
    241                continue
    242
    243            category = NODE_CATEGORIES[i]
    244            anode = rt.biped.getNode(com, i)
    245
    246            if not anode:
    247                continue
    248
    249            for j in range(1, nl + 1):
    250                alink = rt.biped.getNode(com, i, link=j)
    251                if alink:
    252                    nodes[category].append(alink)
    253
    254        return nodes
    255
    256    def get_grouped_nodes(self, inBip,inGroupName):
    257        """
    258        Biped의 체인 이름으로 노드 반환
    259
    260        Args:
    261            inBip: 기준 Biped 객체
    262            inGroupName: 체인 이름 (예: "lArm", "rLeg" 등)
    263
    264        Returns:
    265            해당 체인에 속하는 Biped 노드 리스트
    266        """
    267        nodes = self.get_all_grouped_nodes(inBip)
    268
    269        if inGroupName in nodes:
    270            return nodes[inGroupName]
    271
    272        return []
    273
    274    def is_left_node(self, inNode):
    275        """
    276        노드가 왼쪽인지 확인
    277
    278        Args:
    279            inNode: 확인할 노드 객체
    280
    281        Returns:
    282            왼쪽 노드이면 True, 아니면 False
    283        """
    284        if rt.classOf(inNode) != rt.Biped_Object:
    285            return False
    286        com = self.get_com(inNode)
    287        nodes = self.get_all_grouped_nodes(com)
    288
    289        categories = ["lArm", "lFingers", "lLeg", "lToes"]
    290        for category in categories:
    291            groupedNodes = nodes[category]
    292            if inNode in groupedNodes:
    293                return True
    294
    295        return False
    296
    297    def is_right_node(self, inNode):
    298        """
    299        노드가 오른쪽인지 확인
    300
    301        Args:
    302            inNode: 확인할 노드 객체
    303
    304        Returns:
    305            오른쪽 노드이면 True, 아니면 False
    306        """
    307        if rt.classOf(inNode) != rt.Biped_Object:
    308            return False
    309        com = self.get_com(inNode)
    310        nodes = self.get_all_grouped_nodes(com)
    311
    312        categories = ["rArm", "rFingers", "rLeg", "rToes"]
    313        for category in categories:
    314            groupedNodes = nodes[category]
    315            if inNode in groupedNodes:
    316                return True
    317
    318        return False
    319
    320    def get_nodes_by_skeleton_order(self, inBip):
    321        """
    322        스켈레톤 순서대로 Biped 노드 반환
    323
    324        Args:
    325            inBip: 기준 Biped 객체
    326
    327        Returns:
    328            순서대로 정렬된 Biped 노드 리스트
    329        """
    330        nodes = self.get_all_grouped_nodes(inBip)
    331
    332        # Define the order of categories in final array
    333        ORDER = [
    334            "head", "pelvis", "lArm", "lFingers", "lLeg", "lToes", "neck",
    335            "rArm", "rFingers", "rLeg", "rToes", "spine", "tail",
    336            "pony1", "pony2", "prop1", "prop2", "prop3"
    337        ]
    338
    339        # Build final array in the desired order
    340        bipNodeArray = []
    341        for category in ORDER:
    342            bipNodeArray.extend(nodes[category])
    343
    344        return bipNodeArray
    345
    346    def load_bip_file(self, inBipRoot, inFile):
    347        """
    348        Biped BIP 파일 로드
    349
    350        Args:
    351            inBipRoot: 로드 대상 Biped 루트 노드
    352            inFile: 로드할 BIP 파일 경로
    353        """
    354        bipNodeArray = self.get_all(inBipRoot)
    355
    356        inBipRoot.controller.figureMode = False
    357        rt.biped.loadBipFile(inBipRoot.controller, inFile)
    358        inBipRoot.controller.figureMode = True
    359        inBipRoot.controller.figureMode = False
    360
    361        keyRange = []
    362        for i in range(1, len(bipNodeArray)):
    363            if bipNodeArray[i].controller.keys.count != 0 and bipNodeArray[i].controller.keys.count != -1:
    364                keyTime = bipNodeArray[i].controller.keys[bipNodeArray[i].controller.keys.count - 1].time
    365                if keyTime not in keyRange:
    366                    keyRange.append(keyTime)
    367
    368        if keyRange and max(keyRange) != 0:
    369            rt.animationRange = rt.interval(0, max(keyRange))
    370            rt.sliderTime = 0
    371
    372    def load_fig_file(self, inBipRoot, inFile):
    373        """
    374        Biped FIG 파일 로드
    375
    376        Args:
    377            inBipRoot: 로드 대상 Biped 루트 노드
    378            inFile: 로드할 FIG 파일 경로
    379        """
    380        inBipRoot.controller.figureMode = False
    381        inBipRoot.controller.figureMode = True
    382        rt.biped.LoadFigFile(inBipRoot.controller, inFile)
    383        inBipRoot.controller.figureMode = False
    384
    385    def save_fig_file(self, inBipRoot, fileName):
    386        """
    387        Biped FIG 파일 저장
    388
    389        Args:
    390            inBipRoot: 저장 대상 Biped 루트 노드
    391            fileName: 저장할 FIG 파일 경로
    392        """
    393        inBipRoot.controller.figureMode = False
    394        inBipRoot.controller.figureMode = True
    395        rt.biped.saveFigFile(inBipRoot.controller, fileName)
    396
    397    def turn_on_figure_mode(self, inBipRoot):
    398        """
    399        Biped Figure 모드 켜기
    400
    401        Args:
    402            inBipRoot: 대상 Biped 객체
    403        """
    404        inBipRoot.controller.figureMode = True
    405
    406    def turn_off_figure_mode(self, inBipRoot):
    407        """
    408        Biped Figure 모드 끄기
    409
    410        Args:
    411            inBipRoot: 대상 Biped 객체
    412        """
    413        inBipRoot.controller.figureMode = False
    414
    415    def delete_copy_collection(self, inBipRoot, inName):
    416        """
    417        Biped 복사 컬렉션 삭제
    418
    419        Args:
    420            inBipRoot: 대상 Biped 객체
    421            inName: 삭제할 컬렉션 이름
    422        """
    423        if self.is_biped_object(inBipRoot):
    424            colNum = rt.biped.numCopyCollections(inBipRoot.controller)
    425            if colNum > 0:
    426                for i in range(1, colNum + 1):
    427                    if rt.biped.getCopyCollection(inBipRoot.controller, i).name == inName:
    428                        rt.biped.deleteCopyCollection(inBipRoot.controller, i)
    429                        break
    430
    431    def delete_all_copy_collection(self, inBipRoot):
    432        """
    433        Biped 모든 복사 컬렉션 삭제
    434
    435        Args:
    436            inBipRoot: 대상 Biped 객체
    437        """
    438        if self.is_biped_object(inBipRoot):
    439            colNum = rt.biped.numCopyCollections(inBipRoot.controller)
    440            if colNum > 0:
    441                rt.biped.deleteAllCopyCollections(inBipRoot.controller)
    442
    443    def link_base_skeleton(self, skinBoneBaseName="b"):
    444        """
    445        기본 스켈레톤 링크 (Biped와 일반 뼈대 연결)
    446        """
    447        rt.setWaitCursor()
    448
    449        bipSkel = self.get_bips()
    450        baseSkel = [None] * len(bipSkel)
    451
    452        for i in range(len(bipSkel)):
    453            baseSkeletonName = self.name.replace_base(bipSkel[i].name, skinBoneBaseName)
    454            baseSkeletonName = self.name.replace_filteringChar(baseSkeletonName, "_")
    455            baseSkelObj = rt.getNodeByName(baseSkeletonName)
    456            if rt.isValidObj(baseSkelObj):
    457                baseSkel[i] = baseSkelObj
    458
    459            self.anim.save_xform(bipSkel[i])
    460            self.anim.set_xform(bipSkel[i])
    461
    462            self.anim.save_xform(baseSkel[i])
    463            self.anim.set_xform(baseSkel[i])
    464
    465        for i in range(len(baseSkel)):
    466            if baseSkel[i] is not None:
    467                baseSkel[i].scale.controller = rt.scaleXYZ()
    468                baseSkel[i].controller = rt.link_constraint()
    469
    470                self.anim.set_xform([baseSkel[i]], space="World")
    471                baseSkel[i].transform.controller.AddTarget(bipSkel[i], 0)
    472
    473        for i in range(len(baseSkel)):
    474            if baseSkel[i] is not None:
    475                baseSkel[i].boneEnable = True
    476
    477        rt.setArrowCursor()
    478
    479    def unlink_base_skeleton(self, skinBoneBaseName="b"):
    480        """
    481        기본 스켈레톤 링크 해제
    482        """
    483        rt.setWaitCursor()
    484
    485        bipComs = self.get_coms()
    486        allBips = self.get_nodes(bipComs[0])
    487        bipSkel = [item for item in allBips if item != bipComs[0]]
    488        baseSkel = [None] * len(bipSkel)
    489
    490        for i in range(len(bipSkel)):
    491            baseSkeletonName = self.name.replace_name_part("Base", bipSkel[i].name, skinBoneBaseName)
    492            baseSkeletonName = self.name.replace_filtering_char(baseSkeletonName, "_")
    493            print("baseSkeletonName", baseSkeletonName)
    494            baseSkelObj = rt.getNodeByName(baseSkeletonName)
    495            print("baseSkelObj", baseSkelObj)
    496            if rt.isValidObj(baseSkelObj):
    497                baseSkel[i] = baseSkelObj
    498
    499            self.anim.save_xform(bipSkel[i])
    500            self.anim.set_xform(bipSkel[i])
    501
    502            self.anim.save_xform(baseSkel[i])
    503            self.anim.set_xform(baseSkel[i])
    504
    505        for i in range(len(baseSkel)):
    506            if baseSkel[i] is not None:
    507                baseSkel[i].controller = rt.prs()
    508                self.anim.set_xform([baseSkel[i]], space="World")
    509
    510        for i in range(len(baseSkel)):
    511            if baseSkel[i] is not None:
    512                baseSkel[i].boneEnable = True
    513
    514        rt.setArrowCursor()
    515
    516    def convert_name_for_ue5(self, inBipRoot, inBipNameConfigFile):
    517        """
    518        Biped 이름을 UE5에 맞게 변환
    519
    520        Args:
    521            inBipRoot: 변환할 Biped 객체
    522
    523        Returns:
    524            변환된 Biped 객체
    525        """
    526        bipComs = self.get_coms()
    527
    528        if len(bipComs) > 1:
    529            rt.messageBox("Please select only one Biped object.")
    530            return False
    531
    532        from pyjallib.max.name import Name
    533
    534        bipNameTool = Name(configPath=inBipNameConfigFile)
    535
    536        bipObj = bipComs[0]
    537        bipNodes = self.get_all(bipObj)
    538        for bipNode in bipNodes:
    539            if bipNode.name == bipObj.controller.rootName:
    540                bipNode.name = bipNode.name.lower()
    541                continue
    542
    543            bipNodeNameDict = bipNameTool.convert_to_dictionary(bipNode.name)
    544
    545            newNameDict = {}
    546            for namePartName, value in bipNodeNameDict.items():
    547                namePart = bipNameTool.get_name_part(namePartName)
    548                desc = namePart.get_description_by_value(value)
    549
    550                if namePartName == "RealName" or namePartName == "Index" or namePartName == "Nub":
    551                    newNameDict[namePartName] = value
    552                else:
    553                    newNameDict[namePartName] = self.name.get_name_part(namePartName).get_value_by_description(desc)
    554
    555            if newNameDict["Index"] == "" and self.name._has_digit(newNameDict["RealName"]):
    556                if "Finger" not in newNameDict["RealName"]:
    557                    splitedRealName = self.name._split_into_string_and_digit(newNameDict["RealName"])
    558                    newNameDict["RealName"] = splitedRealName[0]
    559                    newNameDict["Index"] = splitedRealName[1]
    560            if newNameDict["Nub"] == "" and bipNameTool.get_name_part_value_by_description("Nub", "Nub") in (newNameDict["RealName"]):
    561                newNameDict["RealName"] = newNameDict["RealName"].replace(bipNameTool.get_name_part_value_by_description("Nub", "Nub"), "")
    562                newNameDict["Nub"] = self.name.get_name_part_value_by_description("Nub", "Nub")
    563
    564            if newNameDict["RealName"] == "Forearm":
    565                newNameDict["RealName"] = "Lowerarm"
    566
    567            if newNameDict["RealName"] == "Spine" or newNameDict["RealName"] == "Neck":
    568                if newNameDict["Index"] == "":
    569                    newNameDict["Index"] = str(int(1)).zfill(self.name.get_padding_num())
    570                else:
    571                    newNameDict["Index"] = str(int(newNameDict["Index"]) + 1).zfill(self.name.get_padding_num())
    572
    573            newBipName = self.name.combine(newNameDict)
    574
    575            bipNode.name = newBipName.lower()
    576
    577        # 손가락 바꾸는 부분
    578        # 5개가 아닌 손가락은 지원하지 않음
    579        # 손가락 하나의 최대 링크는 3개
    580        indices = []
    581        if bipObj.controller.knuckles:
    582            pass
    583        else:
    584            indices = list(range(0, 15, 3))
    585
    586        fingerNum = bipObj.controller.fingers
    587        fingerLinkNum = bipObj.controller.fingerLinks
    588
    589        lFingersList = []
    590        rFingersList = []
    591
    592        for i in range(1, fingerNum+1):
    593            fingers = []
    594            for j in range(1, fingerLinkNum+1):
    595                linkIndex = (i-1)*fingerLinkNum + j
    596                fingerNode = rt.biped.getNode(bipObj.controller, rt.name("lFingers"), link=linkIndex)
    597                fingers.append(fingerNode)
    598            lFingersList.append(fingers)
    599        for i in range(1, fingerNum+1):
    600            fingers = []
    601            for j in range(1, fingerLinkNum+1):
    602                linkIndex = (i-1)*fingerLinkNum + j
    603                fingerNode = rt.biped.getNode(bipObj.controller, rt.name("rFingers"), link=linkIndex)
    604                fingers.append(fingerNode)
    605            rFingersList.append(fingers)
    606
    607        fingerName = ["thumb", "index", "middle", "ring", "pinky"]
    608
    609        for i, fingers in enumerate(lFingersList):
    610            for j, item in enumerate(fingers):
    611                item.name = self.name.replace_name_part("RealName", item.name, fingerName[i])
    612                item.name = self.name.replace_name_part("Index", item.name, str(j+1))
    613
    614            fingerNub = self.bone.get_every_children(fingers[-1])[0]
    615            fingerNub.name = self.name.replace_name_part("RealName", fingerNub.name, fingerName[i])
    616            fingerNub.name = self.name.remove_name_part("Index", fingerNub.name)
    617            fingerNub.name = self.name.replace_name_part("Nub", fingerNub.name, self.name.get_name_part_value_by_description("Nub", "Nub"))
    618
    619        for i, fingers in enumerate(rFingersList):
    620            for j, item in enumerate(fingers):
    621                item.name = self.name.replace_name_part("RealName", item.name, fingerName[i])
    622                item.name = self.name.replace_name_part("Index", item.name, str(j+1))
    623
    624            fingerNub = self.bone.get_every_children(fingers[-1])[0]
    625            fingerNub.name = self.name.replace_name_part("RealName", fingerNub.name, fingerName[i])
    626            fingerNub.name = self.name.remove_name_part("Index", fingerNub.name)
    627            fingerNub.name = self.name.replace_name_part("Nub", fingerNub.name, self.name.get_name_part_value_by_description("Nub", "Nub"))
    628
    629        # Toe 이름 바꾸는 부분
    630        lToesList = []
    631        rToesList = []
    632
    633        toeNum = bipObj.controller.toes
    634        toeLinkNum = bipObj.controller.toeLinks
    635
    636        # Use the same sequential indexing pattern as fingers
    637        for i in range(1, toeNum+1):
    638            toes = []
    639            for j in range(1, toeLinkNum+1):
    640                linkIndex = (i-1)*toeLinkNum + j
    641                toeNode = rt.biped.getNode(bipObj.controller, rt.name("lToes"), link=linkIndex)
    642                if toeNode:
    643                    toes.append(toeNode)
    644            if toes:
    645                lToesList.append(toes)
    646
    647        for i in range(1, toeNum+1):
    648            toes = []
    649            for j in range(1, toeLinkNum+1):
    650                linkIndex = (i-1)*toeLinkNum + j
    651                toeNode = rt.biped.getNode(bipObj.controller, rt.name("rToes"), link=linkIndex)
    652                if toeNode:
    653                    toes.append(toeNode)
    654            if toes:
    655                rToesList.append(toes)
    656
    657        for i, toes in enumerate(lToesList):
    658            for j, item in enumerate(toes):
    659                item.name = self.name.replace_name_part("RealName", item.name, "ball"+str(i+1))
    660                item.name = self.name.replace_name_part("Index", item.name, str(j+1))
    661
    662            toeNub = self.bone.get_every_children(toes[-1])[0]
    663            toeNub.name = self.name.replace_name_part("RealName", toeNub.name, "ball"+str(i+1))
    664            toeNub.name = self.name.remove_name_part("Index", toeNub.name)
    665            toeNub.name = self.name.replace_name_part("Nub", toeNub.name, self.name.get_name_part_value_by_description("Nub", "Nub"))
    666
    667        for i, toes in enumerate(rToesList):
    668            for j, item in enumerate(toes):
    669                item.name = self.name.replace_name_part("RealName", item.name, "ball"+str(i+1))
    670                item.name = self.name.replace_name_part("Index", item.name, str(j+1))
    671
    672            toeNub = self.bone.get_every_children(toes[-1])[0]
    673            toeNub.name = self.name.replace_name_part("RealName", toeNub.name, "ball"+str(i+1))
    674            toeNub.name = self.name.remove_name_part("Index", toeNub.name)
    675            toeNub.name = self.name.replace_name_part("Nub", toeNub.name, self.name.get_name_part_value_by_description("Nub", "Nub"))
    676
    677        if toeNum == 1:
    678            if toeLinkNum == 1:
    679                lToesList[0][0].name = self.name.replace_name_part("RealName", lToesList[0][0].name, "ball")
    680                lToesList[0][0].name = self.name.remove_name_part("Index", lToesList[0][0].name)
    681            else:
    682                for i, item in enumerate(lToesList[0]):
    683                    item.name = self.name.replace_name_part("RealName", item.name, "ball")
    684                    item.name = self.name.replace_name_part("Index", item.name, str(i+1))
    685
    686            toeNub = self.bone.get_every_children(lToesList[0][-1])[0]
    687            toeNub.name = self.name.replace_name_part("RealName", toeNub.name, "ball")
    688            toeNub.name = self.name.remove_name_part("Index", toeNub.name)
    689            toeNub.name = self.name.replace_name_part("Nub", toeNub.name, self.name.get_name_part_value_by_description("Nub", "Nub"))
    690
    691            if toeLinkNum == 1:
    692                rToesList[0][0].name = self.name.replace_name_part("RealName", lToesList[0][0].name, "ball")
    693                rToesList[0][0].name = self.name.remove_name_part("Index", lToesList[0][0].name)
    694            else:
    695                for i, item in enumerate(rToesList[0]):
    696                    item.name = self.name.replace_name_part("RealName", item.name, "ball")
    697                    item.name = self.name.replace_name_part("Index", item.name, str(i+1))
    698
    699            toeNub = self.bone.get_every_children(rToesList[0][-1])[0]
    700            toeNub.name = self.name.replace_name_part("RealName", toeNub.name, "ball")
    701            toeNub.name = self.name.remove_name_part("Index", toeNub.name)
    702            toeNub.name = self.name.replace_name_part("Nub", toeNub.name, self.name.get_name_part_value_by_description("Nub", "Nub"))
    703
    704        return True

Biped 객체 관련 기능을 제공하는 클래스. MAXScript의 _Bip 구조체 개념을 Python으로 재구현한 클래스이며, 3ds Max의 기능들을 pymxs API를 통해 제어합니다.

Bip(animService=None, nameService=None, boneService=None) View Source

    28    def __init__(self, animService=None, nameService=None, boneService=None):
    29        """
    30        클래스 초기화
    31
    32        Args:
    33            animService: Anim 서비스 인스턴스 (제공되지 않으면 새로 생성)
    34            nameService: Name 서비스 인스턴스 (제공되지 않으면 새로 생성)
    35            boneService: Bone 서비스 인스턴스 (제공되지 않으면 새로 생성)
    36        """
    37        self.anim = animService if animService else Anim()
    38        self.name = nameService if nameService else Name()
    39        self.bone = boneService if boneService else Bone(nameService=self.name, animService=self.anim) # Pass potentially new instances

클래스 초기화

Args: animService: Anim 서비스 인스턴스 (제공되지 않으면 새로 생성) nameService: Name 서비스 인스턴스 (제공되지 않으면 새로 생성) boneService: Bone 서비스 인스턴스 (제공되지 않으면 새로 생성)

anim
name
bone
def get_bips(self): View Source

    41    def get_bips(self):
    42        """
    43        씬 내의 모든 Biped_Object를 찾음
    44
    45        Returns:
    46            Biped_Object 리스트
    47        """
    48        return [obj for obj in rt.objects if rt.isKindOf(obj, rt.Biped_Object)]

씬 내의 모든 Biped_Object를 찾음

Returns: Biped_Object 리스트

def get_coms_name(self): View Source

    50    def get_coms_name(self):
    51        """
    52        씬 내 모든 Biped COM(Center of Mass)의 이름 리스트 반환
    53
    54        Returns:
    55            Biped COM 이름 리스트
    56        """
    57        bips = self.get_bips()
    58        bipComsName = []
    59
    60        for obj in bips:
    61            rootName = obj.controller.rootName
    62            if rootName not in bipComsName:
    63                bipComsName.append(rootName)
    64
    65        return bipComsName

씬 내 모든 Biped COM(Center of Mass)의 이름 리스트 반환

Returns: Biped COM 이름 리스트

def get_coms(self): View Source

    67    def get_coms(self):
    68        """
    69        씬 내 모든 Biped COM(Center of Mass) 객체 리스트 반환
    70
    71        Returns:
    72            Biped COM 객체 리스트
    73        """
    74        bips = self.get_bips()
    75        bipComs = []
    76
    77        for obj in bips:
    78            rootNode = obj.controller.rootNode
    79            if rootNode not in bipComs:
    80                bipComs.append(rootNode)
    81
    82        return bipComs

씬 내 모든 Biped COM(Center of Mass) 객체 리스트 반환

Returns: Biped COM 객체 리스트

def is_biped_object(self, inObj): View Source

    84    def is_biped_object(self, inObj):
    85        """
    86        객체가 Biped 관련 객체인지 확인
    87
    88        Args:
    89            inObj: 확인할 객체
    90
    91        Returns:
    92            Biped 관련 객체이면 True, 아니면 False
    93        """
    94        return (rt.classOf(inObj.controller) == rt.BipSlave_control or
    95                rt.classOf(inObj.controller) == rt.Footsteps or
    96                rt.classOf(inObj.controller) == rt.Vertical_Horizontal_Turn)

객체가 Biped 관련 객체인지 확인

Args: inObj: 확인할 객체

Returns: Biped 관련 객체이면 True, 아니면 False

def get_com(self, inBip): View Source

     98    def get_com(self, inBip):
     99        """
    100        Biped 객체의 COM(Center of Mass) 반환
    101
    102        Args:
    103            inBip: COM을 찾을 Biped 객체
    104
    105        Returns:
    106            Biped의 COM 객체 또는 None
    107        """
    108        if self.is_biped_object(inBip):
    109            return inBip.controller.rootNode
    110        return None

Biped 객체의 COM(Center of Mass) 반환

Args: inBip: COM을 찾을 Biped 객체

Returns: Biped의 COM 객체 또는 None

def get_all(self, inBip): View Source

    112    def get_all(self, inBip):
    113        """
    114        Biped와 관련된 모든 객체 반환
    115
    116        Args:
    117            inBip: 기준 Biped 객체
    118
    119        Returns:
    120            Biped 관련 모든 객체 리스트
    121        """
    122        returnVal = []
    123
    124        if self.is_biped_object(inBip):
    125            root = self.get_com(inBip)
    126            allNodes = [root]
    127            returnVal = [root]
    128
    129            for obj in allNodes:
    130                for child in obj.children:
    131                    if child not in allNodes:
    132                        allNodes.append(child)
    133                    if self.is_biped_object(child) and child not in returnVal:
    134                        returnVal.append(child)
    135
    136                if obj.parent is not None:
    137                    if obj.parent not in allNodes:
    138                        allNodes.append(obj.parent)
    139                    if self.is_biped_object(obj.parent) and obj.parent not in returnVal:
    140                        returnVal.append(obj.parent)
    141
    142        return returnVal

Biped와 관련된 모든 객체 반환

Args: inBip: 기준 Biped 객체

Returns: Biped 관련 모든 객체 리스트

def get_nodes(self, inBip): View Source

    144    def get_nodes(self, inBip):
    145        """
    146        Biped의 실제 노드만 반환 (더미나 Footstep은 제외)
    147
    148        Args:
    149            inBip: 기준 Biped 객체
    150
    151        Returns:
    152            Biped의 노드 객체 리스트
    153        """
    154        returnVal = []
    155
    156        if self.is_biped_object(inBip):
    157            root = self.get_com(inBip)
    158            allNodes = [root]
    159            returnVal = [root]
    160
    161            for obj in allNodes:
    162                for child in obj.children:
    163                    if rt.classOf(child) != rt.Dummy and rt.classOf(child.controller) != rt.Footsteps:
    164                        if child not in allNodes:
    165                            allNodes.append(child)
    166                        if self.is_biped_object(child) and child not in returnVal:
    167                            returnVal.append(child)
    168
    169                if obj.parent is not None:
    170                    if rt.classOf(obj.parent) != rt.Dummy and rt.classOf(obj.parent.controller) != rt.Footsteps:
    171                        if obj.parent not in allNodes:
    172                            allNodes.append(obj.parent)
    173                        if self.is_biped_object(obj.parent) and obj.parent not in returnVal:
    174                            returnVal.append(obj.parent)
    175
    176        return returnVal

Biped의 실제 노드만 반환 (더미나 Footstep은 제외)

Args: inBip: 기준 Biped 객체

Returns: Biped의 노드 객체 리스트

def get_dummy_and_footstep(self, inBip): View Source

    178    def get_dummy_and_footstep(self, inBip):
    179        """
    180        Biped의 더미와 Footstep 객체만 반환
    181
    182        Args:
    183            inBip: 기준 Biped 객체
    184
    185        Returns:
    186            더미와 Footstep 객체 리스트
    187        """
    188        returnVal = []
    189
    190        if self.is_biped_object(inBip):
    191            bipArray = self.get_all(inBip)
    192            returnVal = [item for item in bipArray if rt.classOf(item) == rt.Dummy or rt.classOf(item.controller) == rt.Footsteps]
    193
    194        return returnVal

Biped의 더미와 Footstep 객체만 반환

Args: inBip: 기준 Biped 객체

Returns: 더미와 Footstep 객체 리스트

def get_all_grouped_nodes(self, inBip): View Source

    196    def get_all_grouped_nodes(self, inBip):
    197        """
    198        Biped의 체인 이름으로 노드 반환
    199
    200        Args:
    201            inBip: 기준 Biped 객체
    202
    203        Returns:
    204            해당 체인에 속하는 Biped 노드 리스트
    205        """
    206        # Define node categories with their corresponding index numbers
    207        NODE_CATEGORIES = {
    208            1: "lArm",
    209            2: "rArm",
    210            3: "lFingers",
    211            4: "rFingers",
    212            5: "lLeg",
    213            6: "rLeg",
    214            7: "lToes",
    215            8: "rToes",
    216            9: "spine",
    217            10: "tail",
    218            11: "head",
    219            12: "pelvis",
    220            17: "neck",
    221            18: "pony1",
    222            19: "pony2",
    223            20: "prop1",
    224            21: "prop2",
    225            22: "prop3"
    226        }
    227
    228        # Initialize node collections dictionary
    229        nodes = {category: [] for category in NODE_CATEGORIES.values()}
    230
    231        com = inBip.controller.rootNode
    232        if rt.classOf(inBip) != rt.Biped_Object:
    233            return nodes
    234
    235        nn = rt.biped.maxNumNodes(com)
    236        nl = rt.biped.maxNumLinks(com)
    237
    238        # Collect nodes by category
    239        for i in range(1, nn + 1):
    240            if i not in NODE_CATEGORIES:
    241                continue
    242
    243            category = NODE_CATEGORIES[i]
    244            anode = rt.biped.getNode(com, i)
    245
    246            if not anode:
    247                continue
    248
    249            for j in range(1, nl + 1):
    250                alink = rt.biped.getNode(com, i, link=j)
    251                if alink:
    252                    nodes[category].append(alink)
    253
    254        return nodes

Biped의 체인 이름으로 노드 반환

Args: inBip: 기준 Biped 객체

Returns: 해당 체인에 속하는 Biped 노드 리스트

def get_grouped_nodes(self, inBip, inGroupName): View Source

    256    def get_grouped_nodes(self, inBip,inGroupName):
    257        """
    258        Biped의 체인 이름으로 노드 반환
    259
    260        Args:
    261            inBip: 기준 Biped 객체
    262            inGroupName: 체인 이름 (예: "lArm", "rLeg" 등)
    263
    264        Returns:
    265            해당 체인에 속하는 Biped 노드 리스트
    266        """
    267        nodes = self.get_all_grouped_nodes(inBip)
    268
    269        if inGroupName in nodes:
    270            return nodes[inGroupName]
    271
    272        return []

Biped의 체인 이름으로 노드 반환

Args: inBip: 기준 Biped 객체 inGroupName: 체인 이름 (예: "lArm", "rLeg" 등)

Returns: 해당 체인에 속하는 Biped 노드 리스트

def is_left_node(self, inNode): View Source

    274    def is_left_node(self, inNode):
    275        """
    276        노드가 왼쪽인지 확인
    277
    278        Args:
    279            inNode: 확인할 노드 객체
    280
    281        Returns:
    282            왼쪽 노드이면 True, 아니면 False
    283        """
    284        if rt.classOf(inNode) != rt.Biped_Object:
    285            return False
    286        com = self.get_com(inNode)
    287        nodes = self.get_all_grouped_nodes(com)
    288
    289        categories = ["lArm", "lFingers", "lLeg", "lToes"]
    290        for category in categories:
    291            groupedNodes = nodes[category]
    292            if inNode in groupedNodes:
    293                return True
    294
    295        return False

노드가 왼쪽인지 확인

Args: inNode: 확인할 노드 객체

Returns: 왼쪽 노드이면 True, 아니면 False

def is_right_node(self, inNode): View Source

    297    def is_right_node(self, inNode):
    298        """
    299        노드가 오른쪽인지 확인
    300
    301        Args:
    302            inNode: 확인할 노드 객체
    303
    304        Returns:
    305            오른쪽 노드이면 True, 아니면 False
    306        """
    307        if rt.classOf(inNode) != rt.Biped_Object:
    308            return False
    309        com = self.get_com(inNode)
    310        nodes = self.get_all_grouped_nodes(com)
    311
    312        categories = ["rArm", "rFingers", "rLeg", "rToes"]
    313        for category in categories:
    314            groupedNodes = nodes[category]
    315            if inNode in groupedNodes:
    316                return True
    317
    318        return False

노드가 오른쪽인지 확인

Args: inNode: 확인할 노드 객체

Returns: 오른쪽 노드이면 True, 아니면 False

def get_nodes_by_skeleton_order(self, inBip): View Source

    320    def get_nodes_by_skeleton_order(self, inBip):
    321        """
    322        스켈레톤 순서대로 Biped 노드 반환
    323
    324        Args:
    325            inBip: 기준 Biped 객체
    326
    327        Returns:
    328            순서대로 정렬된 Biped 노드 리스트
    329        """
    330        nodes = self.get_all_grouped_nodes(inBip)
    331
    332        # Define the order of categories in final array
    333        ORDER = [
    334            "head", "pelvis", "lArm", "lFingers", "lLeg", "lToes", "neck",
    335            "rArm", "rFingers", "rLeg", "rToes", "spine", "tail",
    336            "pony1", "pony2", "prop1", "prop2", "prop3"
    337        ]
    338
    339        # Build final array in the desired order
    340        bipNodeArray = []
    341        for category in ORDER:
    342            bipNodeArray.extend(nodes[category])
    343
    344        return bipNodeArray

스켈레톤 순서대로 Biped 노드 반환

Args: inBip: 기준 Biped 객체

Returns: 순서대로 정렬된 Biped 노드 리스트

def load_bip_file(self, inBipRoot, inFile): View Source

    346    def load_bip_file(self, inBipRoot, inFile):
    347        """
    348        Biped BIP 파일 로드
    349
    350        Args:
    351            inBipRoot: 로드 대상 Biped 루트 노드
    352            inFile: 로드할 BIP 파일 경로
    353        """
    354        bipNodeArray = self.get_all(inBipRoot)
    355
    356        inBipRoot.controller.figureMode = False
    357        rt.biped.loadBipFile(inBipRoot.controller, inFile)
    358        inBipRoot.controller.figureMode = True
    359        inBipRoot.controller.figureMode = False
    360
    361        keyRange = []
    362        for i in range(1, len(bipNodeArray)):
    363            if bipNodeArray[i].controller.keys.count != 0 and bipNodeArray[i].controller.keys.count != -1:
    364                keyTime = bipNodeArray[i].controller.keys[bipNodeArray[i].controller.keys.count - 1].time
    365                if keyTime not in keyRange:
    366                    keyRange.append(keyTime)
    367
    368        if keyRange and max(keyRange) != 0:
    369            rt.animationRange = rt.interval(0, max(keyRange))
    370            rt.sliderTime = 0

Biped BIP 파일 로드

Args: inBipRoot: 로드 대상 Biped 루트 노드 inFile: 로드할 BIP 파일 경로

def load_fig_file(self, inBipRoot, inFile): View Source

    372    def load_fig_file(self, inBipRoot, inFile):
    373        """
    374        Biped FIG 파일 로드
    375
    376        Args:
    377            inBipRoot: 로드 대상 Biped 루트 노드
    378            inFile: 로드할 FIG 파일 경로
    379        """
    380        inBipRoot.controller.figureMode = False
    381        inBipRoot.controller.figureMode = True
    382        rt.biped.LoadFigFile(inBipRoot.controller, inFile)
    383        inBipRoot.controller.figureMode = False

Biped FIG 파일 로드

Args: inBipRoot: 로드 대상 Biped 루트 노드 inFile: 로드할 FIG 파일 경로

def save_fig_file(self, inBipRoot, fileName): View Source

    385    def save_fig_file(self, inBipRoot, fileName):
    386        """
    387        Biped FIG 파일 저장
    388
    389        Args:
    390            inBipRoot: 저장 대상 Biped 루트 노드
    391            fileName: 저장할 FIG 파일 경로
    392        """
    393        inBipRoot.controller.figureMode = False
    394        inBipRoot.controller.figureMode = True
    395        rt.biped.saveFigFile(inBipRoot.controller, fileName)

Biped FIG 파일 저장

Args: inBipRoot: 저장 대상 Biped 루트 노드 fileName: 저장할 FIG 파일 경로

def turn_on_figure_mode(self, inBipRoot): View Source

    397    def turn_on_figure_mode(self, inBipRoot):
    398        """
    399        Biped Figure 모드 켜기
    400
    401        Args:
    402            inBipRoot: 대상 Biped 객체
    403        """
    404        inBipRoot.controller.figureMode = True

Biped Figure 모드 켜기

Args: inBipRoot: 대상 Biped 객체

def turn_off_figure_mode(self, inBipRoot): View Source

    406    def turn_off_figure_mode(self, inBipRoot):
    407        """
    408        Biped Figure 모드 끄기
    409
    410        Args:
    411            inBipRoot: 대상 Biped 객체
    412        """
    413        inBipRoot.controller.figureMode = False

Biped Figure 모드 끄기

Args: inBipRoot: 대상 Biped 객체

def delete_copy_collection(self, inBipRoot, inName): View Source

    415    def delete_copy_collection(self, inBipRoot, inName):
    416        """
    417        Biped 복사 컬렉션 삭제
    418
    419        Args:
    420            inBipRoot: 대상 Biped 객체
    421            inName: 삭제할 컬렉션 이름
    422        """
    423        if self.is_biped_object(inBipRoot):
    424            colNum = rt.biped.numCopyCollections(inBipRoot.controller)
    425            if colNum > 0:
    426                for i in range(1, colNum + 1):
    427                    if rt.biped.getCopyCollection(inBipRoot.controller, i).name == inName:
    428                        rt.biped.deleteCopyCollection(inBipRoot.controller, i)
    429                        break

Biped 복사 컬렉션 삭제

Args: inBipRoot: 대상 Biped 객체 inName: 삭제할 컬렉션 이름

def delete_all_copy_collection(self, inBipRoot): View Source

    431    def delete_all_copy_collection(self, inBipRoot):
    432        """
    433        Biped 모든 복사 컬렉션 삭제
    434
    435        Args:
    436            inBipRoot: 대상 Biped 객체
    437        """
    438        if self.is_biped_object(inBipRoot):
    439            colNum = rt.biped.numCopyCollections(inBipRoot.controller)
    440            if colNum > 0:
    441                rt.biped.deleteAllCopyCollections(inBipRoot.controller)

Biped 모든 복사 컬렉션 삭제

Args: inBipRoot: 대상 Biped 객체

def link_base_skeleton(self, skinBoneBaseName='b'): View Source

    443    def link_base_skeleton(self, skinBoneBaseName="b"):
    444        """
    445        기본 스켈레톤 링크 (Biped와 일반 뼈대 연결)
    446        """
    447        rt.setWaitCursor()
    448
    449        bipSkel = self.get_bips()
    450        baseSkel = [None] * len(bipSkel)
    451
    452        for i in range(len(bipSkel)):
    453            baseSkeletonName = self.name.replace_base(bipSkel[i].name, skinBoneBaseName)
    454            baseSkeletonName = self.name.replace_filteringChar(baseSkeletonName, "_")
    455            baseSkelObj = rt.getNodeByName(baseSkeletonName)
    456            if rt.isValidObj(baseSkelObj):
    457                baseSkel[i] = baseSkelObj
    458
    459            self.anim.save_xform(bipSkel[i])
    460            self.anim.set_xform(bipSkel[i])
    461
    462            self.anim.save_xform(baseSkel[i])
    463            self.anim.set_xform(baseSkel[i])
    464
    465        for i in range(len(baseSkel)):
    466            if baseSkel[i] is not None:
    467                baseSkel[i].scale.controller = rt.scaleXYZ()
    468                baseSkel[i].controller = rt.link_constraint()
    469
    470                self.anim.set_xform([baseSkel[i]], space="World")
    471                baseSkel[i].transform.controller.AddTarget(bipSkel[i], 0)
    472
    473        for i in range(len(baseSkel)):
    474            if baseSkel[i] is not None:
    475                baseSkel[i].boneEnable = True
    476
    477        rt.setArrowCursor()

기본 스켈레톤 링크 (Biped와 일반 뼈대 연결)

def unlink_base_skeleton(self, skinBoneBaseName='b'): View Source

    479    def unlink_base_skeleton(self, skinBoneBaseName="b"):
    480        """
    481        기본 스켈레톤 링크 해제
    482        """
    483        rt.setWaitCursor()
    484
    485        bipComs = self.get_coms()
    486        allBips = self.get_nodes(bipComs[0])
    487        bipSkel = [item for item in allBips if item != bipComs[0]]
    488        baseSkel = [None] * len(bipSkel)
    489
    490        for i in range(len(bipSkel)):
    491            baseSkeletonName = self.name.replace_name_part("Base", bipSkel[i].name, skinBoneBaseName)
    492            baseSkeletonName = self.name.replace_filtering_char(baseSkeletonName, "_")
    493            print("baseSkeletonName", baseSkeletonName)
    494            baseSkelObj = rt.getNodeByName(baseSkeletonName)
    495            print("baseSkelObj", baseSkelObj)
    496            if rt.isValidObj(baseSkelObj):
    497                baseSkel[i] = baseSkelObj
    498
    499            self.anim.save_xform(bipSkel[i])
    500            self.anim.set_xform(bipSkel[i])
    501
    502            self.anim.save_xform(baseSkel[i])
    503            self.anim.set_xform(baseSkel[i])
    504
    505        for i in range(len(baseSkel)):
    506            if baseSkel[i] is not None:
    507                baseSkel[i].controller = rt.prs()
    508                self.anim.set_xform([baseSkel[i]], space="World")
    509
    510        for i in range(len(baseSkel)):
    511            if baseSkel[i] is not None:
    512                baseSkel[i].boneEnable = True
    513
    514        rt.setArrowCursor()

기본 스켈레톤 링크 해제

def convert_name_for_ue5(self, inBipRoot, inBipNameConfigFile): View Source

    516    def convert_name_for_ue5(self, inBipRoot, inBipNameConfigFile):
    517        """
    518        Biped 이름을 UE5에 맞게 변환
    519
    520        Args:
    521            inBipRoot: 변환할 Biped 객체
    522
    523        Returns:
    524            변환된 Biped 객체
    525        """
    526        bipComs = self.get_coms()
    527
    528        if len(bipComs) > 1:
    529            rt.messageBox("Please select only one Biped object.")
    530            return False
    531
    532        from pyjallib.max.name import Name
    533
    534        bipNameTool = Name(configPath=inBipNameConfigFile)
    535
    536        bipObj = bipComs[0]
    537        bipNodes = self.get_all(bipObj)
    538        for bipNode in bipNodes:
    539            if bipNode.name == bipObj.controller.rootName:
    540                bipNode.name = bipNode.name.lower()
    541                continue
    542
    543            bipNodeNameDict = bipNameTool.convert_to_dictionary(bipNode.name)
    544
    545            newNameDict = {}
    546            for namePartName, value in bipNodeNameDict.items():
    547                namePart = bipNameTool.get_name_part(namePartName)
    548                desc = namePart.get_description_by_value(value)
    549
    550                if namePartName == "RealName" or namePartName == "Index" or namePartName == "Nub":
    551                    newNameDict[namePartName] = value
    552                else:
    553                    newNameDict[namePartName] = self.name.get_name_part(namePartName).get_value_by_description(desc)
    554
    555            if newNameDict["Index"] == "" and self.name._has_digit(newNameDict["RealName"]):
    556                if "Finger" not in newNameDict["RealName"]:
    557                    splitedRealName = self.name._split_into_string_and_digit(newNameDict["RealName"])
    558                    newNameDict["RealName"] = splitedRealName[0]
    559                    newNameDict["Index"] = splitedRealName[1]
    560            if newNameDict["Nub"] == "" and bipNameTool.get_name_part_value_by_description("Nub", "Nub") in (newNameDict["RealName"]):
    561                newNameDict["RealName"] = newNameDict["RealName"].replace(bipNameTool.get_name_part_value_by_description("Nub", "Nub"), "")
    562                newNameDict["Nub"] = self.name.get_name_part_value_by_description("Nub", "Nub")
    563
    564            if newNameDict["RealName"] == "Forearm":
    565                newNameDict["RealName"] = "Lowerarm"
    566
    567            if newNameDict["RealName"] == "Spine" or newNameDict["RealName"] == "Neck":
    568                if newNameDict["Index"] == "":
    569                    newNameDict["Index"] = str(int(1)).zfill(self.name.get_padding_num())
    570                else:
    571                    newNameDict["Index"] = str(int(newNameDict["Index"]) + 1).zfill(self.name.get_padding_num())
    572
    573            newBipName = self.name.combine(newNameDict)
    574
    575            bipNode.name = newBipName.lower()
    576
    577        # 손가락 바꾸는 부분
    578        # 5개가 아닌 손가락은 지원하지 않음
    579        # 손가락 하나의 최대 링크는 3개
    580        indices = []
    581        if bipObj.controller.knuckles:
    582            pass
    583        else:
    584            indices = list(range(0, 15, 3))
    585
    586        fingerNum = bipObj.controller.fingers
    587        fingerLinkNum = bipObj.controller.fingerLinks
    588
    589        lFingersList = []
    590        rFingersList = []
    591
    592        for i in range(1, fingerNum+1):
    593            fingers = []
    594            for j in range(1, fingerLinkNum+1):
    595                linkIndex = (i-1)*fingerLinkNum + j
    596                fingerNode = rt.biped.getNode(bipObj.controller, rt.name("lFingers"), link=linkIndex)
    597                fingers.append(fingerNode)
    598            lFingersList.append(fingers)
    599        for i in range(1, fingerNum+1):
    600            fingers = []
    601            for j in range(1, fingerLinkNum+1):
    602                linkIndex = (i-1)*fingerLinkNum + j
    603                fingerNode = rt.biped.getNode(bipObj.controller, rt.name("rFingers"), link=linkIndex)
    604                fingers.append(fingerNode)
    605            rFingersList.append(fingers)
    606
    607        fingerName = ["thumb", "index", "middle", "ring", "pinky"]
    608
    609        for i, fingers in enumerate(lFingersList):
    610            for j, item in enumerate(fingers):
    611                item.name = self.name.replace_name_part("RealName", item.name, fingerName[i])
    612                item.name = self.name.replace_name_part("Index", item.name, str(j+1))
    613
    614            fingerNub = self.bone.get_every_children(fingers[-1])[0]
    615            fingerNub.name = self.name.replace_name_part("RealName", fingerNub.name, fingerName[i])
    616            fingerNub.name = self.name.remove_name_part("Index", fingerNub.name)
    617            fingerNub.name = self.name.replace_name_part("Nub", fingerNub.name, self.name.get_name_part_value_by_description("Nub", "Nub"))
    618
    619        for i, fingers in enumerate(rFingersList):
    620            for j, item in enumerate(fingers):
    621                item.name = self.name.replace_name_part("RealName", item.name, fingerName[i])
    622                item.name = self.name.replace_name_part("Index", item.name, str(j+1))
    623
    624            fingerNub = self.bone.get_every_children(fingers[-1])[0]
    625            fingerNub.name = self.name.replace_name_part("RealName", fingerNub.name, fingerName[i])
    626            fingerNub.name = self.name.remove_name_part("Index", fingerNub.name)
    627            fingerNub.name = self.name.replace_name_part("Nub", fingerNub.name, self.name.get_name_part_value_by_description("Nub", "Nub"))
    628
    629        # Toe 이름 바꾸는 부분
    630        lToesList = []
    631        rToesList = []
    632
    633        toeNum = bipObj.controller.toes
    634        toeLinkNum = bipObj.controller.toeLinks
    635
    636        # Use the same sequential indexing pattern as fingers
    637        for i in range(1, toeNum+1):
    638            toes = []
    639            for j in range(1, toeLinkNum+1):
    640                linkIndex = (i-1)*toeLinkNum + j
    641                toeNode = rt.biped.getNode(bipObj.controller, rt.name("lToes"), link=linkIndex)
    642                if toeNode:
    643                    toes.append(toeNode)
    644            if toes:
    645                lToesList.append(toes)
    646
    647        for i in range(1, toeNum+1):
    648            toes = []
    649            for j in range(1, toeLinkNum+1):
    650                linkIndex = (i-1)*toeLinkNum + j
    651                toeNode = rt.biped.getNode(bipObj.controller, rt.name("rToes"), link=linkIndex)
    652                if toeNode:
    653                    toes.append(toeNode)
    654            if toes:
    655                rToesList.append(toes)
    656
    657        for i, toes in enumerate(lToesList):
    658            for j, item in enumerate(toes):
    659                item.name = self.name.replace_name_part("RealName", item.name, "ball"+str(i+1))
    660                item.name = self.name.replace_name_part("Index", item.name, str(j+1))
    661
    662            toeNub = self.bone.get_every_children(toes[-1])[0]
    663            toeNub.name = self.name.replace_name_part("RealName", toeNub.name, "ball"+str(i+1))
    664            toeNub.name = self.name.remove_name_part("Index", toeNub.name)
    665            toeNub.name = self.name.replace_name_part("Nub", toeNub.name, self.name.get_name_part_value_by_description("Nub", "Nub"))
    666
    667        for i, toes in enumerate(rToesList):
    668            for j, item in enumerate(toes):
    669                item.name = self.name.replace_name_part("RealName", item.name, "ball"+str(i+1))
    670                item.name = self.name.replace_name_part("Index", item.name, str(j+1))
    671
    672            toeNub = self.bone.get_every_children(toes[-1])[0]
    673            toeNub.name = self.name.replace_name_part("RealName", toeNub.name, "ball"+str(i+1))
    674            toeNub.name = self.name.remove_name_part("Index", toeNub.name)
    675            toeNub.name = self.name.replace_name_part("Nub", toeNub.name, self.name.get_name_part_value_by_description("Nub", "Nub"))
    676
    677        if toeNum == 1:
    678            if toeLinkNum == 1:
    679                lToesList[0][0].name = self.name.replace_name_part("RealName", lToesList[0][0].name, "ball")
    680                lToesList[0][0].name = self.name.remove_name_part("Index", lToesList[0][0].name)
    681            else:
    682                for i, item in enumerate(lToesList[0]):
    683                    item.name = self.name.replace_name_part("RealName", item.name, "ball")
    684                    item.name = self.name.replace_name_part("Index", item.name, str(i+1))
    685
    686            toeNub = self.bone.get_every_children(lToesList[0][-1])[0]
    687            toeNub.name = self.name.replace_name_part("RealName", toeNub.name, "ball")
    688            toeNub.name = self.name.remove_name_part("Index", toeNub.name)
    689            toeNub.name = self.name.replace_name_part("Nub", toeNub.name, self.name.get_name_part_value_by_description("Nub", "Nub"))
    690
    691            if toeLinkNum == 1:
    692                rToesList[0][0].name = self.name.replace_name_part("RealName", lToesList[0][0].name, "ball")
    693                rToesList[0][0].name = self.name.remove_name_part("Index", lToesList[0][0].name)
    694            else:
    695                for i, item in enumerate(rToesList[0]):
    696                    item.name = self.name.replace_name_part("RealName", item.name, "ball")
    697                    item.name = self.name.replace_name_part("Index", item.name, str(i+1))
    698
    699            toeNub = self.bone.get_every_children(rToesList[0][-1])[0]
    700            toeNub.name = self.name.replace_name_part("RealName", toeNub.name, "ball")
    701            toeNub.name = self.name.remove_name_part("Index", toeNub.name)
    702            toeNub.name = self.name.replace_name_part("Nub", toeNub.name, self.name.get_name_part_value_by_description("Nub", "Nub"))
    703
    704        return True

Biped 이름을 UE5에 맞게 변환

Args: inBipRoot: 변환할 Biped 객체

Returns: 변환된 Biped 객체

class Skin: View Source

     24class Skin:
     25    """
     26    고급 스킨 관련 기능을 제공하는 클래스.
     27    MAXScript의 ODC_Char_Skin 구조체 개념을 Python으로 재구현한 클래스이며,
     28    3ds Max의 기능들을 pymxs API를 통해 제어합니다.
     29    """
     30
     31    def __init__(self):
     32        """
     33        클래스 초기화
     34        """
     35        self.skin_match_list = []
     36
     37    def has_skin(self, obj=None):
     38        """
     39        객체에 스킨 모디파이어가 있는지 확인
     40
     41        Args:
     42            obj: 확인할 객체 (기본값: 현재 선택된 객체)
     43
     44        Returns:
     45            True: 스킨 모디파이어가 있는 경우
     46            False: 없는 경우
     47        """
     48        if obj is None:
     49            if len(rt.selection) > 0:
     50                obj = rt.selection[0]
     51            else:
     52                return False
     53
     54        # 객체의 모든 모디파이어를 검사하여 Skin 모디파이어가 있는지 확인
     55        for mod in obj.modifiers:
     56            if rt.classOf(mod) == rt.Skin:
     57                return True
     58        return False
     59
     60    def is_valid_bone(self, inNode):
     61        """
     62        노드가 유효한 스킨 본인지 확인
     63
     64        Args:
     65            inNode: 확인할 노드
     66
     67        Returns:
     68            True: 유효한 본인 경우
     69            False: 아닌 경우
     70        """
     71        return (rt.superClassOf(inNode) == rt.GeometryClass or
     72                rt.classOf(inNode) == rt.BoneGeometry or
     73                rt.superClassOf(inNode) == rt.Helper)
     74
     75    def get_skin_mod(self, obj=None):
     76        """
     77        객체의 스킨 모디파이어 배열 반환
     78
     79        Args:
     80            obj: 모디파이어를 가져올 객체 (기본값: 현재 선택된 객체)
     81
     82        Returns:
     83            스킨 모디파이어 배열
     84        """
     85        if obj is None:
     86            if len(rt.selection) > 0:
     87                obj = rt.selection[0]
     88            else:
     89                return []
     90
     91        return [mod for mod in obj.modifiers if rt.classOf(mod) == rt.Skin]
     92
     93    def bind_skin(self, obj, bone_array):
     94        """
     95        객체에 스킨 모디파이어 바인딩
     96
     97        Args:
     98            obj: 바인딩할 객체
     99            bone_array: 바인딩할 본 배열
    100
    101        Returns:
    102            True: 성공한 경우
    103            False: 실패한 경우
    104        """
    105        if obj is None or len(bone_array) < 1:
    106            print("Select at least 1 influence and an object.")
    107            return False
    108
    109        # Switch to modify mode
    110        rt.execute("max modify mode")
    111
    112        # Check if the object is valid for skinning
    113        if rt.superClassOf(obj) != rt.GeometryClass:
    114            print(f"{obj.name} must be 'Edit_Mesh' or 'Edit_Poly'.")
    115            return False
    116
    117        # Add skin modifier
    118        objmod = rt.Skin()
    119        rt.addModifier(obj, objmod)
    120        rt.select(obj)
    121
    122        # Add bones to skin modifier
    123        wgt = 1.0
    124        for each in bone_array:
    125            rt.skinOps.addBone(objmod, each, wgt)
    126
    127        # Set skin modifier options
    128        objmod.filter_vertices = True
    129        objmod.filter_envelopes = False
    130        objmod.filter_cross_sections = True
    131        objmod.enableDQ = False
    132        objmod.bone_Limit = 8
    133        objmod.colorAllWeights = True
    134        objmod.showNoEnvelopes = True
    135
    136        return True
    137
    138    def optimize_skin(self, skin_mod, bone_limit=8, skin_tolerance=0.01):
    139        """
    140        스킨 모디파이어 최적화
    141
    142        Args:
    143            skin_mod: 스킨 모디파이어
    144            bone_limit: 본 제한 수 (기본값: 8)
    145            skin_tolerance: 스킨 가중치 허용 오차 (기본값: 0.01)
    146        """
    147        # 스킨 모디파이어 설정
    148        skin_mod.enableDQ = False
    149        skin_mod.bone_Limit = bone_limit
    150        skin_mod.clearZeroLimit = skin_tolerance
    151        rt.skinOps.RemoveZeroWeights(skin_mod)
    152        skin_mod.clearZeroLimit = 0
    153
    154        skin_mod.filter_vertices = True
    155        skin_mod.showNoEnvelopes = True
    156
    157        rt.skinOps.closeWeightTable(skin_mod)
    158        rt.skinOps.closeWeightTool(skin_mod)
    159
    160        if rt.skinOps.getNumberBones(skin_mod) > 1:
    161            list_of_bones = [i for i in range(1, rt.skinOps.GetNumberBones(skin_mod) + 1)]
    162
    163            for v in range(1, rt.skinOps.GetNumberVertices(skin_mod) + 1):
    164                for b in range(1, rt.skinOps.GetVertexWeightCount(skin_mod, v) + 1):
    165                    bone_id = rt.skinOps.GetVertexWeightBoneID(skin_mod, v, b)
    166                    if bone_id in list_of_bones:
    167                        list_of_bones.remove(bone_id)
    168
    169            # 역순으로 본 제거 (인덱스 변경 문제 방지)
    170            for i in range(len(list_of_bones) - 1, -1, -1):
    171                bone_id = list_of_bones[i]
    172                rt.skinOps.SelectBone(skin_mod, bone_id)
    173                rt.skinOps.removebone(skin_mod, bone_id)
    174
    175            if rt.skinOps.getNumberBones(skin_mod) > 1:
    176                rt.skinOps.SelectBone(skin_mod, 1)
    177
    178            skin_mod_obj = rt.getCurrentSelection()[0]
    179
    180            print(f"Obj:{skin_mod_obj.name} Removed:{len(list_of_bones)} Left:{rt.skinOps.GetNumberBones(skin_mod)}")
    181
    182    def optimize_skin_process(self, objs=None, optim_all_skin_mod=False, bone_limit=8, skin_tolerance=0.01):
    183        """
    184        여러 객체의 스킨 최적화 프로세스
    185
    186        Args:
    187            objs: 최적화할 객체 배열 (기본값: 현재 선택된 객체들)
    188            optim_all_skin_mod: 모든 스킨 모디파이어 최적화 여부 (기본값: False)
    189            bone_limit: 본 제한 수 (기본값: 8)
    190            skin_tolerance: 스킨 가중치 허용 오차 (기본값: 0.01)
    191        """
    192        if objs is None:
    193            objs = rt.selection
    194
    195        if not objs:
    196            return
    197
    198        rt.execute("max modify mode")
    199
    200        for obj in objs:
    201            if self.has_skin(obj):
    202                mod_id = [i+1 for i in range(len(obj.modifiers)) if rt.classOf(obj.modifiers[i]) == rt.Skin]
    203
    204                if not optim_all_skin_mod:
    205                    mod_id = [mod_id[0]]
    206
    207                for each in mod_id:
    208                    rt.modPanel.setCurrentObject(obj.modifiers[each-1])
    209                    self.optimize_skin(obj.modifiers[each-1], bone_limit=bone_limit, skin_tolerance=skin_tolerance)
    210
    211        rt.select(objs)
    212
    213    def load_skin(self, obj, file_path, load_bind_pose=False, keep_skin=False):
    214        """
    215        스킨 데이터 로드
    216
    217        Args:
    218            obj: 로드할 객체
    219            file_path: 스킨 파일 경로
    220            load_bind_pose: 바인드 포즈 로드 여부
    221            keep_skin: 기존 스킨 유지 여부
    222
    223        Returns:
    224            누락된 본 배열
    225        """
    226        # 기본값 설정
    227        if keep_skin != True:
    228            keep_skin = False
    229
    230        # 객체 선택
    231        rt.select(obj)
    232        data = []
    233        missing_bones = []
    234
    235        # 파일 열기
    236        try:
    237            with open(file_path, 'r') as f:
    238                for line in f:
    239                    data.append(line.strip())
    240        except:
    241            return []
    242
    243        # 버텍스 수 확인
    244        if len(data) - 1 != obj.verts.count or obj.verts.count == 0:
    245            print("Bad number of verts")
    246            return []
    247
    248        # 기존 스킨 모디파이어 처리
    249        if not keep_skin:
    250            for i in range(len(obj.modifiers) - 1, -1, -1):
    251                if rt.classOf(obj.modifiers[i]) == rt.Skin:
    252                    rt.deleteModifier(obj, i+1)
    253
    254        # 모디파이 모드 설정
    255        rt.setCommandPanelTaskMode(rt.Name('modify'))
    256
    257        # 새 스킨 모디파이어 생성
    258        new_skin = rt.Skin()
    259        rt.addModifier(obj, new_skin, before=1 if keep_skin else 0)
    260
    261        # 스킨 이름 설정
    262        if keep_skin:
    263            new_skin.name = "Skin_" + os.path.splitext(os.path.basename(file_path))[0]
    264
    265        # 현재 모디파이어 설정
    266        rt.modPanel.setCurrentObject(new_skin)
    267
    268        tempData = [rt.execute(item) for item in data]
    269
    270        # 본 데이터 처리
    271        bones_data = rt.execute(tempData[0])
    272        hierarchy = []
    273
    274        for i in range(len(bones_data)):
    275            # 본 이름으로 노드 찾기
    276            my_bone = [node for node in rt.objects if node.name == bones_data[i]]
    277
    278            # 없는 본인 경우 더미 생성
    279            if len(my_bone) == 0:
    280                print(f"Missing bone: {bones_data[i]}")
    281                tmp = rt.Dummy(name=bones_data[i])
    282                my_bone = [tmp]
    283                missing_bones.append(tmp)
    284
    285            # 계층 구조 확인
    286            if len(my_bone) > 1 and len(hierarchy) != 0:
    287                print(f"Multiple bones are named: {my_bone[0].name} ({len(my_bone)})")
    288                good_bone = None
    289                for o in my_bone:
    290                    if o in hierarchy:
    291                        good_bone = o
    292                        break
    293                if good_bone is not None:
    294                    my_bone = [good_bone]
    295
    296            # 사용할 본 결정
    297            my_bone = my_bone[0]
    298
    299            # 계층에 추가
    300            if my_bone not in hierarchy:
    301                hierarchy.append(my_bone)
    302                all_nodes = list(hierarchy)
    303
    304                for node in all_nodes:
    305                    # 자식 노드 추가
    306                    for child in node.children:
    307                        if child not in all_nodes:
    308                            all_nodes.append(child)
    309                    # 부모 노드 추가
    310                    if node.parent is not None and node.parent not in all_nodes:
    311                        all_nodes.append(node.parent)
    312
    313                    # 계층에 추가
    314                    for node in all_nodes:
    315                        if self.is_valid_bone(node) and node not in hierarchy:
    316                            hierarchy.append(node)
    317
    318            # 본 추가
    319            rt.skinOps.addBone(new_skin, my_bone, 1.0)
    320
    321            # 바인드 포즈 로드
    322            if load_bind_pose:
    323                bind_pose_file = os.path.splitext(file_path)[0] + "bp"
    324                bind_poses = []
    325
    326                if os.path.exists(bind_pose_file):
    327                    try:
    328                        with open(bind_pose_file, 'r') as f:
    329                            for line in f:
    330                                bind_poses.append(rt.execute(line.strip()))
    331                    except:
    332                        pass
    333
    334                if i < len(bind_poses) and bind_poses[i] is not None:
    335                    rt.skinUtils.SetBoneBindTM(obj, my_bone, bind_poses[i])
    336
    337        # 가중치 데이터 처리
    338        for i in range(1, obj.verts.count + 1):
    339            bone_id = []
    340            bone_weight = []
    341            good_bones = []
    342            all_bone_weight = [0] * len(bones_data)
    343
    344            # 가중치 합산
    345            for b in range(len(tempData[i][0])):
    346                bone_index = tempData[i][0][b]
    347                weight = tempData[i][1][b]
    348                all_bone_weight[bone_index-1] += weight
    349                good_bones.append(bone_index)
    350
    351            # 가중치 적용
    352            for b in good_bones:
    353                bone_id.append(b)
    354                bone_weight.append(all_bone_weight[b-1])
    355
    356            # 가중치 설정
    357            if len(bone_id) != 0:
    358                rt.skinOps.SetVertexWeights(new_skin, i, bone_id[0], 1.0)  # Max 2014 sp5 hack
    359                rt.skinOps.ReplaceVertexWeights(new_skin, i, bone_id, bone_weight)
    360
    361        return missing_bones
    362
    363    def save_skin(self, obj=None, file_path=None, save_bind_pose=False):
    364        """
    365        스킨 데이터 저장
    366        MAXScript의 saveskin.ms 를 Python으로 변환한 함수
    367
    368        Args:
    369            obj: 저장할 객체 (기본값: 현재 선택된 객체)
    370            file_path: 저장할 파일 경로 (기본값: None, 자동 생성)
    371
    372        Returns:
    373            저장된 파일 경로
    374        """
    375        # 현재 선택된 객체가 없는 경우 선택된 객체 사용
    376        if obj is None:
    377            if len(rt.selection) > 0:
    378                obj = rt.selection[0]
    379            else:
    380                print("No object selected")
    381                return None
    382
    383        # 현재 스킨 모디파이어 가져오기
    384        skin_mod = rt.modPanel.getCurrentObject()
    385
    386        # 스킨 모디파이어가 아니거나 본이 없는 경우 종료
    387        if rt.classOf(skin_mod) != rt.Skin or rt.skinOps.GetNumberBones(skin_mod) <= 0:
    388            print("Current modifier is not a Skin modifier or has no bones")
    389            return None
    390
    391        # 본 리스트 생성
    392        bones_list = []
    393        for i in range(1, rt.skinOps.GetNumberBones(skin_mod) + 1):
    394            bones_list.append(rt.skinOps.GetBoneName(skin_mod, i, 1))
    395
    396        # 스킨 데이터 생성
    397        skin_data = "\"#(\\\"" + "\\\",\\\"".join(str(x) for x in bones_list) + "\\\")\"\n"
    398
    399        # 버텍스별 가중치 데이터 수집
    400        for v in range(1, rt.skinOps.GetNumberVertices(skin_mod) + 1):
    401            bone_array = []
    402            weight_array = []
    403
    404            for b in range(1, rt.skinOps.GetVertexWeightCount(skin_mod, v) + 1):
    405                bone_array.append(rt.skinOps.GetVertexWeightBoneID(skin_mod, v, b))
    406                weight_array.append(rt.skinOps.GetVertexWeight(skin_mod, v, b))
    407
    408            stringBoneArray = "#(" + ",".join(str(x) for x in bone_array) + ")"
    409            stringWeightArray = "#(" + ",".join(str(w) for w in weight_array) + ")"
    410            skin_data += ("#(" + stringBoneArray + ", " + stringWeightArray + ")\n")
    411
    412        # 파일 경로가 지정되지 않은 경우 자동 생성
    413        if file_path is None:
    414            # animations 폴더 내 skindata 폴더 생성
    415            animations_dir = rt.getDir(rt.Name('animations'))
    416            skin_data_dir = os.path.join(animations_dir, "skindata")
    417
    418            if not os.path.exists(skin_data_dir):
    419                os.makedirs(skin_data_dir)
    420
    421            # 파일명 생성 (객체명 + 버텍스수 + 면수)
    422            file_name = f"{obj.name} [v{obj.mesh.verts.count}] [t{obj.mesh.faces.count}].skin"
    423            file_path = os.path.join(skin_data_dir, file_name)
    424
    425        print(f"Saving to: {file_path}")
    426
    427        # 스킨 데이터 파일 저장
    428        try:
    429            with open(file_path, 'w') as f:
    430                for data in skin_data:
    431                    f.write(data)
    432        except Exception as e:
    433            print(f"Error saving skin data: {e}")
    434            return None
    435
    436        if save_bind_pose:
    437            # 바인드 포즈 데이터 수집 및 저장
    438            bind_poses = []
    439            for i in range(1, rt.skinOps.GetNumberBones(skin_mod) + 1):
    440                bone_name = rt.skinOps.GetBoneName(skin_mod, i, 1)
    441                bone_node = rt.getNodeByName(bone_name)
    442                bind_pose = rt.skinUtils.GetBoneBindTM(obj, bone_node)
    443                bind_poses.append(bind_pose)
    444
    445            # 바인드 포즈 파일 저장
    446            bind_pose_file = file_path[:-4] + "bp"  # .skin -> .bp
    447            try:
    448                with open(bind_pose_file, 'w') as f:
    449                    for pose in bind_poses:
    450                        f.write(str(pose) + '\n')
    451            except Exception as e:
    452                print(f"Error saving bind pose data: {e}")
    453
    454        return file_path
    455
    456    def get_bone_id(self, skin_mod, b_array, type=1, refresh=True):
    457        """
    458        스킨 모디파이어에서 본 ID 가져오기
    459
    460        Args:
    461            skin_mod: 스킨 모디파이어
    462            b_array: 본 배열
    463            type: 0=객체, 1=객체 이름
    464            refresh: 인터페이스 업데이트 여부
    465
    466        Returns:
    467            본 ID 배열
    468        """
    469        bone_id = []
    470
    471        if refresh:
    472            rt.modPanel.setCurrentObject(skin_mod)
    473
    474        for i in range(1, rt.skinOps.GetNumberBones(skin_mod) + 1):
    475            if type == 0:
    476                bone_name = rt.skinOps.GetBoneName(skin_mod, i, 1)
    477                id = b_array.index(bone_name) + 1 if bone_name in b_array else 0
    478            elif type == 1:
    479                bone = rt.getNodeByName(rt.skinOps.GetBoneName(skin_mod, i, 1))
    480                id = b_array.index(bone) + 1 if bone in b_array else 0
    481
    482            if id != 0:
    483                bone_id.append(i)
    484
    485        return bone_id
    486
    487    def get_bone_id_from_name(self, in_skin_mod, bone_name):
    488        """
    489        본 이름으로 본 ID 가져오기
    490
    491        Args:
    492            in_skin_mod: 스킨 모디파이어를 가진 객체
    493            bone_name: 본 이름
    494
    495        Returns:
    496            본 ID
    497        """
    498        for i in range(1, rt.skinOps.GetNumberBones(in_skin_mod) + 1):
    499            if rt.skinOps.GetBoneName(in_skin_mod, i, 1) == bone_name:
    500                return i
    501        return None
    502
    503    def get_bones_from_skin(self, objs, skin_mod_index):
    504        """
    505        스킨 모디파이어에서 사용된 본 배열 가져오기
    506
    507        Args:
    508            objs: 객체 배열
    509            skin_mod_index: 스킨 모디파이어 인덱스
    510
    511        Returns:
    512            본 배열
    513        """
    514        inf_list = []
    515
    516        for obj in objs:
    517            if rt.isValidNode(obj):
    518                deps = rt.refs.dependsOn(obj.modifiers[skin_mod_index])
    519                for n in deps:
    520                    if rt.isValidNode(n) and self.is_valid_bone(n):
    521                        if n not in inf_list:
    522                            inf_list.append(n)
    523
    524        return inf_list
    525
    526    def find_skin_mod_id(self, obj):
    527        """
    528        객체에서 스킨 모디파이어 인덱스 찾기
    529
    530        Args:
    531            obj: 대상 객체
    532
    533        Returns:
    534            스킨 모디파이어 인덱스 배열
    535        """
    536        return [i+1 for i in range(len(obj.modifiers)) if rt.classOf(obj.modifiers[i]) == rt.Skin]
    537
    538    def sel_vert_from_bones(self, skin_mod, threshold=0.01):
    539        """
    540        선택된 본에 영향 받는 버텍스 선택
    541
    542        Args:
    543            skin_mod: 스킨 모디파이어
    544            threshold: 가중치 임계값 (기본값: 0.01)
    545
    546        Returns:
    547            선택된 버텍스 배열
    548        """
    549        verts_to_sel = []
    550
    551        if skin_mod is not None:
    552            le_bone = rt.skinOps.getSelectedBone(skin_mod)
    553            svc = rt.skinOps.GetNumberVertices(skin_mod)
    554
    555            for o in range(1, svc + 1):
    556                lv = rt.skinOps.GetVertexWeightCount(skin_mod, o)
    557
    558                for k in range(1, lv + 1):
    559                    if rt.skinOps.GetVertexWeightBoneID(skin_mod, o, k) == le_bone:
    560                        if rt.skinOps.GetVertexWeight(skin_mod, o, k) >= threshold:
    561                            if o not in verts_to_sel:
    562                                verts_to_sel.append(o)
    563
    564            rt.skinOps.SelectVertices(skin_mod, verts_to_sel)
    565
    566        else:
    567            print("You must have a skinned object selected")
    568
    569        return verts_to_sel
    570
    571    def sel_all_verts(self, skin_mod):
    572        """
    573        스킨 모디파이어의 모든 버텍스 선택
    574
    575        Args:
    576            skin_mod: 스킨 모디파이어
    577
    578        Returns:
    579            선택된 버텍스 배열
    580        """
    581        verts_to_sel = []
    582
    583        if skin_mod is not None:
    584            svc = rt.skinOps.GetNumberVertices(skin_mod)
    585
    586            for o in range(1, svc + 1):
    587                verts_to_sel.append(o)
    588
    589            rt.skinOps.SelectVertices(skin_mod, verts_to_sel)
    590
    591        return verts_to_sel
    592
    593    def make_rigid_skin(self, skin_mod, vert_list):
    594        """
    595        버텍스 가중치를 경직화(rigid) 처리
    596
    597        Args:
    598            skin_mod: 스킨 모디파이어
    599            vert_list: 버텍스 리스트
    600
    601        Returns:
    602            [본 ID 배열, 가중치 배열]
    603        """
    604        weight_array = {}
    605        vert_count = 0
    606        bone_array = []
    607        final_weight = []
    608
    609        # 가중치 수집
    610        for v in vert_list:
    611            for cur_bone in range(1, rt.skinOps.GetVertexWeightCount(skin_mod, v) + 1):
    612                cur_id = rt.skinOps.GetVertexWeightBoneID(skin_mod, v, cur_bone)
    613
    614                if cur_id not in weight_array:
    615                    weight_array[cur_id] = 0
    616
    617                cur_weight = rt.skinOps.GetVertexWeight(skin_mod, v, cur_bone)
    618                weight_array[cur_id] += cur_weight
    619                vert_count += cur_weight
    620
    621        # 최종 가중치 계산
    622        for i in weight_array:
    623            if weight_array[i] > 0:
    624                new_val = weight_array[i] / vert_count
    625                if new_val > 0.01:
    626                    bone_array.append(i)
    627                    final_weight.append(new_val)
    628
    629        return [bone_array, final_weight]
    630
    631    def transfert_skin_data(self, obj, source_bones, target_bones, vtx_list):
    632        """
    633        스킨 가중치 데이터 이전
    634
    635        Args:
    636            obj: 대상 객체
    637            source_bones: 원본 본 배열
    638            target_bones: 대상 본
    639            vtx_list: 버텍스 리스트
    640        """
    641        skin_data = []
    642        new_skin_data = []
    643
    644        # 본 ID 가져오기
    645        source_bones_id = [self.get_bone_id_from_name(obj, b.name) for b in source_bones]
    646        target_bone_id = self.get_bone_id_from_name(obj, target_bones.name)
    647
    648        bone_list = [n for n in rt.refs.dependsOn(obj.skin) if rt.isValidNode(n) and self.is_valid_bone(n)]
    649        bone_id_map = {self.get_bone_id_from_name(obj, b.name): i for i, b in enumerate(bone_list)}
    650
    651        # 스킨 데이터 수집
    652        for vtx in vtx_list:
    653            bone_array = []
    654            weight_array = []
    655            bone_weight = [0] * len(bone_list)
    656
    657            for b in range(1, rt.skinOps.GetVertexWeightCount(obj.skin, vtx) + 1):
    658                bone_idx = rt.skinOps.GetVertexWeightBoneID(obj.skin, vtx, b)
    659                bone_weight[bone_id_map[bone_idx]] += rt.skinOps.GetVertexWeight(obj.skin, vtx, b)
    660
    661            for b in range(len(bone_weight)):
    662                if bone_weight[b] > 0:
    663                    bone_array.append(b+1)
    664                    weight_array.append(bone_weight[b])
    665
    666            skin_data.append([bone_array, weight_array])
    667            new_skin_data.append([bone_array[:], weight_array[:]])
    668
    669        # 스킨 데이터 이전
    670        for b, source_bone_id in enumerate(source_bones_id):
    671            vtx_id = []
    672            vtx_weight = []
    673
    674            # 원본 본의 가중치 추출
    675            for vtx in range(len(skin_data)):
    676                for i in range(len(skin_data[vtx][0])):
    677                    if skin_data[vtx][0][i] == source_bone_id:
    678                        vtx_id.append(vtx)
    679                        vtx_weight.append(skin_data[vtx][1][i])
    680
    681            # 원본 본 영향력 제거
    682            for vtx in range(len(vtx_id)):
    683                for i in range(len(new_skin_data[vtx_id[vtx]][0])):
    684                    if new_skin_data[vtx_id[vtx]][0][i] == source_bone_id:
    685                        new_skin_data[vtx_id[vtx]][1][i] = 0.0
    686
    687            # 타겟 본에 영향력 추가
    688            for vtx in range(len(vtx_id)):
    689                id = new_skin_data[vtx_id[vtx]][0].index(target_bone_id) if target_bone_id in new_skin_data[vtx_id[vtx]][0] else -1
    690
    691                if id == -1:
    692                    new_skin_data[vtx_id[vtx]][0].append(target_bone_id)
    693                    new_skin_data[vtx_id[vtx]][1].append(vtx_weight[vtx])
    694                else:
    695                    new_skin_data[vtx_id[vtx]][1][id] += vtx_weight[vtx]
    696
    697        # 스킨 데이터 적용
    698        for i in range(len(vtx_list)):
    699            rt.skinOps.ReplaceVertexWeights(obj.skin, vtx_list[i],
    700                                           skin_data[i][0], new_skin_data[i][1])
    701
    702    def smooth_skin(self, inObj, inVertMode=VertexMode.Edges, inRadius=5.0, inIterNum=3, inKeepMax=False):
    703        """
    704        스킨 가중치 부드럽게 하기
    705
    706        Args:
    707            inObj: 대상 객체
    708            inVertMode: 버텍스 모드 (기본값: 1)
    709            inRadius: 반경 (기본값: 5.0)
    710            inIterNum: 반복 횟수 (기본값: 3)
    711            inKeepMax: 최대 가중치 유지 여부 (기본값: False)
    712
    713        Returns:
    714            None
    715        """
    716        maxScriptCode = textwrap.dedent(r'''
    717            struct _SmoothSkin (
    718            SmoothSkinMaxUndo = 10,
    719            UndoWeights = #(),
    720            SmoothSkinData = #(#(), #(), #(), #(), #(), #(), #()),
    721            smoothRadius = 5.0,
    722            iterNum = 1,
    723            keepMax = false,
    724
    725            -- vertGroupMode: Edges, Attach, All, Stiff
    726            vertGroupMode = 1,
    727
    728            fn make_rigid_skin skin_mod vert_list =
    729            (
    730                /*
    731                Rigidify vertices weights in skin modifier
    732                */
    733                WeightArray = #()
    734                VertCount = 0
    735                BoneArray = #()
    736                FinalWeight = #()
    737
    738                for v in vert_list do
    739                (
    740                    for CurBone = 1 to (skinOps.GetVertexWeightCount skin_mod v) do
    741                    (
    742                        CurID = (skinOps.GetVertexWeightBoneID skin_mod v CurBone)
    743                        if WeightArray[CurID] == undefined do WeightArray[CurID] = 0
    744
    745                        CurWeight = (skinOps.GetVertexWeight skin_mod v CurBone)
    746                        WeightArray[CurID] += CurWeight
    747                        VertCount += CurWeight
    748                    )
    749
    750                    for i = 1 to WeightArray.count where WeightArray[i] != undefined and WeightArray[i] > 0 do
    751                    (
    752                        NewVal = (WeightArray[i] / VertCount)
    753                        if NewVal > 0.01 do (append BoneArray i; append FinalWeight NewVal)
    754                    )
    755                )
    756                return #(BoneArray, FinalWeight)
    757            ),
    758
    759            fn smooth_skin =
    760            (
    761                if $selection.count != 1 then return false
    762
    763                p = 0
    764                for iter = 1 to iterNum do
    765                (
    766                    p += 1
    767                    if classOf (modPanel.getCurrentObject()) != Skin then return false
    768
    769                    obj = $; skinMod = modPanel.getCurrentObject()
    770                    FinalBoneArray = #(); FinalWeightArray = #(); o = 1
    771
    772                    UseOldData = (obj == SmoothSkinData[1][1]) and (obj.verts.count == SmoothSkinData[1][2])
    773                    if not UseOldData do SmoothSkinData = #(#(), #(), #(), #(), #(), #(), #())
    774                    SmoothSkinData[1][1] = obj; SmoothSkinData[1][2] = obj.verts.count
    775
    776                    tmpObj = copy Obj
    777                    tmpObj.modifiers[skinMod.name].enabled = false
    778
    779                    fn DoNormalizeWeight Weight =
    780                    (
    781                        WeightLength = 0; NormalizeWeight = #()
    782                        for w = 1 to Weight.count do WeightLength += Weight[w]
    783                        if WeightLength != 0 then
    784                            for w = 1 to Weight.count do NormalizeWeight[w] = Weight[w] * (1 / WeightLength)
    785                        else
    786                            NormalizeWeight[1] = 1.0
    787                        return NormalizeWeight
    788                    )
    789
    790                    skinMod.clearZeroLimit = 0.00
    791                    skinOps.RemoveZeroWeights skinMod
    792
    793                    posarray = for a in tmpObj.verts collect a.pos
    794
    795                    if (SmoothSkinData[8] != smoothRadius) do (SmoothSkinData[6] = #(); SmoothSkinData[7] = #())
    796
    797                    for v = 1 to obj.verts.count where (skinOps.IsVertexSelected skinMod v == 1) and (not keepMax or (skinOps.GetVertexWeightCount skinmod v != 1)) do
    798                    (
    799                        VertBros = #{}; VertBrosRatio = #()
    800                        Weightarray = #(); BoneArray = #(); FinalWeight = #()
    801                        WeightArray.count = skinOps.GetNumberBones skinMod
    802
    803                        if vertGroupMode == 1 and (SmoothSkinData[2][v] == undefined) do
    804                        (
    805                            if (classof tmpObj == Editable_Poly) or (classof tmpObj == PolyMeshObject) then
    806                            (
    807                                CurEdges = polyop.GetEdgesUsingVert tmpObj v
    808                                for CE in CurEdges do VertBros += (polyop.getEdgeVerts tmpObj CE) as bitArray
    809                            )
    810                            else
    811                            (
    812                                CurEdges = meshop.GetEdgesUsingvert tmpObj v
    813                                for i in CurEdges do CurEdges[i] = (getEdgeVis tmpObj (1+(i-1)/3)(1+mod (i-1) 3))
    814                                for CE in CurEdges do VertBros += (meshop.getVertsUsingEdge tmpObj CE) as bitArray
    815                            )
    816
    817                            VertBros = VertBros as array
    818                            SmoothSkinData[2][v] = #()
    819                            SmoothSkinData[3][v] = #()
    820
    821                            if VertBros.count > 0 do
    822                            (
    823                                for vb in VertBros do
    824                                (
    825                                    CurDist = distance posarray[v] posarray[vb]
    826                                    if CurDist == 0 then
    827                                        append VertBrosRatio 0
    828                                    else
    829                                        append VertBrosRatio (1 / CurDist)
    830                                )
    831
    832                                VertBrosRatio = DoNormalizeWeight VertBrosRatio
    833                                VertBrosRatio[finditem VertBros v] = 1
    834                                SmoothSkinData[2][v] = VertBros
    835                                SmoothSkinData[3][v] = VertBrosRatio
    836                            )
    837                        )
    838
    839                        if vertGroupMode == 2 do
    840                        (
    841                            SmoothSkinData[4][v] = for vb = 1 to posarray.count where (skinOps.IsVertexSelected skinMod vb == 0) and (distance posarray[v] posarray[vb]) < smoothRadius collect vb
    842                            SmoothSkinData[5][v] = for vb in SmoothSkinData[4][v] collect
    843                                (CurDist = distance posarray[v] posarray[vb]; if CurDist == 0 then 0 else (1 / CurDist))
    844                            SmoothSkinData[5][v] = DoNormalizeWeight SmoothSkinData[5][v]
    845                            for i = 1 to SmoothSkinData[5][v].count do SmoothSkinData[5][v][i] *= 2
    846                        )
    847
    848                        if vertGroupMode == 3 and (SmoothSkinData[6][v] == undefined) do
    849                        (
    850                            SmoothSkinData[6][v] = for vb = 1 to posarray.count where (distance posarray[v] posarray[vb]) < smoothRadius collect vb
    851                            SmoothSkinData[7][v] = for vb in SmoothSkinData[6][v] collect
    852                                (CurDist = distance posarray[v] posarray[vb]; if CurDist == 0 then 0 else (1 / CurDist))
    853                            SmoothSkinData[7][v] = DoNormalizeWeight SmoothSkinData[7][v]
    854                            for i = 1 to SmoothSkinData[7][v].count do SmoothSkinData[7][v][i] *= 2
    855                        )
    856
    857                        if vertGroupMode != 4 do
    858                        (
    859                            VertBros = SmoothSkinData[vertGroupMode * 2][v]
    860                            VertBrosRatio = SmoothSkinData[(vertGroupMode * 2) + 1][v]
    861
    862                            for z = 1 to VertBros.count do
    863                                for CurBone = 1 to (skinOps.GetVertexWeightCount skinMod VertBros[z]) do
    864                                (
    865                                    CurID = (skinOps.GetVertexWeightBoneID skinMod VertBros[z] CurBone)
    866                                    if WeightArray[CurID] == undefined do WeightArray[CurID] = 0
    867                                    WeightArray[CurID] += (skinOps.GetVertexWeight skinMod VertBros[z] CurBone) * VertBrosRatio[z]
    868                                )
    869
    870                            for i = 1 to WeightArray.count where WeightArray[i] != undefined and WeightArray[i] > 0 do
    871                            (
    872                                NewVal = (WeightArray[i] / 2)
    873                                if NewVal > 0.01 do (append BoneArray i; append FinalWeight NewVal)
    874                            )
    875                            FinalBoneArray[v] = BoneArray
    876                            FinalWeightArray[v] = FinalWeight
    877                        )
    878                    )
    879
    880                    if vertGroupMode == 4 then
    881                    (
    882                        convertTopoly tmpObj
    883                        polyObj = tmpObj
    884
    885                        -- Only test selected
    886                        VertSelection = for v = 1 to obj.verts.count where (skinOps.IsVertexSelected skinMod v == 1) collect v
    887                        DoneEdge = (polyobj.edges as bitarray) - polyop.getEdgesUsingVert polyObj VertSelection
    888                        DoneFace = (polyobj.faces as bitarray) - polyop.getFacesUsingVert polyObj VertSelection
    889
    890                        -- Elements
    891                        SmallElements = #()
    892                        for f = 1 to polyobj.faces.count where not DoneFace[f] do
    893                        (
    894                            CurElement = polyop.getElementsUsingFace polyObj #{f}
    895
    896                            CurVerts = polyop.getVertsUsingFace polyobj CurElement; MaxDist = 0
    897                            for v1 in CurVerts do
    898                                for v2 in CurVerts where MaxDist < (smoothRadius * 2) do
    899                                (
    900                                    dist = distance polyobj.verts[v1].pos polyobj.verts[v2].pos
    901                                    if dist > MaxDist do MaxDist = dist
    902                                )
    903                            if MaxDist < (smoothRadius * 2) do append SmallElements CurVerts
    904                            DoneFace += CurElement
    905                        )
    906
    907                        -- Loops
    908                        EdgeLoops = #()
    909                        for ed in SmallElements do DoneEdge += polyop.getEdgesUsingVert polyobj ed
    910                        for ed = 1 to polyobj.edges.count where not DoneEdge[ed] do
    911                        (
    912                            polyobj.selectedEdges = #{ed}
    913                            polyobj.ButtonOp #SelectEdgeLoop
    914                            CurEdgeLoop = (polyobj.selectedEdges as bitarray)
    915                            if CurEdgeLoop.numberSet > 2 do
    916                            (
    917                                CurVerts = (polyop.getvertsusingedge polyobj CurEdgeLoop); MaxDist = 0
    918                                for v1 in CurVerts do
    919                                    for v2 in CurVerts where MaxDist < (smoothRadius * 2) do
    920                                    (
    921                                        dist = distance polyobj.verts[v1].pos polyobj.verts[v2].pos
    922                                        if dist > MaxDist do MaxDist = dist
    923                                    )
    924                                if MaxDist < (smoothRadius * 2) do append EdgeLoops CurVerts
    925                            )
    926                            DoneEdge += CurEdgeLoop
    927                        )
    928
    929                        modPanel.setCurrentObject SkinMod; subobjectLevel = 1
    930                        for z in #(SmallElements, EdgeLoops) do
    931                            for i in z do
    932                            (
    933                                VertList = for v3 in i where (skinOps.IsVertexSelected skinMod v3 == 1) collect v3
    934                                NewWeights = self.make_rigid_skin SkinMod VertList
    935                                for v3 in VertList do (FinalBoneArray[v3] = NewWeights[1]; FinalWeightArray[v3] = NewWeights[2])
    936                            )
    937                    )
    938
    939                    SmoothSkinData[8] = smoothRadius
    940
    941                    delete tmpObj
    942                    OldWeightArray = #(); OldBoneArray = #(); LastWeights = #()
    943                    for sv = 1 to FinalBoneArray.count where FinalBonearray[sv] != undefined and FinalBoneArray[sv].count != 0 do
    944                    (
    945                        -- Home-Made undo
    946                        NumItem = skinOps.GetVertexWeightCount skinMod sv
    947                        OldWeightArray.count = OldBoneArray.count = NumItem
    948                        for CurBone = 1 to NumItem do
    949                        (
    950                            OldBoneArray[CurBone] = (skinOps.GetVertexWeightBoneID skinMod sv CurBone)
    951                            OldWeightArray[CurBone] = (skinOps.GetVertexWeight skinMod sv CurBone)
    952                        )
    953
    954                        append LastWeights #(skinMod, sv, deepcopy OldBoneArray, deepcopy OldWeightArray)
    955                        if UndoWeights.count >= SmoothSkinMaxUndo do deleteItem UndoWeights 1
    956
    957                        skinOps.ReplaceVertexWeights skinMod sv FinalBoneArray[sv] FinalWeightArray[sv]
    958                    )
    959
    960                    append UndoWeights LastWeights
    961
    962                    prog = ((p as float / iterNum as float) * 100.0)
    963                    format "Smoothing Progress:%\n" prog
    964                )
    965            ),
    966
    967            fn undo_smooth_skin = (
    968                CurUndo = UndoWeights[UndoWeights.count]
    969                try(
    970                    if modPanel.GetCurrentObject() != CurUndo[1][1] do (modPanel.setCurrentObject CurUndo[1][1]; subobjectLevel = 1)
    971                    for i in CurUndo do skinOps.ReplaceVertexWeights i[1] i[2] i[3] i[4]
    972                )
    973                catch( print "Undo fail")
    974                deleteitem UndoWeights UndoWeights.count
    975                if UndoWeights.count == 0 then return false
    976            ),
    977
    978            fn setting inVertMode inRadius inIterNum inKeepMax = (
    979                vertGroupMode = inVertMode
    980                smoothRadius = inRadius
    981                iterNum = inIterNum
    982                keepMax = inKeepMax
    983            )
    984        )
    985        ''')
    986
    987        if rt.isValidNode(inObj):
    988            rt.select(inObj)
    989            rt.execute("max modify mode")
    990
    991            targetSkinMod = self.get_skin_mod(inObj)
    992            rt.modPanel.setCurrentObject(targetSkinMod[0])
    993
    994            rt.execute(maxScriptCode)
    995            smooth_skin = rt._SmoothSkin()
    996            smooth_skin.setting(inVertMode.value, inRadius, inIterNum, inKeepMax)
    997            smooth_skin.smooth_skin()

고급 스킨 관련 기능을 제공하는 클래스. MAXScript의 ODC_Char_Skin 구조체 개념을 Python으로 재구현한 클래스이며, 3ds Max의 기능들을 pymxs API를 통해 제어합니다.

Skin() View Source

    31    def __init__(self):
    32        """
    33        클래스 초기화
    34        """
    35        self.skin_match_list = []

클래스 초기화

skin_match_list
def has_skin(self, obj=None): View Source

    37    def has_skin(self, obj=None):
    38        """
    39        객체에 스킨 모디파이어가 있는지 확인
    40
    41        Args:
    42            obj: 확인할 객체 (기본값: 현재 선택된 객체)
    43
    44        Returns:
    45            True: 스킨 모디파이어가 있는 경우
    46            False: 없는 경우
    47        """
    48        if obj is None:
    49            if len(rt.selection) > 0:
    50                obj = rt.selection[0]
    51            else:
    52                return False
    53
    54        # 객체의 모든 모디파이어를 검사하여 Skin 모디파이어가 있는지 확인
    55        for mod in obj.modifiers:
    56            if rt.classOf(mod) == rt.Skin:
    57                return True
    58        return False

객체에 스킨 모디파이어가 있는지 확인

Args: obj: 확인할 객체 (기본값: 현재 선택된 객체)

Returns: True: 스킨 모디파이어가 있는 경우 False: 없는 경우

def is_valid_bone(self, inNode): View Source

    60    def is_valid_bone(self, inNode):
    61        """
    62        노드가 유효한 스킨 본인지 확인
    63
    64        Args:
    65            inNode: 확인할 노드
    66
    67        Returns:
    68            True: 유효한 본인 경우
    69            False: 아닌 경우
    70        """
    71        return (rt.superClassOf(inNode) == rt.GeometryClass or
    72                rt.classOf(inNode) == rt.BoneGeometry or
    73                rt.superClassOf(inNode) == rt.Helper)

노드가 유효한 스킨 본인지 확인

Args: inNode: 확인할 노드

Returns: True: 유효한 본인 경우 False: 아닌 경우

def get_skin_mod(self, obj=None): View Source

    75    def get_skin_mod(self, obj=None):
    76        """
    77        객체의 스킨 모디파이어 배열 반환
    78
    79        Args:
    80            obj: 모디파이어를 가져올 객체 (기본값: 현재 선택된 객체)
    81
    82        Returns:
    83            스킨 모디파이어 배열
    84        """
    85        if obj is None:
    86            if len(rt.selection) > 0:
    87                obj = rt.selection[0]
    88            else:
    89                return []
    90
    91        return [mod for mod in obj.modifiers if rt.classOf(mod) == rt.Skin]

객체의 스킨 모디파이어 배열 반환

Args: obj: 모디파이어를 가져올 객체 (기본값: 현재 선택된 객체)

Returns: 스킨 모디파이어 배열

def bind_skin(self, obj, bone_array): View Source

     93    def bind_skin(self, obj, bone_array):
     94        """
     95        객체에 스킨 모디파이어 바인딩
     96
     97        Args:
     98            obj: 바인딩할 객체
     99            bone_array: 바인딩할 본 배열
    100
    101        Returns:
    102            True: 성공한 경우
    103            False: 실패한 경우
    104        """
    105        if obj is None or len(bone_array) < 1:
    106            print("Select at least 1 influence and an object.")
    107            return False
    108
    109        # Switch to modify mode
    110        rt.execute("max modify mode")
    111
    112        # Check if the object is valid for skinning
    113        if rt.superClassOf(obj) != rt.GeometryClass:
    114            print(f"{obj.name} must be 'Edit_Mesh' or 'Edit_Poly'.")
    115            return False
    116
    117        # Add skin modifier
    118        objmod = rt.Skin()
    119        rt.addModifier(obj, objmod)
    120        rt.select(obj)
    121
    122        # Add bones to skin modifier
    123        wgt = 1.0
    124        for each in bone_array:
    125            rt.skinOps.addBone(objmod, each, wgt)
    126
    127        # Set skin modifier options
    128        objmod.filter_vertices = True
    129        objmod.filter_envelopes = False
    130        objmod.filter_cross_sections = True
    131        objmod.enableDQ = False
    132        objmod.bone_Limit = 8
    133        objmod.colorAllWeights = True
    134        objmod.showNoEnvelopes = True
    135
    136        return True

객체에 스킨 모디파이어 바인딩

Args: obj: 바인딩할 객체 bone_array: 바인딩할 본 배열

Returns: True: 성공한 경우 False: 실패한 경우

def optimize_skin(self, skin_mod, bone_limit=8, skin_tolerance=0.01): View Source

    138    def optimize_skin(self, skin_mod, bone_limit=8, skin_tolerance=0.01):
    139        """
    140        스킨 모디파이어 최적화
    141
    142        Args:
    143            skin_mod: 스킨 모디파이어
    144            bone_limit: 본 제한 수 (기본값: 8)
    145            skin_tolerance: 스킨 가중치 허용 오차 (기본값: 0.01)
    146        """
    147        # 스킨 모디파이어 설정
    148        skin_mod.enableDQ = False
    149        skin_mod.bone_Limit = bone_limit
    150        skin_mod.clearZeroLimit = skin_tolerance
    151        rt.skinOps.RemoveZeroWeights(skin_mod)
    152        skin_mod.clearZeroLimit = 0
    153
    154        skin_mod.filter_vertices = True
    155        skin_mod.showNoEnvelopes = True
    156
    157        rt.skinOps.closeWeightTable(skin_mod)
    158        rt.skinOps.closeWeightTool(skin_mod)
    159
    160        if rt.skinOps.getNumberBones(skin_mod) > 1:
    161            list_of_bones = [i for i in range(1, rt.skinOps.GetNumberBones(skin_mod) + 1)]
    162
    163            for v in range(1, rt.skinOps.GetNumberVertices(skin_mod) + 1):
    164                for b in range(1, rt.skinOps.GetVertexWeightCount(skin_mod, v) + 1):
    165                    bone_id = rt.skinOps.GetVertexWeightBoneID(skin_mod, v, b)
    166                    if bone_id in list_of_bones:
    167                        list_of_bones.remove(bone_id)
    168
    169            # 역순으로 본 제거 (인덱스 변경 문제 방지)
    170            for i in range(len(list_of_bones) - 1, -1, -1):
    171                bone_id = list_of_bones[i]
    172                rt.skinOps.SelectBone(skin_mod, bone_id)
    173                rt.skinOps.removebone(skin_mod, bone_id)
    174
    175            if rt.skinOps.getNumberBones(skin_mod) > 1:
    176                rt.skinOps.SelectBone(skin_mod, 1)
    177
    178            skin_mod_obj = rt.getCurrentSelection()[0]
    179
    180            print(f"Obj:{skin_mod_obj.name} Removed:{len(list_of_bones)} Left:{rt.skinOps.GetNumberBones(skin_mod)}")

스킨 모디파이어 최적화

Args: skin_mod: 스킨 모디파이어 bone_limit: 본 제한 수 (기본값: 8) skin_tolerance: 스킨 가중치 허용 오차 (기본값: 0.01)

def optimize_skin_process( self, objs=None, optim_all_skin_mod=False, bone_limit=8, skin_tolerance=0.01): View Source

    182    def optimize_skin_process(self, objs=None, optim_all_skin_mod=False, bone_limit=8, skin_tolerance=0.01):
    183        """
    184        여러 객체의 스킨 최적화 프로세스
    185
    186        Args:
    187            objs: 최적화할 객체 배열 (기본값: 현재 선택된 객체들)
    188            optim_all_skin_mod: 모든 스킨 모디파이어 최적화 여부 (기본값: False)
    189            bone_limit: 본 제한 수 (기본값: 8)
    190            skin_tolerance: 스킨 가중치 허용 오차 (기본값: 0.01)
    191        """
    192        if objs is None:
    193            objs = rt.selection
    194
    195        if not objs:
    196            return
    197
    198        rt.execute("max modify mode")
    199
    200        for obj in objs:
    201            if self.has_skin(obj):
    202                mod_id = [i+1 for i in range(len(obj.modifiers)) if rt.classOf(obj.modifiers[i]) == rt.Skin]
    203
    204                if not optim_all_skin_mod:
    205                    mod_id = [mod_id[0]]
    206
    207                for each in mod_id:
    208                    rt.modPanel.setCurrentObject(obj.modifiers[each-1])
    209                    self.optimize_skin(obj.modifiers[each-1], bone_limit=bone_limit, skin_tolerance=skin_tolerance)
    210
    211        rt.select(objs)

여러 객체의 스킨 최적화 프로세스

Args: objs: 최적화할 객체 배열 (기본값: 현재 선택된 객체들) optim_all_skin_mod: 모든 스킨 모디파이어 최적화 여부 (기본값: False) bone_limit: 본 제한 수 (기본값: 8) skin_tolerance: 스킨 가중치 허용 오차 (기본값: 0.01)

def load_skin(self, obj, file_path, load_bind_pose=False, keep_skin=False): View Source

    213    def load_skin(self, obj, file_path, load_bind_pose=False, keep_skin=False):
    214        """
    215        스킨 데이터 로드
    216
    217        Args:
    218            obj: 로드할 객체
    219            file_path: 스킨 파일 경로
    220            load_bind_pose: 바인드 포즈 로드 여부
    221            keep_skin: 기존 스킨 유지 여부
    222
    223        Returns:
    224            누락된 본 배열
    225        """
    226        # 기본값 설정
    227        if keep_skin != True:
    228            keep_skin = False
    229
    230        # 객체 선택
    231        rt.select(obj)
    232        data = []
    233        missing_bones = []
    234
    235        # 파일 열기
    236        try:
    237            with open(file_path, 'r') as f:
    238                for line in f:
    239                    data.append(line.strip())
    240        except:
    241            return []
    242
    243        # 버텍스 수 확인
    244        if len(data) - 1 != obj.verts.count or obj.verts.count == 0:
    245            print("Bad number of verts")
    246            return []
    247
    248        # 기존 스킨 모디파이어 처리
    249        if not keep_skin:
    250            for i in range(len(obj.modifiers) - 1, -1, -1):
    251                if rt.classOf(obj.modifiers[i]) == rt.Skin:
    252                    rt.deleteModifier(obj, i+1)
    253
    254        # 모디파이 모드 설정
    255        rt.setCommandPanelTaskMode(rt.Name('modify'))
    256
    257        # 새 스킨 모디파이어 생성
    258        new_skin = rt.Skin()
    259        rt.addModifier(obj, new_skin, before=1 if keep_skin else 0)
    260
    261        # 스킨 이름 설정
    262        if keep_skin:
    263            new_skin.name = "Skin_" + os.path.splitext(os.path.basename(file_path))[0]
    264
    265        # 현재 모디파이어 설정
    266        rt.modPanel.setCurrentObject(new_skin)
    267
    268        tempData = [rt.execute(item) for item in data]
    269
    270        # 본 데이터 처리
    271        bones_data = rt.execute(tempData[0])
    272        hierarchy = []
    273
    274        for i in range(len(bones_data)):
    275            # 본 이름으로 노드 찾기
    276            my_bone = [node for node in rt.objects if node.name == bones_data[i]]
    277
    278            # 없는 본인 경우 더미 생성
    279            if len(my_bone) == 0:
    280                print(f"Missing bone: {bones_data[i]}")
    281                tmp = rt.Dummy(name=bones_data[i])
    282                my_bone = [tmp]
    283                missing_bones.append(tmp)
    284
    285            # 계층 구조 확인
    286            if len(my_bone) > 1 and len(hierarchy) != 0:
    287                print(f"Multiple bones are named: {my_bone[0].name} ({len(my_bone)})")
    288                good_bone = None
    289                for o in my_bone:
    290                    if o in hierarchy:
    291                        good_bone = o
    292                        break
    293                if good_bone is not None:
    294                    my_bone = [good_bone]
    295
    296            # 사용할 본 결정
    297            my_bone = my_bone[0]
    298
    299            # 계층에 추가
    300            if my_bone not in hierarchy:
    301                hierarchy.append(my_bone)
    302                all_nodes = list(hierarchy)
    303
    304                for node in all_nodes:
    305                    # 자식 노드 추가
    306                    for child in node.children:
    307                        if child not in all_nodes:
    308                            all_nodes.append(child)
    309                    # 부모 노드 추가
    310                    if node.parent is not None and node.parent not in all_nodes:
    311                        all_nodes.append(node.parent)
    312
    313                    # 계층에 추가
    314                    for node in all_nodes:
    315                        if self.is_valid_bone(node) and node not in hierarchy:
    316                            hierarchy.append(node)
    317
    318            # 본 추가
    319            rt.skinOps.addBone(new_skin, my_bone, 1.0)
    320
    321            # 바인드 포즈 로드
    322            if load_bind_pose:
    323                bind_pose_file = os.path.splitext(file_path)[0] + "bp"
    324                bind_poses = []
    325
    326                if os.path.exists(bind_pose_file):
    327                    try:
    328                        with open(bind_pose_file, 'r') as f:
    329                            for line in f:
    330                                bind_poses.append(rt.execute(line.strip()))
    331                    except:
    332                        pass
    333
    334                if i < len(bind_poses) and bind_poses[i] is not None:
    335                    rt.skinUtils.SetBoneBindTM(obj, my_bone, bind_poses[i])
    336
    337        # 가중치 데이터 처리
    338        for i in range(1, obj.verts.count + 1):
    339            bone_id = []
    340            bone_weight = []
    341            good_bones = []
    342            all_bone_weight = [0] * len(bones_data)
    343
    344            # 가중치 합산
    345            for b in range(len(tempData[i][0])):
    346                bone_index = tempData[i][0][b]
    347                weight = tempData[i][1][b]
    348                all_bone_weight[bone_index-1] += weight
    349                good_bones.append(bone_index)
    350
    351            # 가중치 적용
    352            for b in good_bones:
    353                bone_id.append(b)
    354                bone_weight.append(all_bone_weight[b-1])
    355
    356            # 가중치 설정
    357            if len(bone_id) != 0:
    358                rt.skinOps.SetVertexWeights(new_skin, i, bone_id[0], 1.0)  # Max 2014 sp5 hack
    359                rt.skinOps.ReplaceVertexWeights(new_skin, i, bone_id, bone_weight)
    360
    361        return missing_bones

스킨 데이터 로드

Args: obj: 로드할 객체 file_path: 스킨 파일 경로 load_bind_pose: 바인드 포즈 로드 여부 keep_skin: 기존 스킨 유지 여부

Returns: 누락된 본 배열

def save_skin(self, obj=None, file_path=None, save_bind_pose=False): View Source

    363    def save_skin(self, obj=None, file_path=None, save_bind_pose=False):
    364        """
    365        스킨 데이터 저장
    366        MAXScript의 saveskin.ms 를 Python으로 변환한 함수
    367
    368        Args:
    369            obj: 저장할 객체 (기본값: 현재 선택된 객체)
    370            file_path: 저장할 파일 경로 (기본값: None, 자동 생성)
    371
    372        Returns:
    373            저장된 파일 경로
    374        """
    375        # 현재 선택된 객체가 없는 경우 선택된 객체 사용
    376        if obj is None:
    377            if len(rt.selection) > 0:
    378                obj = rt.selection[0]
    379            else:
    380                print("No object selected")
    381                return None
    382
    383        # 현재 스킨 모디파이어 가져오기
    384        skin_mod = rt.modPanel.getCurrentObject()
    385
    386        # 스킨 모디파이어가 아니거나 본이 없는 경우 종료
    387        if rt.classOf(skin_mod) != rt.Skin or rt.skinOps.GetNumberBones(skin_mod) <= 0:
    388            print("Current modifier is not a Skin modifier or has no bones")
    389            return None
    390
    391        # 본 리스트 생성
    392        bones_list = []
    393        for i in range(1, rt.skinOps.GetNumberBones(skin_mod) + 1):
    394            bones_list.append(rt.skinOps.GetBoneName(skin_mod, i, 1))
    395
    396        # 스킨 데이터 생성
    397        skin_data = "\"#(\\\"" + "\\\",\\\"".join(str(x) for x in bones_list) + "\\\")\"\n"
    398
    399        # 버텍스별 가중치 데이터 수집
    400        for v in range(1, rt.skinOps.GetNumberVertices(skin_mod) + 1):
    401            bone_array = []
    402            weight_array = []
    403
    404            for b in range(1, rt.skinOps.GetVertexWeightCount(skin_mod, v) + 1):
    405                bone_array.append(rt.skinOps.GetVertexWeightBoneID(skin_mod, v, b))
    406                weight_array.append(rt.skinOps.GetVertexWeight(skin_mod, v, b))
    407
    408            stringBoneArray = "#(" + ",".join(str(x) for x in bone_array) + ")"
    409            stringWeightArray = "#(" + ",".join(str(w) for w in weight_array) + ")"
    410            skin_data += ("#(" + stringBoneArray + ", " + stringWeightArray + ")\n")
    411
    412        # 파일 경로가 지정되지 않은 경우 자동 생성
    413        if file_path is None:
    414            # animations 폴더 내 skindata 폴더 생성
    415            animations_dir = rt.getDir(rt.Name('animations'))
    416            skin_data_dir = os.path.join(animations_dir, "skindata")
    417
    418            if not os.path.exists(skin_data_dir):
    419                os.makedirs(skin_data_dir)
    420
    421            # 파일명 생성 (객체명 + 버텍스수 + 면수)
    422            file_name = f"{obj.name} [v{obj.mesh.verts.count}] [t{obj.mesh.faces.count}].skin"
    423            file_path = os.path.join(skin_data_dir, file_name)
    424
    425        print(f"Saving to: {file_path}")
    426
    427        # 스킨 데이터 파일 저장
    428        try:
    429            with open(file_path, 'w') as f:
    430                for data in skin_data:
    431                    f.write(data)
    432        except Exception as e:
    433            print(f"Error saving skin data: {e}")
    434            return None
    435
    436        if save_bind_pose:
    437            # 바인드 포즈 데이터 수집 및 저장
    438            bind_poses = []
    439            for i in range(1, rt.skinOps.GetNumberBones(skin_mod) + 1):
    440                bone_name = rt.skinOps.GetBoneName(skin_mod, i, 1)
    441                bone_node = rt.getNodeByName(bone_name)
    442                bind_pose = rt.skinUtils.GetBoneBindTM(obj, bone_node)
    443                bind_poses.append(bind_pose)
    444
    445            # 바인드 포즈 파일 저장
    446            bind_pose_file = file_path[:-4] + "bp"  # .skin -> .bp
    447            try:
    448                with open(bind_pose_file, 'w') as f:
    449                    for pose in bind_poses:
    450                        f.write(str(pose) + '\n')
    451            except Exception as e:
    452                print(f"Error saving bind pose data: {e}")
    453
    454        return file_path

스킨 데이터 저장 MAXScript의 saveskin.ms 를 Python으로 변환한 함수

Args: obj: 저장할 객체 (기본값: 현재 선택된 객체) file_path: 저장할 파일 경로 (기본값: None, 자동 생성)

Returns: 저장된 파일 경로

def get_bone_id(self, skin_mod, b_array, type=1, refresh=True): View Source

    456    def get_bone_id(self, skin_mod, b_array, type=1, refresh=True):
    457        """
    458        스킨 모디파이어에서 본 ID 가져오기
    459
    460        Args:
    461            skin_mod: 스킨 모디파이어
    462            b_array: 본 배열
    463            type: 0=객체, 1=객체 이름
    464            refresh: 인터페이스 업데이트 여부
    465
    466        Returns:
    467            본 ID 배열
    468        """
    469        bone_id = []
    470
    471        if refresh:
    472            rt.modPanel.setCurrentObject(skin_mod)
    473
    474        for i in range(1, rt.skinOps.GetNumberBones(skin_mod) + 1):
    475            if type == 0:
    476                bone_name = rt.skinOps.GetBoneName(skin_mod, i, 1)
    477                id = b_array.index(bone_name) + 1 if bone_name in b_array else 0
    478            elif type == 1:
    479                bone = rt.getNodeByName(rt.skinOps.GetBoneName(skin_mod, i, 1))
    480                id = b_array.index(bone) + 1 if bone in b_array else 0
    481
    482            if id != 0:
    483                bone_id.append(i)
    484
    485        return bone_id

스킨 모디파이어에서 본 ID 가져오기

Args: skin_mod: 스킨 모디파이어 b_array: 본 배열 type: 0=객체, 1=객체 이름 refresh: 인터페이스 업데이트 여부

Returns: 본 ID 배열

def get_bone_id_from_name(self, in_skin_mod, bone_name): View Source

    487    def get_bone_id_from_name(self, in_skin_mod, bone_name):
    488        """
    489        본 이름으로 본 ID 가져오기
    490
    491        Args:
    492            in_skin_mod: 스킨 모디파이어를 가진 객체
    493            bone_name: 본 이름
    494
    495        Returns:
    496            본 ID
    497        """
    498        for i in range(1, rt.skinOps.GetNumberBones(in_skin_mod) + 1):
    499            if rt.skinOps.GetBoneName(in_skin_mod, i, 1) == bone_name:
    500                return i
    501        return None

본 이름으로 본 ID 가져오기

Args: in_skin_mod: 스킨 모디파이어를 가진 객체 bone_name: 본 이름

Returns: 본 ID

def get_bones_from_skin(self, objs, skin_mod_index): View Source

    503    def get_bones_from_skin(self, objs, skin_mod_index):
    504        """
    505        스킨 모디파이어에서 사용된 본 배열 가져오기
    506
    507        Args:
    508            objs: 객체 배열
    509            skin_mod_index: 스킨 모디파이어 인덱스
    510
    511        Returns:
    512            본 배열
    513        """
    514        inf_list = []
    515
    516        for obj in objs:
    517            if rt.isValidNode(obj):
    518                deps = rt.refs.dependsOn(obj.modifiers[skin_mod_index])
    519                for n in deps:
    520                    if rt.isValidNode(n) and self.is_valid_bone(n):
    521                        if n not in inf_list:
    522                            inf_list.append(n)
    523
    524        return inf_list

스킨 모디파이어에서 사용된 본 배열 가져오기

Args: objs: 객체 배열 skin_mod_index: 스킨 모디파이어 인덱스

Returns: 본 배열

def find_skin_mod_id(self, obj): View Source

    526    def find_skin_mod_id(self, obj):
    527        """
    528        객체에서 스킨 모디파이어 인덱스 찾기
    529
    530        Args:
    531            obj: 대상 객체
    532
    533        Returns:
    534            스킨 모디파이어 인덱스 배열
    535        """
    536        return [i+1 for i in range(len(obj.modifiers)) if rt.classOf(obj.modifiers[i]) == rt.Skin]

객체에서 스킨 모디파이어 인덱스 찾기

Args: obj: 대상 객체

Returns: 스킨 모디파이어 인덱스 배열

def sel_vert_from_bones(self, skin_mod, threshold=0.01): View Source

    538    def sel_vert_from_bones(self, skin_mod, threshold=0.01):
    539        """
    540        선택된 본에 영향 받는 버텍스 선택
    541
    542        Args:
    543            skin_mod: 스킨 모디파이어
    544            threshold: 가중치 임계값 (기본값: 0.01)
    545
    546        Returns:
    547            선택된 버텍스 배열
    548        """
    549        verts_to_sel = []
    550
    551        if skin_mod is not None:
    552            le_bone = rt.skinOps.getSelectedBone(skin_mod)
    553            svc = rt.skinOps.GetNumberVertices(skin_mod)
    554
    555            for o in range(1, svc + 1):
    556                lv = rt.skinOps.GetVertexWeightCount(skin_mod, o)
    557
    558                for k in range(1, lv + 1):
    559                    if rt.skinOps.GetVertexWeightBoneID(skin_mod, o, k) == le_bone:
    560                        if rt.skinOps.GetVertexWeight(skin_mod, o, k) >= threshold:
    561                            if o not in verts_to_sel:
    562                                verts_to_sel.append(o)
    563
    564            rt.skinOps.SelectVertices(skin_mod, verts_to_sel)
    565
    566        else:
    567            print("You must have a skinned object selected")
    568
    569        return verts_to_sel

선택된 본에 영향 받는 버텍스 선택

Args: skin_mod: 스킨 모디파이어 threshold: 가중치 임계값 (기본값: 0.01)

Returns: 선택된 버텍스 배열

def sel_all_verts(self, skin_mod): View Source

    571    def sel_all_verts(self, skin_mod):
    572        """
    573        스킨 모디파이어의 모든 버텍스 선택
    574
    575        Args:
    576            skin_mod: 스킨 모디파이어
    577
    578        Returns:
    579            선택된 버텍스 배열
    580        """
    581        verts_to_sel = []
    582
    583        if skin_mod is not None:
    584            svc = rt.skinOps.GetNumberVertices(skin_mod)
    585
    586            for o in range(1, svc + 1):
    587                verts_to_sel.append(o)
    588
    589            rt.skinOps.SelectVertices(skin_mod, verts_to_sel)
    590
    591        return verts_to_sel

스킨 모디파이어의 모든 버텍스 선택

Args: skin_mod: 스킨 모디파이어

Returns: 선택된 버텍스 배열

def make_rigid_skin(self, skin_mod, vert_list): View Source

    593    def make_rigid_skin(self, skin_mod, vert_list):
    594        """
    595        버텍스 가중치를 경직화(rigid) 처리
    596
    597        Args:
    598            skin_mod: 스킨 모디파이어
    599            vert_list: 버텍스 리스트
    600
    601        Returns:
    602            [본 ID 배열, 가중치 배열]
    603        """
    604        weight_array = {}
    605        vert_count = 0
    606        bone_array = []
    607        final_weight = []
    608
    609        # 가중치 수집
    610        for v in vert_list:
    611            for cur_bone in range(1, rt.skinOps.GetVertexWeightCount(skin_mod, v) + 1):
    612                cur_id = rt.skinOps.GetVertexWeightBoneID(skin_mod, v, cur_bone)
    613
    614                if cur_id not in weight_array:
    615                    weight_array[cur_id] = 0
    616
    617                cur_weight = rt.skinOps.GetVertexWeight(skin_mod, v, cur_bone)
    618                weight_array[cur_id] += cur_weight
    619                vert_count += cur_weight
    620
    621        # 최종 가중치 계산
    622        for i in weight_array:
    623            if weight_array[i] > 0:
    624                new_val = weight_array[i] / vert_count
    625                if new_val > 0.01:
    626                    bone_array.append(i)
    627                    final_weight.append(new_val)
    628
    629        return [bone_array, final_weight]

버텍스 가중치를 경직화(rigid) 처리

Args: skin_mod: 스킨 모디파이어 vert_list: 버텍스 리스트

Returns: [본 ID 배열, 가중치 배열]

def transfert_skin_data(self, obj, source_bones, target_bones, vtx_list): View Source

    631    def transfert_skin_data(self, obj, source_bones, target_bones, vtx_list):
    632        """
    633        스킨 가중치 데이터 이전
    634
    635        Args:
    636            obj: 대상 객체
    637            source_bones: 원본 본 배열
    638            target_bones: 대상 본
    639            vtx_list: 버텍스 리스트
    640        """
    641        skin_data = []
    642        new_skin_data = []
    643
    644        # 본 ID 가져오기
    645        source_bones_id = [self.get_bone_id_from_name(obj, b.name) for b in source_bones]
    646        target_bone_id = self.get_bone_id_from_name(obj, target_bones.name)
    647
    648        bone_list = [n for n in rt.refs.dependsOn(obj.skin) if rt.isValidNode(n) and self.is_valid_bone(n)]
    649        bone_id_map = {self.get_bone_id_from_name(obj, b.name): i for i, b in enumerate(bone_list)}
    650
    651        # 스킨 데이터 수집
    652        for vtx in vtx_list:
    653            bone_array = []
    654            weight_array = []
    655            bone_weight = [0] * len(bone_list)
    656
    657            for b in range(1, rt.skinOps.GetVertexWeightCount(obj.skin, vtx) + 1):
    658                bone_idx = rt.skinOps.GetVertexWeightBoneID(obj.skin, vtx, b)
    659                bone_weight[bone_id_map[bone_idx]] += rt.skinOps.GetVertexWeight(obj.skin, vtx, b)
    660
    661            for b in range(len(bone_weight)):
    662                if bone_weight[b] > 0:
    663                    bone_array.append(b+1)
    664                    weight_array.append(bone_weight[b])
    665
    666            skin_data.append([bone_array, weight_array])
    667            new_skin_data.append([bone_array[:], weight_array[:]])
    668
    669        # 스킨 데이터 이전
    670        for b, source_bone_id in enumerate(source_bones_id):
    671            vtx_id = []
    672            vtx_weight = []
    673
    674            # 원본 본의 가중치 추출
    675            for vtx in range(len(skin_data)):
    676                for i in range(len(skin_data[vtx][0])):
    677                    if skin_data[vtx][0][i] == source_bone_id:
    678                        vtx_id.append(vtx)
    679                        vtx_weight.append(skin_data[vtx][1][i])
    680
    681            # 원본 본 영향력 제거
    682            for vtx in range(len(vtx_id)):
    683                for i in range(len(new_skin_data[vtx_id[vtx]][0])):
    684                    if new_skin_data[vtx_id[vtx]][0][i] == source_bone_id:
    685                        new_skin_data[vtx_id[vtx]][1][i] = 0.0
    686
    687            # 타겟 본에 영향력 추가
    688            for vtx in range(len(vtx_id)):
    689                id = new_skin_data[vtx_id[vtx]][0].index(target_bone_id) if target_bone_id in new_skin_data[vtx_id[vtx]][0] else -1
    690
    691                if id == -1:
    692                    new_skin_data[vtx_id[vtx]][0].append(target_bone_id)
    693                    new_skin_data[vtx_id[vtx]][1].append(vtx_weight[vtx])
    694                else:
    695                    new_skin_data[vtx_id[vtx]][1][id] += vtx_weight[vtx]
    696
    697        # 스킨 데이터 적용
    698        for i in range(len(vtx_list)):
    699            rt.skinOps.ReplaceVertexWeights(obj.skin, vtx_list[i],
    700                                           skin_data[i][0], new_skin_data[i][1])

스킨 가중치 데이터 이전

Args: obj: 대상 객체 source_bones: 원본 본 배열 target_bones: 대상 본 vtx_list: 버텍스 리스트

def smooth_skin( self, inObj, inVertMode=<VertexMode.Edges: 1>, inRadius=5.0, inIterNum=3, inKeepMax=False): View Source

    702    def smooth_skin(self, inObj, inVertMode=VertexMode.Edges, inRadius=5.0, inIterNum=3, inKeepMax=False):
    703        """
    704        스킨 가중치 부드럽게 하기
    705
    706        Args:
    707            inObj: 대상 객체
    708            inVertMode: 버텍스 모드 (기본값: 1)
    709            inRadius: 반경 (기본값: 5.0)
    710            inIterNum: 반복 횟수 (기본값: 3)
    711            inKeepMax: 최대 가중치 유지 여부 (기본값: False)
    712
    713        Returns:
    714            None
    715        """
    716        maxScriptCode = textwrap.dedent(r'''
    717            struct _SmoothSkin (
    718            SmoothSkinMaxUndo = 10,
    719            UndoWeights = #(),
    720            SmoothSkinData = #(#(), #(), #(), #(), #(), #(), #()),
    721            smoothRadius = 5.0,
    722            iterNum = 1,
    723            keepMax = false,
    724
    725            -- vertGroupMode: Edges, Attach, All, Stiff
    726            vertGroupMode = 1,
    727
    728            fn make_rigid_skin skin_mod vert_list =
    729            (
    730                /*
    731                Rigidify vertices weights in skin modifier
    732                */
    733                WeightArray = #()
    734                VertCount = 0
    735                BoneArray = #()
    736                FinalWeight = #()
    737
    738                for v in vert_list do
    739                (
    740                    for CurBone = 1 to (skinOps.GetVertexWeightCount skin_mod v) do
    741                    (
    742                        CurID = (skinOps.GetVertexWeightBoneID skin_mod v CurBone)
    743                        if WeightArray[CurID] == undefined do WeightArray[CurID] = 0
    744
    745                        CurWeight = (skinOps.GetVertexWeight skin_mod v CurBone)
    746                        WeightArray[CurID] += CurWeight
    747                        VertCount += CurWeight
    748                    )
    749
    750                    for i = 1 to WeightArray.count where WeightArray[i] != undefined and WeightArray[i] > 0 do
    751                    (
    752                        NewVal = (WeightArray[i] / VertCount)
    753                        if NewVal > 0.01 do (append BoneArray i; append FinalWeight NewVal)
    754                    )
    755                )
    756                return #(BoneArray, FinalWeight)
    757            ),
    758
    759            fn smooth_skin =
    760            (
    761                if $selection.count != 1 then return false
    762
    763                p = 0
    764                for iter = 1 to iterNum do
    765                (
    766                    p += 1
    767                    if classOf (modPanel.getCurrentObject()) != Skin then return false
    768
    769                    obj = $; skinMod = modPanel.getCurrentObject()
    770                    FinalBoneArray = #(); FinalWeightArray = #(); o = 1
    771
    772                    UseOldData = (obj == SmoothSkinData[1][1]) and (obj.verts.count == SmoothSkinData[1][2])
    773                    if not UseOldData do SmoothSkinData = #(#(), #(), #(), #(), #(), #(), #())
    774                    SmoothSkinData[1][1] = obj; SmoothSkinData[1][2] = obj.verts.count
    775
    776                    tmpObj = copy Obj
    777                    tmpObj.modifiers[skinMod.name].enabled = false
    778
    779                    fn DoNormalizeWeight Weight =
    780                    (
    781                        WeightLength = 0; NormalizeWeight = #()
    782                        for w = 1 to Weight.count do WeightLength += Weight[w]
    783                        if WeightLength != 0 then
    784                            for w = 1 to Weight.count do NormalizeWeight[w] = Weight[w] * (1 / WeightLength)
    785                        else
    786                            NormalizeWeight[1] = 1.0
    787                        return NormalizeWeight
    788                    )
    789
    790                    skinMod.clearZeroLimit = 0.00
    791                    skinOps.RemoveZeroWeights skinMod
    792
    793                    posarray = for a in tmpObj.verts collect a.pos
    794
    795                    if (SmoothSkinData[8] != smoothRadius) do (SmoothSkinData[6] = #(); SmoothSkinData[7] = #())
    796
    797                    for v = 1 to obj.verts.count where (skinOps.IsVertexSelected skinMod v == 1) and (not keepMax or (skinOps.GetVertexWeightCount skinmod v != 1)) do
    798                    (
    799                        VertBros = #{}; VertBrosRatio = #()
    800                        Weightarray = #(); BoneArray = #(); FinalWeight = #()
    801                        WeightArray.count = skinOps.GetNumberBones skinMod
    802
    803                        if vertGroupMode == 1 and (SmoothSkinData[2][v] == undefined) do
    804                        (
    805                            if (classof tmpObj == Editable_Poly) or (classof tmpObj == PolyMeshObject) then
    806                            (
    807                                CurEdges = polyop.GetEdgesUsingVert tmpObj v
    808                                for CE in CurEdges do VertBros += (polyop.getEdgeVerts tmpObj CE) as bitArray
    809                            )
    810                            else
    811                            (
    812                                CurEdges = meshop.GetEdgesUsingvert tmpObj v
    813                                for i in CurEdges do CurEdges[i] = (getEdgeVis tmpObj (1+(i-1)/3)(1+mod (i-1) 3))
    814                                for CE in CurEdges do VertBros += (meshop.getVertsUsingEdge tmpObj CE) as bitArray
    815                            )
    816
    817                            VertBros = VertBros as array
    818                            SmoothSkinData[2][v] = #()
    819                            SmoothSkinData[3][v] = #()
    820
    821                            if VertBros.count > 0 do
    822                            (
    823                                for vb in VertBros do
    824                                (
    825                                    CurDist = distance posarray[v] posarray[vb]
    826                                    if CurDist == 0 then
    827                                        append VertBrosRatio 0
    828                                    else
    829                                        append VertBrosRatio (1 / CurDist)
    830                                )
    831
    832                                VertBrosRatio = DoNormalizeWeight VertBrosRatio
    833                                VertBrosRatio[finditem VertBros v] = 1
    834                                SmoothSkinData[2][v] = VertBros
    835                                SmoothSkinData[3][v] = VertBrosRatio
    836                            )
    837                        )
    838
    839                        if vertGroupMode == 2 do
    840                        (
    841                            SmoothSkinData[4][v] = for vb = 1 to posarray.count where (skinOps.IsVertexSelected skinMod vb == 0) and (distance posarray[v] posarray[vb]) < smoothRadius collect vb
    842                            SmoothSkinData[5][v] = for vb in SmoothSkinData[4][v] collect
    843                                (CurDist = distance posarray[v] posarray[vb]; if CurDist == 0 then 0 else (1 / CurDist))
    844                            SmoothSkinData[5][v] = DoNormalizeWeight SmoothSkinData[5][v]
    845                            for i = 1 to SmoothSkinData[5][v].count do SmoothSkinData[5][v][i] *= 2
    846                        )
    847
    848                        if vertGroupMode == 3 and (SmoothSkinData[6][v] == undefined) do
    849                        (
    850                            SmoothSkinData[6][v] = for vb = 1 to posarray.count where (distance posarray[v] posarray[vb]) < smoothRadius collect vb
    851                            SmoothSkinData[7][v] = for vb in SmoothSkinData[6][v] collect
    852                                (CurDist = distance posarray[v] posarray[vb]; if CurDist == 0 then 0 else (1 / CurDist))
    853                            SmoothSkinData[7][v] = DoNormalizeWeight SmoothSkinData[7][v]
    854                            for i = 1 to SmoothSkinData[7][v].count do SmoothSkinData[7][v][i] *= 2
    855                        )
    856
    857                        if vertGroupMode != 4 do
    858                        (
    859                            VertBros = SmoothSkinData[vertGroupMode * 2][v]
    860                            VertBrosRatio = SmoothSkinData[(vertGroupMode * 2) + 1][v]
    861
    862                            for z = 1 to VertBros.count do
    863                                for CurBone = 1 to (skinOps.GetVertexWeightCount skinMod VertBros[z]) do
    864                                (
    865                                    CurID = (skinOps.GetVertexWeightBoneID skinMod VertBros[z] CurBone)
    866                                    if WeightArray[CurID] == undefined do WeightArray[CurID] = 0
    867                                    WeightArray[CurID] += (skinOps.GetVertexWeight skinMod VertBros[z] CurBone) * VertBrosRatio[z]
    868                                )
    869
    870                            for i = 1 to WeightArray.count where WeightArray[i] != undefined and WeightArray[i] > 0 do
    871                            (
    872                                NewVal = (WeightArray[i] / 2)
    873                                if NewVal > 0.01 do (append BoneArray i; append FinalWeight NewVal)
    874                            )
    875                            FinalBoneArray[v] = BoneArray
    876                            FinalWeightArray[v] = FinalWeight
    877                        )
    878                    )
    879
    880                    if vertGroupMode == 4 then
    881                    (
    882                        convertTopoly tmpObj
    883                        polyObj = tmpObj
    884
    885                        -- Only test selected
    886                        VertSelection = for v = 1 to obj.verts.count where (skinOps.IsVertexSelected skinMod v == 1) collect v
    887                        DoneEdge = (polyobj.edges as bitarray) - polyop.getEdgesUsingVert polyObj VertSelection
    888                        DoneFace = (polyobj.faces as bitarray) - polyop.getFacesUsingVert polyObj VertSelection
    889
    890                        -- Elements
    891                        SmallElements = #()
    892                        for f = 1 to polyobj.faces.count where not DoneFace[f] do
    893                        (
    894                            CurElement = polyop.getElementsUsingFace polyObj #{f}
    895
    896                            CurVerts = polyop.getVertsUsingFace polyobj CurElement; MaxDist = 0
    897                            for v1 in CurVerts do
    898                                for v2 in CurVerts where MaxDist < (smoothRadius * 2) do
    899                                (
    900                                    dist = distance polyobj.verts[v1].pos polyobj.verts[v2].pos
    901                                    if dist > MaxDist do MaxDist = dist
    902                                )
    903                            if MaxDist < (smoothRadius * 2) do append SmallElements CurVerts
    904                            DoneFace += CurElement
    905                        )
    906
    907                        -- Loops
    908                        EdgeLoops = #()
    909                        for ed in SmallElements do DoneEdge += polyop.getEdgesUsingVert polyobj ed
    910                        for ed = 1 to polyobj.edges.count where not DoneEdge[ed] do
    911                        (
    912                            polyobj.selectedEdges = #{ed}
    913                            polyobj.ButtonOp #SelectEdgeLoop
    914                            CurEdgeLoop = (polyobj.selectedEdges as bitarray)
    915                            if CurEdgeLoop.numberSet > 2 do
    916                            (
    917                                CurVerts = (polyop.getvertsusingedge polyobj CurEdgeLoop); MaxDist = 0
    918                                for v1 in CurVerts do
    919                                    for v2 in CurVerts where MaxDist < (smoothRadius * 2) do
    920                                    (
    921                                        dist = distance polyobj.verts[v1].pos polyobj.verts[v2].pos
    922                                        if dist > MaxDist do MaxDist = dist
    923                                    )
    924                                if MaxDist < (smoothRadius * 2) do append EdgeLoops CurVerts
    925                            )
    926                            DoneEdge += CurEdgeLoop
    927                        )
    928
    929                        modPanel.setCurrentObject SkinMod; subobjectLevel = 1
    930                        for z in #(SmallElements, EdgeLoops) do
    931                            for i in z do
    932                            (
    933                                VertList = for v3 in i where (skinOps.IsVertexSelected skinMod v3 == 1) collect v3
    934                                NewWeights = self.make_rigid_skin SkinMod VertList
    935                                for v3 in VertList do (FinalBoneArray[v3] = NewWeights[1]; FinalWeightArray[v3] = NewWeights[2])
    936                            )
    937                    )
    938
    939                    SmoothSkinData[8] = smoothRadius
    940
    941                    delete tmpObj
    942                    OldWeightArray = #(); OldBoneArray = #(); LastWeights = #()
    943                    for sv = 1 to FinalBoneArray.count where FinalBonearray[sv] != undefined and FinalBoneArray[sv].count != 0 do
    944                    (
    945                        -- Home-Made undo
    946                        NumItem = skinOps.GetVertexWeightCount skinMod sv
    947                        OldWeightArray.count = OldBoneArray.count = NumItem
    948                        for CurBone = 1 to NumItem do
    949                        (
    950                            OldBoneArray[CurBone] = (skinOps.GetVertexWeightBoneID skinMod sv CurBone)
    951                            OldWeightArray[CurBone] = (skinOps.GetVertexWeight skinMod sv CurBone)
    952                        )
    953
    954                        append LastWeights #(skinMod, sv, deepcopy OldBoneArray, deepcopy OldWeightArray)
    955                        if UndoWeights.count >= SmoothSkinMaxUndo do deleteItem UndoWeights 1
    956
    957                        skinOps.ReplaceVertexWeights skinMod sv FinalBoneArray[sv] FinalWeightArray[sv]
    958                    )
    959
    960                    append UndoWeights LastWeights
    961
    962                    prog = ((p as float / iterNum as float) * 100.0)
    963                    format "Smoothing Progress:%\n" prog
    964                )
    965            ),
    966
    967            fn undo_smooth_skin = (
    968                CurUndo = UndoWeights[UndoWeights.count]
    969                try(
    970                    if modPanel.GetCurrentObject() != CurUndo[1][1] do (modPanel.setCurrentObject CurUndo[1][1]; subobjectLevel = 1)
    971                    for i in CurUndo do skinOps.ReplaceVertexWeights i[1] i[2] i[3] i[4]
    972                )
    973                catch( print "Undo fail")
    974                deleteitem UndoWeights UndoWeights.count
    975                if UndoWeights.count == 0 then return false
    976            ),
    977
    978            fn setting inVertMode inRadius inIterNum inKeepMax = (
    979                vertGroupMode = inVertMode
    980                smoothRadius = inRadius
    981                iterNum = inIterNum
    982                keepMax = inKeepMax
    983            )
    984        )
    985        ''')
    986
    987        if rt.isValidNode(inObj):
    988            rt.select(inObj)
    989            rt.execute("max modify mode")
    990
    991            targetSkinMod = self.get_skin_mod(inObj)
    992            rt.modPanel.setCurrentObject(targetSkinMod[0])
    993
    994            rt.execute(maxScriptCode)
    995            smooth_skin = rt._SmoothSkin()
    996            smooth_skin.setting(inVertMode.value, inRadius, inIterNum, inKeepMax)
    997            smooth_skin.smooth_skin()

스킨 가중치 부드럽게 하기

Args: inObj: 대상 객체 inVertMode: 버텍스 모드 (기본값: 1) inRadius: 반경 (기본값: 5.0) inIterNum: 반복 횟수 (기본값: 3) inKeepMax: 최대 가중치 유지 여부 (기본값: False)

Returns: None

class Morph: View Source

     24class Morph:
     25    """
     26    모프(Morph) 관련 기능을 제공하는 클래스.
     27    MAXScript의 _Morph 구조체 개념을 Python으로 재구현한 클래스이며,
     28    3ds Max의 기능들을 pymxs API를 통해 제어합니다.
     29    """
     30
     31    def __init__(self):
     32        """클래스 초기화"""
     33        self.channelMaxViewNum = 100
     34
     35    def get_modifier_index(self, inObj):
     36        """
     37        객체에서 Morpher 모디파이어의 인덱스를 찾음
     38
     39        Args:
     40            inObj: 검색할 객체
     41
     42        Returns:
     43            Morpher 모디파이어의 인덱스 (없으면 0)
     44        """
     45        returnVal = 0
     46        if len(inObj.modifiers) > 0:
     47            for i in range(len(inObj.modifiers)):
     48                if rt.classOf(inObj.modifiers[i]) == rt.Morpher:
     49                    returnVal = i + 1  # MaxScript는 1부터 시작하므로 +1 추가
     50
     51        return returnVal
     52
     53    def get_modifier(self, inObj):
     54        """
     55        객체에서 Morpher 모디파이어를 찾음
     56
     57        Args:
     58            inObj: 검색할 객체
     59
     60        Returns:
     61            Morpher 모디파이어 (없으면 None)
     62        """
     63        returnVal = None
     64        modIndex = self.get_modifier_index(inObj)
     65        if modIndex > 0:
     66            returnVal = inObj.modifiers[modIndex - 1]  # Python 인덱스는 0부터 시작하므로 -1 조정
     67
     68        return returnVal
     69
     70    def get_channel_num(self, inObj):
     71        """
     72        객체의 Morpher에 있는 채널 수를 반환
     73
     74        Args:
     75            inObj: 검색할 객체
     76
     77        Returns:
     78            모프 채널 수
     79        """
     80        returnVal = 0
     81        morphMod = self.get_modifier(inObj)
     82        if morphMod is not None:
     83            morphChannelExistance = True
     84            morphChannelCounter = 0
     85
     86            while morphChannelExistance:
     87                for i in range(morphChannelCounter + 1, morphChannelCounter + self.channelMaxViewNum + 1):
     88                    if not rt.WM3_MC_HasData(morphMod, i):
     89                        returnVal = i - 1
     90                        morphChannelExistance = False
     91                        break
     92
     93                morphChannelCounter += self.channelMaxViewNum
     94
     95        return returnVal
     96
     97    def get_all_channel_info(self, inObj):
     98        """
     99        객체의 모든 모프 채널 정보를 가져옴
    100
    101        Args:
    102            inObj: 검색할 객체
    103
    104        Returns:
    105            MorphChannel 객체의 리스트
    106        """
    107        returnVal = []
    108        morphMod = self.get_modifier(inObj)
    109
    110        if morphMod is not None:
    111            channelNum = self.get_channel_num(inObj)
    112            if channelNum > 0:
    113                for i in range(1, channelNum + 1):
    114                    tempChannel = MorphChannel()
    115                    tempChannel.index = i
    116                    tempChannel.hasData = rt.WM3_MC_HasData(morphMod, i)
    117                    tempChannel.name = rt.WM3_MC_GetName(morphMod, i)
    118                    returnVal.append(tempChannel)
    119
    120        return returnVal
    121
    122    def add_target(self, inObj, inTarget, inIndex):
    123        """
    124        특정 인덱스에 모프 타겟 추가
    125
    126        Args:
    127            inObj: 모프를 적용할 객체
    128            inTarget: 타겟 객체
    129            inIndex: 채널 인덱스
    130
    131        Returns:
    132            성공 여부 (True/False)
    133        """
    134        returnVal = False
    135        morphMod = self.get_modifier(inObj)
    136
    137        if morphMod is not None:
    138            rt.WM3_MC_BuildFromNode(morphMod, inIndex, inTarget)
    139            returnVal = rt.WM3_MC_HasData(morphMod, inIndex)
    140
    141        return returnVal
    142
    143    def add_targets(self, inObj, inTargetArray):
    144        """
    145        여러 타겟 객체를 순서대로 모프 채널에 추가
    146
    147        Args:
    148            inObj: 모프를 적용할 객체
    149            inTargetArray: 타겟 객체 배열
    150        """
    151        morphMod = self.get_modifier(inObj)
    152
    153        if morphMod is not None:
    154            for i in range(len(inTargetArray)):
    155                rt.WM3_MC_BuildFromNode(morphMod, i + 1, inTargetArray[i])
    156
    157    def get_all_channel_name(self, inObj):
    158        """
    159        객체의 모든 모프 채널 이름을 가져옴
    160
    161        Args:
    162            inObj: 검색할 객체
    163
    164        Returns:
    165            채널 이름 리스트
    166        """
    167        returnVal = []
    168        channelArray = self.get_all_channel_info(inObj)
    169
    170        if len(channelArray) > 0:
    171            returnVal = [item.name for item in channelArray]
    172
    173        return returnVal
    174
    175    def get_channel_name(self, inObj, inIndex):
    176        """
    177        특정 인덱스의 모프 채널 이름을 가져옴
    178
    179        Args:
    180            inObj: 검색할 객체
    181            inIndex: 채널 인덱스
    182
    183        Returns:
    184            채널 이름 (없으면 빈 문자열)
    185        """
    186        returnVal = ""
    187        channelArray = self.get_all_channel_info(inObj)
    188
    189        try:
    190            if len(channelArray) > 0:
    191                returnVal = channelArray[inIndex - 1].name
    192        except:
    193            returnVal = ""
    194
    195        return returnVal
    196
    197    def get_channelIndex(self, inObj, inName):
    198        """
    199        채널 이름으로 모프 채널 인덱스를 가져옴
    200
    201        Args:
    202            inObj: 검색할 객체
    203            inName: 채널 이름
    204
    205        Returns:
    206            채널 인덱스 (없으면 0)
    207        """
    208        returnVal = 0
    209        channelArray = self.get_all_channel_info(inObj)
    210
    211        if len(channelArray) > 0:
    212            for item in channelArray:
    213                if item.name == inName:
    214                    returnVal = item.index
    215                    break
    216
    217        return returnVal
    218
    219    def get_channel_value_by_name(self, inObj, inName):
    220        """
    221        채널 이름으로 모프 채널 값을 가져옴
    222
    223        Args:
    224            inObj: 검색할 객체
    225            inName: 채널 이름
    226
    227        Returns:
    228            채널 값 (0.0 ~ 100.0)
    229        """
    230        returnVal = 0.0
    231        channelIndex = self.get_channelIndex(inObj, inName)
    232        morphMod = self.get_modifier(inObj)
    233
    234        if channelIndex > 0:
    235            try:
    236                returnVal = rt.WM3_MC_GetValue(morphMod, channelIndex)
    237            except:
    238                returnVal = 0.0
    239
    240        return returnVal
    241
    242    def get_channel_value_by_index(self, inObj, inIndex):
    243        """
    244        인덱스로 모프 채널 값을 가져옴
    245
    246        Args:
    247            inObj: 검색할 객체
    248            inIndex: 채널 인덱스
    249
    250        Returns:
    251            채널 값 (0.0 ~ 100.0)
    252        """
    253        returnVal = 0
    254        morphMod = self.get_modifier(inObj)
    255
    256        if morphMod is not None:
    257            try:
    258                returnVal = rt.WM3_MC_GetValue(morphMod, inIndex)
    259            except:
    260                returnVal = 0
    261
    262        return returnVal
    263
    264    def set_channel_value_by_name(self, inObj, inName, inVal):
    265        """
    266        채널 이름으로 모프 채널 값을 설정
    267
    268        Args:
    269            inObj: 모프를 적용할 객체
    270            inName: 채널 이름
    271            inVal: 설정할 값 (0.0 ~ 100.0)
    272
    273        Returns:
    274            성공 여부 (True/False)
    275        """
    276        returnVal = False
    277        morphMod = self.get_modifier(inObj)
    278        channelIndex = self.get_channelIndex(inObj, inName)
    279
    280        if channelIndex > 0:
    281            try:
    282                rt.WM3_MC_SetValue(morphMod, channelIndex, inVal)
    283                returnVal = True
    284            except:
    285                returnVal = False
    286
    287        return returnVal
    288
    289    def set_channel_value_by_index(self, inObj, inIndex, inVal):
    290        """
    291        인덱스로 모프 채널 값을 설정
    292
    293        Args:
    294            inObj: 모프를 적용할 객체
    295            inIndex: 채널 인덱스
    296            inVal: 설정할 값 (0.0 ~ 100.0)
    297
    298        Returns:
    299            성공 여부 (True/False)
    300        """
    301        returnVal = False
    302        morphMod = self.get_modifier(inObj)
    303
    304        if morphMod is not None:
    305            try:
    306                rt.WM3_MC_SetValue(morphMod, inIndex, inVal)
    307                returnVal = True
    308            except:
    309                returnVal = False
    310
    311        return returnVal
    312
    313    def set_channel_name_by_name(self, inObj, inTargetName, inNewName):
    314        """
    315        채널 이름을 이름으로 검색하여 변경
    316
    317        Args:
    318            inObj: 모프를 적용할 객체
    319            inTargetName: 대상 채널의 현재 이름
    320            inNewName: 설정할 새 이름
    321
    322        Returns:
    323            성공 여부 (True/False)
    324        """
    325        returnVal = False
    326        channelIndex = self.get_channelIndex(inObj, inTargetName)
    327        morphMod = self.get_modifier(inObj)
    328
    329        if channelIndex > 0:
    330            rt.WM3_MC_SetName(morphMod, channelIndex, inNewName)
    331            returnVal = True
    332
    333        return returnVal
    334
    335    def set_channel_name_by_index(self, inObj, inIndex, inName):
    336        """
    337        채널 이름을 인덱스로 검색하여 변경
    338
    339        Args:
    340            inObj: 모프를 적용할 객체
    341            inIndex: 대상 채널 인덱스
    342            inName: 설정할 이름
    343
    344        Returns:
    345            성공 여부 (True/False)
    346        """
    347        returnVal = False
    348        morphMod = self.get_modifier(inObj)
    349
    350        if morphMod is not None:
    351            try:
    352                rt.WM3_MC_SetName(morphMod, inIndex, inName)
    353                returnVal = True
    354            except:
    355                returnVal = False
    356
    357        return returnVal
    358
    359    def reset_all_channel_value(self, inObj):
    360        """
    361        모든 모프 채널 값을 0으로 리셋
    362
    363        Args:
    364            inObj: 리셋할 객체
    365        """
    366        totalChannelNum = self.get_channel_num(inObj)
    367
    368        if totalChannelNum > 0:
    369            for i in range(1, totalChannelNum + 1):
    370                self.set_channel_value_by_index(inObj, i, 0.0)
    371
    372    def extract_morph_channel_geometry(self, obj, _feedback_=False):
    373        """
    374        모프 채널의 기하학적 형태를 추출하여 개별 객체로 생성
    375
    376        Args:
    377            obj: 추출 대상 객체
    378            _feedback_: 피드백 메시지 출력 여부
    379
    380        Returns:
    381            추출된 객체 배열
    382        """
    383        extractedObjs = []
    384        morphMod = self.get_modifier(obj)
    385
    386        if rt.IsValidMorpherMod(morphMod):
    387            # 데이터가 있는 모든 채널 인덱스 수집
    388            channels = [i for i in range(1, rt.WM3_NumberOfChannels(morphMod) + 1)
    389                        if rt.WM3_MC_HasData(morphMod, i)]
    390
    391            for i in channels:
    392                channelName = rt.WM3_MC_GetName(morphMod, i)
    393                rt.WM3_MC_SetValue(morphMod, i, 100.0)
    394
    395                objSnapshot = rt.snapshot(obj)
    396                objSnapshot.name = channelName
    397                extractedObjs.append(objSnapshot)
    398
    399                rt.WM3_MC_SetValue(morphMod, i, 0.0)
    400
    401                if _feedback_:
    402                    print(f" - FUNCTION - [ extract_morph_channel_geometry ] - Extracted ---- {objSnapshot.name} ---- successfully!!")
    403        else:
    404            if _feedback_:
    405                print(f" - FUNCTION - [ extract_morph_channel_geometry ] - No valid morpher found on ---- {obj.name} ---- ")
    406
    407        return extractedObjs

모프(Morph) 관련 기능을 제공하는 클래스. MAXScript의 _Morph 구조체 개념을 Python으로 재구현한 클래스이며, 3ds Max의 기능들을 pymxs API를 통해 제어합니다.

Morph() View Source

    31    def __init__(self):
    32        """클래스 초기화"""
    33        self.channelMaxViewNum = 100

클래스 초기화

channelMaxViewNum
def get_modifier_index(self, inObj): View Source

    35    def get_modifier_index(self, inObj):
    36        """
    37        객체에서 Morpher 모디파이어의 인덱스를 찾음
    38
    39        Args:
    40            inObj: 검색할 객체
    41
    42        Returns:
    43            Morpher 모디파이어의 인덱스 (없으면 0)
    44        """
    45        returnVal = 0
    46        if len(inObj.modifiers) > 0:
    47            for i in range(len(inObj.modifiers)):
    48                if rt.classOf(inObj.modifiers[i]) == rt.Morpher:
    49                    returnVal = i + 1  # MaxScript는 1부터 시작하므로 +1 추가
    50
    51        return returnVal

객체에서 Morpher 모디파이어의 인덱스를 찾음

Args: inObj: 검색할 객체

Returns: Morpher 모디파이어의 인덱스 (없으면 0)

def get_modifier(self, inObj): View Source

    53    def get_modifier(self, inObj):
    54        """
    55        객체에서 Morpher 모디파이어를 찾음
    56
    57        Args:
    58            inObj: 검색할 객체
    59
    60        Returns:
    61            Morpher 모디파이어 (없으면 None)
    62        """
    63        returnVal = None
    64        modIndex = self.get_modifier_index(inObj)
    65        if modIndex > 0:
    66            returnVal = inObj.modifiers[modIndex - 1]  # Python 인덱스는 0부터 시작하므로 -1 조정
    67
    68        return returnVal

객체에서 Morpher 모디파이어를 찾음

Args: inObj: 검색할 객체

Returns: Morpher 모디파이어 (없으면 None)

def get_channel_num(self, inObj): View Source

    70    def get_channel_num(self, inObj):
    71        """
    72        객체의 Morpher에 있는 채널 수를 반환
    73
    74        Args:
    75            inObj: 검색할 객체
    76
    77        Returns:
    78            모프 채널 수
    79        """
    80        returnVal = 0
    81        morphMod = self.get_modifier(inObj)
    82        if morphMod is not None:
    83            morphChannelExistance = True
    84            morphChannelCounter = 0
    85
    86            while morphChannelExistance:
    87                for i in range(morphChannelCounter + 1, morphChannelCounter + self.channelMaxViewNum + 1):
    88                    if not rt.WM3_MC_HasData(morphMod, i):
    89                        returnVal = i - 1
    90                        morphChannelExistance = False
    91                        break
    92
    93                morphChannelCounter += self.channelMaxViewNum
    94
    95        return returnVal

객체의 Morpher에 있는 채널 수를 반환

Args: inObj: 검색할 객체

Returns: 모프 채널 수

def get_all_channel_info(self, inObj): View Source

     97    def get_all_channel_info(self, inObj):
     98        """
     99        객체의 모든 모프 채널 정보를 가져옴
    100
    101        Args:
    102            inObj: 검색할 객체
    103
    104        Returns:
    105            MorphChannel 객체의 리스트
    106        """
    107        returnVal = []
    108        morphMod = self.get_modifier(inObj)
    109
    110        if morphMod is not None:
    111            channelNum = self.get_channel_num(inObj)
    112            if channelNum > 0:
    113                for i in range(1, channelNum + 1):
    114                    tempChannel = MorphChannel()
    115                    tempChannel.index = i
    116                    tempChannel.hasData = rt.WM3_MC_HasData(morphMod, i)
    117                    tempChannel.name = rt.WM3_MC_GetName(morphMod, i)
    118                    returnVal.append(tempChannel)
    119
    120        return returnVal

객체의 모든 모프 채널 정보를 가져옴

Args: inObj: 검색할 객체

Returns: MorphChannel 객체의 리스트

def add_target(self, inObj, inTarget, inIndex): View Source

    122    def add_target(self, inObj, inTarget, inIndex):
    123        """
    124        특정 인덱스에 모프 타겟 추가
    125
    126        Args:
    127            inObj: 모프를 적용할 객체
    128            inTarget: 타겟 객체
    129            inIndex: 채널 인덱스
    130
    131        Returns:
    132            성공 여부 (True/False)
    133        """
    134        returnVal = False
    135        morphMod = self.get_modifier(inObj)
    136
    137        if morphMod is not None:
    138            rt.WM3_MC_BuildFromNode(morphMod, inIndex, inTarget)
    139            returnVal = rt.WM3_MC_HasData(morphMod, inIndex)
    140
    141        return returnVal

특정 인덱스에 모프 타겟 추가

Args: inObj: 모프를 적용할 객체 inTarget: 타겟 객체 inIndex: 채널 인덱스

Returns: 성공 여부 (True/False)

def add_targets(self, inObj, inTargetArray): View Source

    143    def add_targets(self, inObj, inTargetArray):
    144        """
    145        여러 타겟 객체를 순서대로 모프 채널에 추가
    146
    147        Args:
    148            inObj: 모프를 적용할 객체
    149            inTargetArray: 타겟 객체 배열
    150        """
    151        morphMod = self.get_modifier(inObj)
    152
    153        if morphMod is not None:
    154            for i in range(len(inTargetArray)):
    155                rt.WM3_MC_BuildFromNode(morphMod, i + 1, inTargetArray[i])

여러 타겟 객체를 순서대로 모프 채널에 추가

Args: inObj: 모프를 적용할 객체 inTargetArray: 타겟 객체 배열

def get_all_channel_name(self, inObj): View Source

    157    def get_all_channel_name(self, inObj):
    158        """
    159        객체의 모든 모프 채널 이름을 가져옴
    160
    161        Args:
    162            inObj: 검색할 객체
    163
    164        Returns:
    165            채널 이름 리스트
    166        """
    167        returnVal = []
    168        channelArray = self.get_all_channel_info(inObj)
    169
    170        if len(channelArray) > 0:
    171            returnVal = [item.name for item in channelArray]
    172
    173        return returnVal

객체의 모든 모프 채널 이름을 가져옴

Args: inObj: 검색할 객체

Returns: 채널 이름 리스트

def get_channel_name(self, inObj, inIndex): View Source

    175    def get_channel_name(self, inObj, inIndex):
    176        """
    177        특정 인덱스의 모프 채널 이름을 가져옴
    178
    179        Args:
    180            inObj: 검색할 객체
    181            inIndex: 채널 인덱스
    182
    183        Returns:
    184            채널 이름 (없으면 빈 문자열)
    185        """
    186        returnVal = ""
    187        channelArray = self.get_all_channel_info(inObj)
    188
    189        try:
    190            if len(channelArray) > 0:
    191                returnVal = channelArray[inIndex - 1].name
    192        except:
    193            returnVal = ""
    194
    195        return returnVal

특정 인덱스의 모프 채널 이름을 가져옴

Args: inObj: 검색할 객체 inIndex: 채널 인덱스

Returns: 채널 이름 (없으면 빈 문자열)

def get_channelIndex(self, inObj, inName): View Source

    197    def get_channelIndex(self, inObj, inName):
    198        """
    199        채널 이름으로 모프 채널 인덱스를 가져옴
    200
    201        Args:
    202            inObj: 검색할 객체
    203            inName: 채널 이름
    204
    205        Returns:
    206            채널 인덱스 (없으면 0)
    207        """
    208        returnVal = 0
    209        channelArray = self.get_all_channel_info(inObj)
    210
    211        if len(channelArray) > 0:
    212            for item in channelArray:
    213                if item.name == inName:
    214                    returnVal = item.index
    215                    break
    216
    217        return returnVal

채널 이름으로 모프 채널 인덱스를 가져옴

Args: inObj: 검색할 객체 inName: 채널 이름

Returns: 채널 인덱스 (없으면 0)

def get_channel_value_by_name(self, inObj, inName): View Source

    219    def get_channel_value_by_name(self, inObj, inName):
    220        """
    221        채널 이름으로 모프 채널 값을 가져옴
    222
    223        Args:
    224            inObj: 검색할 객체
    225            inName: 채널 이름
    226
    227        Returns:
    228            채널 값 (0.0 ~ 100.0)
    229        """
    230        returnVal = 0.0
    231        channelIndex = self.get_channelIndex(inObj, inName)
    232        morphMod = self.get_modifier(inObj)
    233
    234        if channelIndex > 0:
    235            try:
    236                returnVal = rt.WM3_MC_GetValue(morphMod, channelIndex)
    237            except:
    238                returnVal = 0.0
    239
    240        return returnVal

채널 이름으로 모프 채널 값을 가져옴

Args: inObj: 검색할 객체 inName: 채널 이름

Returns: 채널 값 (0.0 ~ 100.0)

def get_channel_value_by_index(self, inObj, inIndex): View Source

    242    def get_channel_value_by_index(self, inObj, inIndex):
    243        """
    244        인덱스로 모프 채널 값을 가져옴
    245
    246        Args:
    247            inObj: 검색할 객체
    248            inIndex: 채널 인덱스
    249
    250        Returns:
    251            채널 값 (0.0 ~ 100.0)
    252        """
    253        returnVal = 0
    254        morphMod = self.get_modifier(inObj)
    255
    256        if morphMod is not None:
    257            try:
    258                returnVal = rt.WM3_MC_GetValue(morphMod, inIndex)
    259            except:
    260                returnVal = 0
    261
    262        return returnVal

인덱스로 모프 채널 값을 가져옴

Args: inObj: 검색할 객체 inIndex: 채널 인덱스

Returns: 채널 값 (0.0 ~ 100.0)

def set_channel_value_by_name(self, inObj, inName, inVal): View Source

    264    def set_channel_value_by_name(self, inObj, inName, inVal):
    265        """
    266        채널 이름으로 모프 채널 값을 설정
    267
    268        Args:
    269            inObj: 모프를 적용할 객체
    270            inName: 채널 이름
    271            inVal: 설정할 값 (0.0 ~ 100.0)
    272
    273        Returns:
    274            성공 여부 (True/False)
    275        """
    276        returnVal = False
    277        morphMod = self.get_modifier(inObj)
    278        channelIndex = self.get_channelIndex(inObj, inName)
    279
    280        if channelIndex > 0:
    281            try:
    282                rt.WM3_MC_SetValue(morphMod, channelIndex, inVal)
    283                returnVal = True
    284            except:
    285                returnVal = False
    286
    287        return returnVal

채널 이름으로 모프 채널 값을 설정

Args: inObj: 모프를 적용할 객체 inName: 채널 이름 inVal: 설정할 값 (0.0 ~ 100.0)

Returns: 성공 여부 (True/False)

def set_channel_value_by_index(self, inObj, inIndex, inVal): View Source

    289    def set_channel_value_by_index(self, inObj, inIndex, inVal):
    290        """
    291        인덱스로 모프 채널 값을 설정
    292
    293        Args:
    294            inObj: 모프를 적용할 객체
    295            inIndex: 채널 인덱스
    296            inVal: 설정할 값 (0.0 ~ 100.0)
    297
    298        Returns:
    299            성공 여부 (True/False)
    300        """
    301        returnVal = False
    302        morphMod = self.get_modifier(inObj)
    303
    304        if morphMod is not None:
    305            try:
    306                rt.WM3_MC_SetValue(morphMod, inIndex, inVal)
    307                returnVal = True
    308            except:
    309                returnVal = False
    310
    311        return returnVal

인덱스로 모프 채널 값을 설정

Args: inObj: 모프를 적용할 객체 inIndex: 채널 인덱스 inVal: 설정할 값 (0.0 ~ 100.0)

Returns: 성공 여부 (True/False)

def set_channel_name_by_name(self, inObj, inTargetName, inNewName): View Source

    313    def set_channel_name_by_name(self, inObj, inTargetName, inNewName):
    314        """
    315        채널 이름을 이름으로 검색하여 변경
    316
    317        Args:
    318            inObj: 모프를 적용할 객체
    319            inTargetName: 대상 채널의 현재 이름
    320            inNewName: 설정할 새 이름
    321
    322        Returns:
    323            성공 여부 (True/False)
    324        """
    325        returnVal = False
    326        channelIndex = self.get_channelIndex(inObj, inTargetName)
    327        morphMod = self.get_modifier(inObj)
    328
    329        if channelIndex > 0:
    330            rt.WM3_MC_SetName(morphMod, channelIndex, inNewName)
    331            returnVal = True
    332
    333        return returnVal

채널 이름을 이름으로 검색하여 변경

Args: inObj: 모프를 적용할 객체 inTargetName: 대상 채널의 현재 이름 inNewName: 설정할 새 이름

Returns: 성공 여부 (True/False)

def set_channel_name_by_index(self, inObj, inIndex, inName): View Source

    335    def set_channel_name_by_index(self, inObj, inIndex, inName):
    336        """
    337        채널 이름을 인덱스로 검색하여 변경
    338
    339        Args:
    340            inObj: 모프를 적용할 객체
    341            inIndex: 대상 채널 인덱스
    342            inName: 설정할 이름
    343
    344        Returns:
    345            성공 여부 (True/False)
    346        """
    347        returnVal = False
    348        morphMod = self.get_modifier(inObj)
    349
    350        if morphMod is not None:
    351            try:
    352                rt.WM3_MC_SetName(morphMod, inIndex, inName)
    353                returnVal = True
    354            except:
    355                returnVal = False
    356
    357        return returnVal

채널 이름을 인덱스로 검색하여 변경

Args: inObj: 모프를 적용할 객체 inIndex: 대상 채널 인덱스 inName: 설정할 이름

Returns: 성공 여부 (True/False)

def reset_all_channel_value(self, inObj): View Source

    359    def reset_all_channel_value(self, inObj):
    360        """
    361        모든 모프 채널 값을 0으로 리셋
    362
    363        Args:
    364            inObj: 리셋할 객체
    365        """
    366        totalChannelNum = self.get_channel_num(inObj)
    367
    368        if totalChannelNum > 0:
    369            for i in range(1, totalChannelNum + 1):
    370                self.set_channel_value_by_index(inObj, i, 0.0)

모든 모프 채널 값을 0으로 리셋

Args: inObj: 리셋할 객체

def extract_morph_channel_geometry(self, obj, _feedback_=False): View Source

    372    def extract_morph_channel_geometry(self, obj, _feedback_=False):
    373        """
    374        모프 채널의 기하학적 형태를 추출하여 개별 객체로 생성
    375
    376        Args:
    377            obj: 추출 대상 객체
    378            _feedback_: 피드백 메시지 출력 여부
    379
    380        Returns:
    381            추출된 객체 배열
    382        """
    383        extractedObjs = []
    384        morphMod = self.get_modifier(obj)
    385
    386        if rt.IsValidMorpherMod(morphMod):
    387            # 데이터가 있는 모든 채널 인덱스 수집
    388            channels = [i for i in range(1, rt.WM3_NumberOfChannels(morphMod) + 1)
    389                        if rt.WM3_MC_HasData(morphMod, i)]
    390
    391            for i in channels:
    392                channelName = rt.WM3_MC_GetName(morphMod, i)
    393                rt.WM3_MC_SetValue(morphMod, i, 100.0)
    394
    395                objSnapshot = rt.snapshot(obj)
    396                objSnapshot.name = channelName
    397                extractedObjs.append(objSnapshot)
    398
    399                rt.WM3_MC_SetValue(morphMod, i, 0.0)
    400
    401                if _feedback_:
    402                    print(f" - FUNCTION - [ extract_morph_channel_geometry ] - Extracted ---- {objSnapshot.name} ---- successfully!!")
    403        else:
    404            if _feedback_:
    405                print(f" - FUNCTION - [ extract_morph_channel_geometry ] - No valid morpher found on ---- {obj.name} ---- ")
    406
    407        return extractedObjs

모프 채널의 기하학적 형태를 추출하여 개별 객체로 생성

Args: obj: 추출 대상 객체 _feedback_: 피드백 메시지 출력 여부

Returns: 추출된 객체 배열

class TwistBone: View Source

     24class TwistBone:
     25    """
     26    트위스트 뼈대(Twist Bone) 관련 기능을 제공하는 클래스.
     27
     28    이 클래스는 3ds Max에서 트위스트 뼈대를 생성하고 제어하는 다양한 기능을 제공합니다.
     29    MAXScript의 _TwistBone 구조체 개념을 Python으로 재구현한 클래스이며,
     30    3ds Max의 기능들을 pymxs API를 통해 제어합니다.
     31
     32    트위스트 뼈대는 상체(Upper)와 하체(Lower) 두 가지 타입으로 생성이 가능하며,
     33    각각 다른 회전 표현식을 사용하여 자연스러운 회전 움직임을 구현합니다.
     34    """
     35
     36    def __init__(self, nameService=None, animService=None, constraintService=None, bipService=None, boneService=None):
     37        """
     38        TwistBone 클래스 초기화.
     39
     40        의존성 주입 방식으로 필요한 서비스들을 외부에서 제공받거나 내부에서 생성합니다.
     41        서비스들이 제공되지 않을 경우 각 서비스의 기본 인스턴스를 생성하여 사용합니다.
     42
     43        Args:
     44            nameService (Name, optional): 이름 처리 서비스. 기본값은 None이며, 제공되지 않으면 새로 생성됩니다.
     45            animService (Anim, optional): 애니메이션 서비스. 기본값은 None이며, 제공되지 않으면 새로 생성됩니다.
     46            constraintService (Constraint, optional): 제약 서비스. 기본값은 None이며, 제공되지 않으면 새로 생성됩니다.
     47            bipService (Bip, optional): 바이페드 서비스. 기본값은 None이며, 제공되지 않으면 새로 생성됩니다.
     48            boneService (Bone, optional): 뼈대 서비스. 기본값은 None이며, 제공되지 않으면 새로 생성됩니다.
     49        """
     50        self.name = nameService if nameService else Name()
     51        self.anim = animService if animService else Anim()
     52        # Ensure dependent services use the potentially newly created instances
     53        self.const = constraintService if constraintService else Constraint(nameService=self.name)
     54        self.bip = bipService if bipService else Bip(animService=self.anim, nameService=self.name)
     55        self.bone = boneService if boneService else Bone(nameService=self.name, animService=self.anim)
     56
     57        # 객체 속성 초기화
     58        self.limb = None
     59        self.child = None
     60        self.twistNum = 0
     61        self.bones = []
     62        self.twistType = ""
     63
     64        self.upperTwistBoneExpression = (
     65            "localTm = limb.transform * (inverse limbParent.transform)\n"
     66            "tm = localTm * inverse(localRefTm)\n"
     67            "\n"
     68            "q = tm.rotation\n"
     69            "\n"
     70            "axis = [1,0,0]\n"
     71            "proj = (dot q.axis axis) * axis\n"
     72            "twist = quat q.angle proj\n"
     73            "twist = normalize twist\n"
     74            "--swing = tm.rotation * (inverse twist)\n"
     75            "\n"
     76            "inverse twist\n"
     77        )
     78
     79        self.lowerTwistBoneExpression = (
     80            "localTm = limb.transform * (inverse limbParent.transform)\n"
     81            "tm = localTm * inverse(localRefTm)\n"
     82            "\n"
     83            "q = tm.rotation\n"
     84            "\n"
     85            "axis = [1,0,0]\n"
     86            "proj = (dot q.axis axis) * axis\n"
     87            "twist = quat q.angle proj\n"
     88            "twist = normalize twist\n"
     89            "--swing = tm.rotation * (inverse twist)\n"
     90            "\n"
     91            "twist\n"
     92        )
     93
     94    def reset(self):
     95        """
     96        클래스의 주요 컴포넌트들을 초기화합니다.
     97        서비스가 아닌 클래스 자체의 작업 데이터를 초기화하는 함수입니다.
     98
     99        Returns:
    100            self: 메소드 체이닝을 위한 자기 자신 반환
    101        """
    102        self.limb = None
    103        self.child = None
    104        self.twistNum = 0
    105        self.bones = []
    106        self.twistType = ""
    107
    108        return self
    109
    110    def create_upper_limb_bones(self, inObj, inChild, twistNum=4):
    111        """
    112        상체(팔, 어깨 등) 부분의 트위스트 뼈대를 생성하는 메소드.
    113
    114        상체용 트위스트 뼈대는 부모 객체(inObj)의 위치에서 시작하여
    115        자식 객체(inChild) 방향으로 여러 개의 뼈대를 생성합니다.
    116        생성된 뼈대들은 스크립트 컨트롤러를 통해 자동으로 회전되어
    117        자연스러운 트위스트 움직임을 표현합니다.
    118
    119        Args:
    120            inObj: 트위스트 뼈대의 부모 객체(뼈). 일반적으로 상완 또는 대퇴부에 해당합니다.
    121            inChild: 자식 객체(뼈). 일반적으로 전완 또는 하퇴부에 해당합니다.
    122            twistNum (int, optional): 생성할 트위스트 뼈대의 개수. 기본값은 4입니다.
    123
    124        Returns:
    125            dict: 생성된 트위스트 뼈대 정보를 담고 있는 사전 객체입니다.
    126                 "Bones": 생성된 뼈대 객체들의 배열
    127                 "Type": "Upper" (상체 타입)
    128                 "Limb": 부모 객체 참조
    129                 "Child": 자식 객체 참조
    130                 "TwistNum": 생성된 트위스트 뼈대 개수
    131        """
    132        limb = inObj
    133        distance = rt.distance(limb, inChild)
    134        facingDirVec = inChild.transform.position - inObj.transform.position
    135        inObjXAxisVec = inObj.objectTransform.row1
    136        distanceDir = 1.0 if rt.dot(inObjXAxisVec, facingDirVec) > 0 else -1.0
    137        offssetAmount = (distance / twistNum) * distanceDir
    138        weightVal = 100.0 / (twistNum-1)
    139
    140        boneChainArray = []
    141
    142        # 첫 번째 트위스트 뼈대 생성
    143        boneName = self.name.add_suffix_to_real_name(inObj.name, self.name._get_filtering_char(inObj.name) + "Twist")
    144        if inObj.name[0].islower():
    145            boneName = boneName.lower()
    146        twistBone = self.bone.create_nub_bone(boneName, 2)
    147        twistBone.name = self.name.replace_name_part("Index", boneName, "1")
    148        twistBone.name = self.name.remove_name_part("Nub", twistBone.name)
    149        twistBone.transform = limb.transform
    150        twistBone.parent = limb
    151        twistBoneLocalRefTM = limb.transform * rt.inverse(limb.parent.transform)
    152
    153        twistBoneRotListController = self.const.assign_rot_list(twistBone)
    154        twistBoneController = rt.Rotation_Script()
    155        twistBoneController.addConstant("localRefTm", twistBoneLocalRefTM)
    156        twistBoneController.addNode("limb", limb)
    157        twistBoneController.addNode("limbParent", limb.parent)
    158        twistBoneController.setExpression(self.upperTwistBoneExpression)
    159        twistBoneController.update()
    160
    161        rt.setPropertyController(twistBoneRotListController, "Available", twistBoneController)
    162        twistBoneRotListController.delete(1)
    163        twistBoneRotListController.setActive(twistBoneRotListController.count)
    164        twistBoneRotListController.weight[0] = 100.0
    165
    166        boneChainArray.append(twistBone)
    167
    168        if twistNum > 1:
    169            lastBone = self.bone.create_nub_bone(boneName, 2)
    170            lastBone.name = self.name.replace_name_part("Index", boneName, str(twistNum))
    171            lastBone.name = self.name.remove_name_part("Nub", lastBone.name)
    172            lastBone.transform = limb.transform
    173            lastBone.parent = limb
    174            self.anim.move_local(lastBone, offssetAmount*(twistNum-1), 0, 0)
    175
    176            if twistNum > 2:
    177                for i in range(1, twistNum-1):
    178                    twistExtraBone = self.bone.create_nub_bone(boneName, 2)
    179                    twistExtraBone.name = self.name.replace_name_part("Index", boneName, str(i+1))
    180                    twistExtraBone.name = self.name.remove_name_part("Nub", twistExtraBone.name)
    181                    twistExtraBone.transform = limb.transform
    182                    twistExtraBone.parent = limb
    183                    self.anim.move_local(twistExtraBone, offssetAmount*i, 0, 0)
    184
    185                    twistExtraBoneRotListController = self.const.assign_rot_list(twistExtraBone)
    186                    twistExtraBoneController = rt.Rotation_Script()
    187                    twistExtraBoneController.addConstant("localRefTm", twistBoneLocalRefTM)
    188                    twistExtraBoneController.addNode("limb", limb)
    189                    twistExtraBoneController.addNode("limbParent", limb.parent)
    190                    twistExtraBoneController.setExpression(self.upperTwistBoneExpression)
    191
    192                    rt.setPropertyController(twistExtraBoneRotListController, "Available", twistExtraBoneController)
    193                    twistExtraBoneRotListController.delete(1)
    194                    twistExtraBoneRotListController.setActive(twistExtraBoneRotListController.count)
    195                    twistExtraBoneRotListController.weight[0] = weightVal * (twistNum-1-i)
    196
    197                    boneChainArray.append(twistExtraBone)
    198
    199            boneChainArray.append(lastBone)
    200
    201        # 결과를 멤버 변수에 저장
    202        self.limb = inObj
    203        self.child = inChild
    204        self.twistNum = twistNum
    205        self.bones = boneChainArray
    206        self.twistType = "Upper"
    207
    208        returnVal = {
    209            "Bones": boneChainArray,
    210            "Type": "Upper",
    211            "Limb": inObj,
    212            "Child": inChild,
    213            "TwistNum": twistNum
    214        }
    215
    216        # 메소드 호출 후 데이터 초기화
    217        self.reset()
    218
    219        return returnVal
    220
    221    def create_lower_limb_bones(self, inObj, inChild, twistNum=4):
    222        """
    223        하체(팔뚝, 다리 등) 부분의 트위스트 뼈대를 생성하는 메소드.
    224
    225        하체용 트위스트 뼈대는 부모 객체(inObj)의 위치에서 시작하여
    226        자식 객체(inChild) 쪽으로 여러 개의 뼈대를 생성합니다.
    227        상체와는 다른 회전 표현식을 사용하여 하체에 적합한 트위스트 움직임을 구현합니다.
    228
    229        Args:
    230            inObj: 트위스트 뼈대의 부모 객체(뼈). 일반적으로 전완 또는 하퇴부에 해당합니다.
    231            inChild: 자식 객체(뼈). 일반적으로 손목 또는 발목에 해당합니다.
    232            twistNum (int, optional): 생성할 트위스트 뼈대의 개수. 기본값은 4입니다.
    233
    234        Returns:
    235            dict: 생성된 트위스트 뼈대 정보를 담고 있는 사전 객체입니다.
    236                 "Bones": 생성된 뼈대 객체들의 배열
    237                 "Type": "Lower" (하체 타입)
    238                 "Limb": 부모 객체 참조
    239                 "Child": 자식 객체 참조
    240                 "TwistNum": 생성된 트위스트 뼈대 개수
    241        """
    242        limb = inChild
    243        distance = rt.distance(inObj, inChild)
    244        facingDirVec = inChild.transform.position - inObj.transform.position
    245        inObjXAxisVec = inObj.objectTransform.row1
    246        distanceDir = 1.0 if rt.dot(inObjXAxisVec, facingDirVec) > 0 else -1.0
    247        offssetAmount = (distance / twistNum) * distanceDir
    248        weightVal = 100.0 / (twistNum-1)
    249
    250        boneChainArray = []
    251
    252        # 첫 번째 트위스트 뼈대 생성
    253        boneName = self.name.add_suffix_to_real_name(inObj.name, self.name._get_filtering_char(inObj.name) + "Twist")
    254        if inObj.name[0].islower():
    255            boneName = boneName.lower()
    256        twistBone = self.bone.create_nub_bone(boneName, 2)
    257        twistBone.name = self.name.replace_name_part("Index", boneName, "1")
    258        twistBone.name = self.name.remove_name_part("Nub", twistBone.name)
    259        twistBone.transform = inObj.transform
    260        twistBone.parent = inObj
    261        self.anim.move_local(twistBone, offssetAmount*(twistNum-1), 0, 0)
    262        twistBoneLocalRefTM = limb.transform * rt.inverse(limb.parent.transform)
    263
    264        twistBoneRotListController = self.const.assign_rot_list(twistBone)
    265        twistBoneController = rt.Rotation_Script()
    266        twistBoneController.addConstant("localRefTm", twistBoneLocalRefTM)
    267        twistBoneController.addNode("limb", limb)
    268        twistBoneController.addNode("limbParent", limb.parent)
    269        twistBoneController.setExpression(self.lowerTwistBoneExpression)
    270        twistBoneController.update()
    271
    272        rt.setPropertyController(twistBoneRotListController, "Available", twistBoneController)
    273        twistBoneRotListController.delete(1)
    274        twistBoneRotListController.setActive(twistBoneRotListController.count)
    275        twistBoneRotListController.weight[0] = 100.0
    276
    277        if twistNum > 1:
    278            lastBone = self.bone.create_nub_bone(boneName, 2)
    279            lastBone.name = self.name.replace_name_part("Index", boneName, str(twistNum))
    280            lastBone.name = self.name.remove_name_part("Nub", lastBone.name)
    281            lastBone.transform = inObj.transform
    282            lastBone.parent = inObj
    283            self.anim.move_local(lastBone, 0, 0, 0)
    284
    285            if twistNum > 2:
    286                for i in range(1, twistNum-1):
    287                    twistExtraBone = self.bone.create_nub_bone(boneName, 2)
    288                    twistExtraBone.name = self.name.replace_name_part("Index", boneName, str(i+1))
    289                    twistExtraBone.name = self.name.remove_name_part("Nub", twistExtraBone.name)
    290                    twistExtraBone.transform = inObj.transform
    291                    twistExtraBone.parent = inObj
    292                    self.anim.move_local(twistExtraBone, offssetAmount*(twistNum-1-i), 0, 0)
    293
    294                    twistExtraBoneRotListController = self.const.assign_rot_list(twistExtraBone)
    295                    twistExtraBoneController = rt.Rotation_Script()
    296                    twistExtraBoneController.addConstant("localRefTm", twistBoneLocalRefTM)
    297                    twistExtraBoneController.addNode("limb", limb)
    298                    twistExtraBoneController.addNode("limbParent", limb.parent)
    299                    twistExtraBoneController.setExpression(self.lowerTwistBoneExpression)
    300
    301                    rt.setPropertyController(twistExtraBoneRotListController, "Available", twistExtraBoneController)
    302                    twistExtraBoneRotListController.delete(1)
    303                    twistExtraBoneRotListController.setActive(twistExtraBoneRotListController.count)
    304                    twistExtraBoneRotListController.weight[0] = weightVal * (twistNum-1-i)
    305
    306                    boneChainArray.append(twistExtraBone)
    307
    308            boneChainArray.append(lastBone)
    309
    310        # 결과를 멤버 변수에 저장
    311        self.limb = inObj
    312        self.child = inChild
    313        self.twistNum = twistNum
    314        self.bones = boneChainArray
    315        self.twistType = "Lower"
    316
    317        returnVal = {
    318            "Bones": boneChainArray,
    319            "Type": "Lower",
    320            "Limb": inObj,
    321            "Child": inChild,
    322            "TwistNum": twistNum
    323        }
    324
    325        # 메소드 호출 후 데이터 초기화
    326        self.reset()
    327
    328        return returnVal

트위스트 뼈대(Twist Bone) 관련 기능을 제공하는 클래스.

이 클래스는 3ds Max에서 트위스트 뼈대를 생성하고 제어하는 다양한 기능을 제공합니다. MAXScript의 _TwistBone 구조체 개념을 Python으로 재구현한 클래스이며, 3ds Max의 기능들을 pymxs API를 통해 제어합니다.

트위스트 뼈대는 상체(Upper)와 하체(Lower) 두 가지 타입으로 생성이 가능하며, 각각 다른 회전 표현식을 사용하여 자연스러운 회전 움직임을 구현합니다.

TwistBone( nameService=None, animService=None, constraintService=None, bipService=None, boneService=None) View Source

    36    def __init__(self, nameService=None, animService=None, constraintService=None, bipService=None, boneService=None):
    37        """
    38        TwistBone 클래스 초기화.
    39
    40        의존성 주입 방식으로 필요한 서비스들을 외부에서 제공받거나 내부에서 생성합니다.
    41        서비스들이 제공되지 않을 경우 각 서비스의 기본 인스턴스를 생성하여 사용합니다.
    42
    43        Args:
    44            nameService (Name, optional): 이름 처리 서비스. 기본값은 None이며, 제공되지 않으면 새로 생성됩니다.
    45            animService (Anim, optional): 애니메이션 서비스. 기본값은 None이며, 제공되지 않으면 새로 생성됩니다.
    46            constraintService (Constraint, optional): 제약 서비스. 기본값은 None이며, 제공되지 않으면 새로 생성됩니다.
    47            bipService (Bip, optional): 바이페드 서비스. 기본값은 None이며, 제공되지 않으면 새로 생성됩니다.
    48            boneService (Bone, optional): 뼈대 서비스. 기본값은 None이며, 제공되지 않으면 새로 생성됩니다.
    49        """
    50        self.name = nameService if nameService else Name()
    51        self.anim = animService if animService else Anim()
    52        # Ensure dependent services use the potentially newly created instances
    53        self.const = constraintService if constraintService else Constraint(nameService=self.name)
    54        self.bip = bipService if bipService else Bip(animService=self.anim, nameService=self.name)
    55        self.bone = boneService if boneService else Bone(nameService=self.name, animService=self.anim)
    56
    57        # 객체 속성 초기화
    58        self.limb = None
    59        self.child = None
    60        self.twistNum = 0
    61        self.bones = []
    62        self.twistType = ""
    63
    64        self.upperTwistBoneExpression = (
    65            "localTm = limb.transform * (inverse limbParent.transform)\n"
    66            "tm = localTm * inverse(localRefTm)\n"
    67            "\n"
    68            "q = tm.rotation\n"
    69            "\n"
    70            "axis = [1,0,0]\n"
    71            "proj = (dot q.axis axis) * axis\n"
    72            "twist = quat q.angle proj\n"
    73            "twist = normalize twist\n"
    74            "--swing = tm.rotation * (inverse twist)\n"
    75            "\n"
    76            "inverse twist\n"
    77        )
    78
    79        self.lowerTwistBoneExpression = (
    80            "localTm = limb.transform * (inverse limbParent.transform)\n"
    81            "tm = localTm * inverse(localRefTm)\n"
    82            "\n"
    83            "q = tm.rotation\n"
    84            "\n"
    85            "axis = [1,0,0]\n"
    86            "proj = (dot q.axis axis) * axis\n"
    87            "twist = quat q.angle proj\n"
    88            "twist = normalize twist\n"
    89            "--swing = tm.rotation * (inverse twist)\n"
    90            "\n"
    91            "twist\n"
    92        )

TwistBone 클래스 초기화.

의존성 주입 방식으로 필요한 서비스들을 외부에서 제공받거나 내부에서 생성합니다. 서비스들이 제공되지 않을 경우 각 서비스의 기본 인스턴스를 생성하여 사용합니다.

Args: nameService (Name, optional): 이름 처리 서비스. 기본값은 None이며, 제공되지 않으면 새로 생성됩니다. animService (Anim, optional): 애니메이션 서비스. 기본값은 None이며, 제공되지 않으면 새로 생성됩니다. constraintService (Constraint, optional): 제약 서비스. 기본값은 None이며, 제공되지 않으면 새로 생성됩니다. bipService (Bip, optional): 바이페드 서비스. 기본값은 None이며, 제공되지 않으면 새로 생성됩니다. boneService (Bone, optional): 뼈대 서비스. 기본값은 None이며, 제공되지 않으면 새로 생성됩니다.

name
anim
const
bip
bone
limb
child
twistNum
bones
twistType
upperTwistBoneExpression
lowerTwistBoneExpression
def reset(self): View Source

     94    def reset(self):
     95        """
     96        클래스의 주요 컴포넌트들을 초기화합니다.
     97        서비스가 아닌 클래스 자체의 작업 데이터를 초기화하는 함수입니다.
     98
     99        Returns:
    100            self: 메소드 체이닝을 위한 자기 자신 반환
    101        """
    102        self.limb = None
    103        self.child = None
    104        self.twistNum = 0
    105        self.bones = []
    106        self.twistType = ""
    107
    108        return self

클래스의 주요 컴포넌트들을 초기화합니다. 서비스가 아닌 클래스 자체의 작업 데이터를 초기화하는 함수입니다.

Returns: self: 메소드 체이닝을 위한 자기 자신 반환

def create_upper_limb_bones(self, inObj, inChild, twistNum=4): View Source

    110    def create_upper_limb_bones(self, inObj, inChild, twistNum=4):
    111        """
    112        상체(팔, 어깨 등) 부분의 트위스트 뼈대를 생성하는 메소드.
    113
    114        상체용 트위스트 뼈대는 부모 객체(inObj)의 위치에서 시작하여
    115        자식 객체(inChild) 방향으로 여러 개의 뼈대를 생성합니다.
    116        생성된 뼈대들은 스크립트 컨트롤러를 통해 자동으로 회전되어
    117        자연스러운 트위스트 움직임을 표현합니다.
    118
    119        Args:
    120            inObj: 트위스트 뼈대의 부모 객체(뼈). 일반적으로 상완 또는 대퇴부에 해당합니다.
    121            inChild: 자식 객체(뼈). 일반적으로 전완 또는 하퇴부에 해당합니다.
    122            twistNum (int, optional): 생성할 트위스트 뼈대의 개수. 기본값은 4입니다.
    123
    124        Returns:
    125            dict: 생성된 트위스트 뼈대 정보를 담고 있는 사전 객체입니다.
    126                 "Bones": 생성된 뼈대 객체들의 배열
    127                 "Type": "Upper" (상체 타입)
    128                 "Limb": 부모 객체 참조
    129                 "Child": 자식 객체 참조
    130                 "TwistNum": 생성된 트위스트 뼈대 개수
    131        """
    132        limb = inObj
    133        distance = rt.distance(limb, inChild)
    134        facingDirVec = inChild.transform.position - inObj.transform.position
    135        inObjXAxisVec = inObj.objectTransform.row1
    136        distanceDir = 1.0 if rt.dot(inObjXAxisVec, facingDirVec) > 0 else -1.0
    137        offssetAmount = (distance / twistNum) * distanceDir
    138        weightVal = 100.0 / (twistNum-1)
    139
    140        boneChainArray = []
    141
    142        # 첫 번째 트위스트 뼈대 생성
    143        boneName = self.name.add_suffix_to_real_name(inObj.name, self.name._get_filtering_char(inObj.name) + "Twist")
    144        if inObj.name[0].islower():
    145            boneName = boneName.lower()
    146        twistBone = self.bone.create_nub_bone(boneName, 2)
    147        twistBone.name = self.name.replace_name_part("Index", boneName, "1")
    148        twistBone.name = self.name.remove_name_part("Nub", twistBone.name)
    149        twistBone.transform = limb.transform
    150        twistBone.parent = limb
    151        twistBoneLocalRefTM = limb.transform * rt.inverse(limb.parent.transform)
    152
    153        twistBoneRotListController = self.const.assign_rot_list(twistBone)
    154        twistBoneController = rt.Rotation_Script()
    155        twistBoneController.addConstant("localRefTm", twistBoneLocalRefTM)
    156        twistBoneController.addNode("limb", limb)
    157        twistBoneController.addNode("limbParent", limb.parent)
    158        twistBoneController.setExpression(self.upperTwistBoneExpression)
    159        twistBoneController.update()
    160
    161        rt.setPropertyController(twistBoneRotListController, "Available", twistBoneController)
    162        twistBoneRotListController.delete(1)
    163        twistBoneRotListController.setActive(twistBoneRotListController.count)
    164        twistBoneRotListController.weight[0] = 100.0
    165
    166        boneChainArray.append(twistBone)
    167
    168        if twistNum > 1:
    169            lastBone = self.bone.create_nub_bone(boneName, 2)
    170            lastBone.name = self.name.replace_name_part("Index", boneName, str(twistNum))
    171            lastBone.name = self.name.remove_name_part("Nub", lastBone.name)
    172            lastBone.transform = limb.transform
    173            lastBone.parent = limb
    174            self.anim.move_local(lastBone, offssetAmount*(twistNum-1), 0, 0)
    175
    176            if twistNum > 2:
    177                for i in range(1, twistNum-1):
    178                    twistExtraBone = self.bone.create_nub_bone(boneName, 2)
    179                    twistExtraBone.name = self.name.replace_name_part("Index", boneName, str(i+1))
    180                    twistExtraBone.name = self.name.remove_name_part("Nub", twistExtraBone.name)
    181                    twistExtraBone.transform = limb.transform
    182                    twistExtraBone.parent = limb
    183                    self.anim.move_local(twistExtraBone, offssetAmount*i, 0, 0)
    184
    185                    twistExtraBoneRotListController = self.const.assign_rot_list(twistExtraBone)
    186                    twistExtraBoneController = rt.Rotation_Script()
    187                    twistExtraBoneController.addConstant("localRefTm", twistBoneLocalRefTM)
    188                    twistExtraBoneController.addNode("limb", limb)
    189                    twistExtraBoneController.addNode("limbParent", limb.parent)
    190                    twistExtraBoneController.setExpression(self.upperTwistBoneExpression)
    191
    192                    rt.setPropertyController(twistExtraBoneRotListController, "Available", twistExtraBoneController)
    193                    twistExtraBoneRotListController.delete(1)
    194                    twistExtraBoneRotListController.setActive(twistExtraBoneRotListController.count)
    195                    twistExtraBoneRotListController.weight[0] = weightVal * (twistNum-1-i)
    196
    197                    boneChainArray.append(twistExtraBone)
    198
    199            boneChainArray.append(lastBone)
    200
    201        # 결과를 멤버 변수에 저장
    202        self.limb = inObj
    203        self.child = inChild
    204        self.twistNum = twistNum
    205        self.bones = boneChainArray
    206        self.twistType = "Upper"
    207
    208        returnVal = {
    209            "Bones": boneChainArray,
    210            "Type": "Upper",
    211            "Limb": inObj,
    212            "Child": inChild,
    213            "TwistNum": twistNum
    214        }
    215
    216        # 메소드 호출 후 데이터 초기화
    217        self.reset()
    218
    219        return returnVal

상체(팔, 어깨 등) 부분의 트위스트 뼈대를 생성하는 메소드.

상체용 트위스트 뼈대는 부모 객체(inObj)의 위치에서 시작하여 자식 객체(inChild) 방향으로 여러 개의 뼈대를 생성합니다. 생성된 뼈대들은 스크립트 컨트롤러를 통해 자동으로 회전되어 자연스러운 트위스트 움직임을 표현합니다.

Args: inObj: 트위스트 뼈대의 부모 객체(뼈). 일반적으로 상완 또는 대퇴부에 해당합니다. inChild: 자식 객체(뼈). 일반적으로 전완 또는 하퇴부에 해당합니다. twistNum (int, optional): 생성할 트위스트 뼈대의 개수. 기본값은 4입니다.

Returns: dict: 생성된 트위스트 뼈대 정보를 담고 있는 사전 객체입니다. "Bones": 생성된 뼈대 객체들의 배열 "Type": "Upper" (상체 타입) "Limb": 부모 객체 참조 "Child": 자식 객체 참조 "TwistNum": 생성된 트위스트 뼈대 개수

def create_lower_limb_bones(self, inObj, inChild, twistNum=4): View Source

    221    def create_lower_limb_bones(self, inObj, inChild, twistNum=4):
    222        """
    223        하체(팔뚝, 다리 등) 부분의 트위스트 뼈대를 생성하는 메소드.
    224
    225        하체용 트위스트 뼈대는 부모 객체(inObj)의 위치에서 시작하여
    226        자식 객체(inChild) 쪽으로 여러 개의 뼈대를 생성합니다.
    227        상체와는 다른 회전 표현식을 사용하여 하체에 적합한 트위스트 움직임을 구현합니다.
    228
    229        Args:
    230            inObj: 트위스트 뼈대의 부모 객체(뼈). 일반적으로 전완 또는 하퇴부에 해당합니다.
    231            inChild: 자식 객체(뼈). 일반적으로 손목 또는 발목에 해당합니다.
    232            twistNum (int, optional): 생성할 트위스트 뼈대의 개수. 기본값은 4입니다.
    233
    234        Returns:
    235            dict: 생성된 트위스트 뼈대 정보를 담고 있는 사전 객체입니다.
    236                 "Bones": 생성된 뼈대 객체들의 배열
    237                 "Type": "Lower" (하체 타입)
    238                 "Limb": 부모 객체 참조
    239                 "Child": 자식 객체 참조
    240                 "TwistNum": 생성된 트위스트 뼈대 개수
    241        """
    242        limb = inChild
    243        distance = rt.distance(inObj, inChild)
    244        facingDirVec = inChild.transform.position - inObj.transform.position
    245        inObjXAxisVec = inObj.objectTransform.row1
    246        distanceDir = 1.0 if rt.dot(inObjXAxisVec, facingDirVec) > 0 else -1.0
    247        offssetAmount = (distance / twistNum) * distanceDir
    248        weightVal = 100.0 / (twistNum-1)
    249
    250        boneChainArray = []
    251
    252        # 첫 번째 트위스트 뼈대 생성
    253        boneName = self.name.add_suffix_to_real_name(inObj.name, self.name._get_filtering_char(inObj.name) + "Twist")
    254        if inObj.name[0].islower():
    255            boneName = boneName.lower()
    256        twistBone = self.bone.create_nub_bone(boneName, 2)
    257        twistBone.name = self.name.replace_name_part("Index", boneName, "1")
    258        twistBone.name = self.name.remove_name_part("Nub", twistBone.name)
    259        twistBone.transform = inObj.transform
    260        twistBone.parent = inObj
    261        self.anim.move_local(twistBone, offssetAmount*(twistNum-1), 0, 0)
    262        twistBoneLocalRefTM = limb.transform * rt.inverse(limb.parent.transform)
    263
    264        twistBoneRotListController = self.const.assign_rot_list(twistBone)
    265        twistBoneController = rt.Rotation_Script()
    266        twistBoneController.addConstant("localRefTm", twistBoneLocalRefTM)
    267        twistBoneController.addNode("limb", limb)
    268        twistBoneController.addNode("limbParent", limb.parent)
    269        twistBoneController.setExpression(self.lowerTwistBoneExpression)
    270        twistBoneController.update()
    271
    272        rt.setPropertyController(twistBoneRotListController, "Available", twistBoneController)
    273        twistBoneRotListController.delete(1)
    274        twistBoneRotListController.setActive(twistBoneRotListController.count)
    275        twistBoneRotListController.weight[0] = 100.0
    276
    277        if twistNum > 1:
    278            lastBone = self.bone.create_nub_bone(boneName, 2)
    279            lastBone.name = self.name.replace_name_part("Index", boneName, str(twistNum))
    280            lastBone.name = self.name.remove_name_part("Nub", lastBone.name)
    281            lastBone.transform = inObj.transform
    282            lastBone.parent = inObj
    283            self.anim.move_local(lastBone, 0, 0, 0)
    284
    285            if twistNum > 2:
    286                for i in range(1, twistNum-1):
    287                    twistExtraBone = self.bone.create_nub_bone(boneName, 2)
    288                    twistExtraBone.name = self.name.replace_name_part("Index", boneName, str(i+1))
    289                    twistExtraBone.name = self.name.remove_name_part("Nub", twistExtraBone.name)
    290                    twistExtraBone.transform = inObj.transform
    291                    twistExtraBone.parent = inObj
    292                    self.anim.move_local(twistExtraBone, offssetAmount*(twistNum-1-i), 0, 0)
    293
    294                    twistExtraBoneRotListController = self.const.assign_rot_list(twistExtraBone)
    295                    twistExtraBoneController = rt.Rotation_Script()
    296                    twistExtraBoneController.addConstant("localRefTm", twistBoneLocalRefTM)
    297                    twistExtraBoneController.addNode("limb", limb)
    298                    twistExtraBoneController.addNode("limbParent", limb.parent)
    299                    twistExtraBoneController.setExpression(self.lowerTwistBoneExpression)
    300
    301                    rt.setPropertyController(twistExtraBoneRotListController, "Available", twistExtraBoneController)
    302                    twistExtraBoneRotListController.delete(1)
    303                    twistExtraBoneRotListController.setActive(twistExtraBoneRotListController.count)
    304                    twistExtraBoneRotListController.weight[0] = weightVal * (twistNum-1-i)
    305
    306                    boneChainArray.append(twistExtraBone)
    307
    308            boneChainArray.append(lastBone)
    309
    310        # 결과를 멤버 변수에 저장
    311        self.limb = inObj
    312        self.child = inChild
    313        self.twistNum = twistNum
    314        self.bones = boneChainArray
    315        self.twistType = "Lower"
    316
    317        returnVal = {
    318            "Bones": boneChainArray,
    319            "Type": "Lower",
    320            "Limb": inObj,
    321            "Child": inChild,
    322            "TwistNum": twistNum
    323        }
    324
    325        # 메소드 호출 후 데이터 초기화
    326        self.reset()
    327
    328        return returnVal

하체(팔뚝, 다리 등) 부분의 트위스트 뼈대를 생성하는 메소드.

하체용 트위스트 뼈대는 부모 객체(inObj)의 위치에서 시작하여 자식 객체(inChild) 쪽으로 여러 개의 뼈대를 생성합니다. 상체와는 다른 회전 표현식을 사용하여 하체에 적합한 트위스트 움직임을 구현합니다.

Args: inObj: 트위스트 뼈대의 부모 객체(뼈). 일반적으로 전완 또는 하퇴부에 해당합니다. inChild: 자식 객체(뼈). 일반적으로 손목 또는 발목에 해당합니다. twistNum (int, optional): 생성할 트위스트 뼈대의 개수. 기본값은 4입니다.

Returns: dict: 생성된 트위스트 뼈대 정보를 담고 있는 사전 객체입니다. "Bones": 생성된 뼈대 객체들의 배열 "Type": "Lower" (하체 타입) "Limb": 부모 객체 참조 "Child": 자식 객체 참조 "TwistNum": 생성된 트위스트 뼈대 개수

class TwistBoneChain: View Source

     52class TwistBoneChain:
     53    def __init__(self, inResult):
     54        """
     55        클래스 초기화.
     56
     57        Args:
     58            bones: 트위스트 뼈대 체인을 구성하는 뼈대 배열 (기본값: None)
     59        """
     60        self.bones = inResult["Bones"]
     61        self.type = inResult["Type"]
     62        self.limb = inResult["Limb"]
     63        self.child = inResult["Child"]
     64        self.twistNum = inResult["TwistNum"]
     65
     66    def get_bone_at_index(self, index):
     67        """
     68        지정된 인덱스의 트위스트 뼈대 가져오기
     69
     70        Args:
     71            index: 가져올 뼈대의 인덱스
     72
     73        Returns:
     74            해당 인덱스의 뼈대 객체 또는 None (인덱스가 범위를 벗어난 경우)
     75        """
     76        if 0 <= index < len(self.bones):
     77            return self.bones[index]
     78        return None
     79
     80    def get_first_bone(self):
     81        """
     82        체인의 첫 번째 트위스트 뼈대 가져오기
     83
     84        Returns:
     85            첫 번째 뼈대 객체 또는 None (체인이 비어있는 경우)
     86        """
     87        return self.bones[0] if self.bones else None
     88
     89    def get_last_bone(self):
     90        """
     91        체인의 마지막 트위스트 뼈대 가져오기
     92
     93        Returns:
     94            마지막 뼈대 객체 또는 None (체인이 비어있는 경우)
     95        """
     96        return self.bones[-1] if self.bones else None
     97
     98    def get_count(self):
     99        """
    100        체인의 트위스트 뼈대 개수 가져오기
    101
    102        Returns:
    103            뼈대 개수
    104        """
    105        return self.twistNum
    106
    107    def is_empty(self):
    108        """
    109        체인이 비어있는지 확인
    110
    111        Returns:
    112            체인이 비어있으면 True, 아니면 False
    113        """
    114        return len(self.bones) == 0
    115
    116    def clear(self):
    117        """체인의 모든 뼈대 제거"""
    118        self.bones = []
    119
    120    def delete_all(self):
    121        """
    122        체인의 모든 뼈대를 3ds Max 씬에서 삭제
    123
    124        Returns:
    125            삭제 성공 여부 (boolean)
    126        """
    127        if not self.bones:
    128            return False
    129
    130        try:
    131            for bone in self.bones:
    132                rt.delete(bone)
    133            self.clear()
    134            return True
    135        except:
    136            return False
    137
    138    def get_type(self):
    139        """
    140        트위스트 뼈대 체인의 타입을 반환합니다.
    141
    142        Returns:
    143            트위스트 뼈대 체인의 타입 ('upperArm', 'foreArm', 'thigh', 'calf', 'bend' 중 하나) 또는 None
    144        """
    145        return self.type
    146
    147    @classmethod
    148    def from_twist_bone_result(cls, inResult):
    149        """
    150        TwistBone 클래스의 결과로부터 TwistBoneChain 인스턴스 생성
    151
    152        Args:
    153            twist_bone_result: TwistBone 클래스의 메서드가 반환한 뼈대 배열
    154            source_bone: 원본 뼈대 객체 (기본값: None)
    155            type_name: 트위스트 뼈대 타입 (기본값: None)
    156
    157        Returns:
    158            TwistBoneChain 인스턴스
    159        """
    160        chain = cls(inResult)
    161
    162        return chain

TwistBoneChain(inResult) View Source

    53    def __init__(self, inResult):
    54        """
    55        클래스 초기화.
    56
    57        Args:
    58            bones: 트위스트 뼈대 체인을 구성하는 뼈대 배열 (기본값: None)
    59        """
    60        self.bones = inResult["Bones"]
    61        self.type = inResult["Type"]
    62        self.limb = inResult["Limb"]
    63        self.child = inResult["Child"]
    64        self.twistNum = inResult["TwistNum"]

클래스 초기화.

Args: bones: 트위스트 뼈대 체인을 구성하는 뼈대 배열 (기본값: None)

bones
type
limb
child
twistNum
def get_bone_at_index(self, index): View Source

    66    def get_bone_at_index(self, index):
    67        """
    68        지정된 인덱스의 트위스트 뼈대 가져오기
    69
    70        Args:
    71            index: 가져올 뼈대의 인덱스
    72
    73        Returns:
    74            해당 인덱스의 뼈대 객체 또는 None (인덱스가 범위를 벗어난 경우)
    75        """
    76        if 0 <= index < len(self.bones):
    77            return self.bones[index]
    78        return None

지정된 인덱스의 트위스트 뼈대 가져오기

Args: index: 가져올 뼈대의 인덱스

Returns: 해당 인덱스의 뼈대 객체 또는 None (인덱스가 범위를 벗어난 경우)

def get_first_bone(self): View Source

    80    def get_first_bone(self):
    81        """
    82        체인의 첫 번째 트위스트 뼈대 가져오기
    83
    84        Returns:
    85            첫 번째 뼈대 객체 또는 None (체인이 비어있는 경우)
    86        """
    87        return self.bones[0] if self.bones else None

체인의 첫 번째 트위스트 뼈대 가져오기

Returns: 첫 번째 뼈대 객체 또는 None (체인이 비어있는 경우)

def get_last_bone(self): View Source

    89    def get_last_bone(self):
    90        """
    91        체인의 마지막 트위스트 뼈대 가져오기
    92
    93        Returns:
    94            마지막 뼈대 객체 또는 None (체인이 비어있는 경우)
    95        """
    96        return self.bones[-1] if self.bones else None

체인의 마지막 트위스트 뼈대 가져오기

Returns: 마지막 뼈대 객체 또는 None (체인이 비어있는 경우)

def get_count(self): View Source

     98    def get_count(self):
     99        """
    100        체인의 트위스트 뼈대 개수 가져오기
    101
    102        Returns:
    103            뼈대 개수
    104        """
    105        return self.twistNum

체인의 트위스트 뼈대 개수 가져오기

Returns: 뼈대 개수

def is_empty(self): View Source

    107    def is_empty(self):
    108        """
    109        체인이 비어있는지 확인
    110
    111        Returns:
    112            체인이 비어있으면 True, 아니면 False
    113        """
    114        return len(self.bones) == 0

체인이 비어있는지 확인

Returns: 체인이 비어있으면 True, 아니면 False

def clear(self): View Source

    116    def clear(self):
    117        """체인의 모든 뼈대 제거"""
    118        self.bones = []

체인의 모든 뼈대 제거

def delete_all(self): View Source

    120    def delete_all(self):
    121        """
    122        체인의 모든 뼈대를 3ds Max 씬에서 삭제
    123
    124        Returns:
    125            삭제 성공 여부 (boolean)
    126        """
    127        if not self.bones:
    128            return False
    129
    130        try:
    131            for bone in self.bones:
    132                rt.delete(bone)
    133            self.clear()
    134            return True
    135        except:
    136            return False

체인의 모든 뼈대를 3ds Max 씬에서 삭제

Returns: 삭제 성공 여부 (boolean)

def get_type(self): View Source

    138    def get_type(self):
    139        """
    140        트위스트 뼈대 체인의 타입을 반환합니다.
    141
    142        Returns:
    143            트위스트 뼈대 체인의 타입 ('upperArm', 'foreArm', 'thigh', 'calf', 'bend' 중 하나) 또는 None
    144        """
    145        return self.type

트위스트 뼈대 체인의 타입을 반환합니다.

Returns: 트위스트 뼈대 체인의 타입 ('upperArm', 'foreArm', 'thigh', 'calf', 'bend' 중 하나) 또는 None

@classmethod
def from_twist_bone_result(cls, inResult): View Source

    147    @classmethod
    148    def from_twist_bone_result(cls, inResult):
    149        """
    150        TwistBone 클래스의 결과로부터 TwistBoneChain 인스턴스 생성
    151
    152        Args:
    153            twist_bone_result: TwistBone 클래스의 메서드가 반환한 뼈대 배열
    154            source_bone: 원본 뼈대 객체 (기본값: None)
    155            type_name: 트위스트 뼈대 타입 (기본값: None)
    156
    157        Returns:
    158            TwistBoneChain 인스턴스
    159        """
    160        chain = cls(inResult)
    161
    162        return chain

TwistBone 클래스의 결과로부터 TwistBoneChain 인스턴스 생성

Args: twist_bone_result: TwistBone 클래스의 메서드가 반환한 뼈대 배열 source_bone: 원본 뼈대 객체 (기본값: None) type_name: 트위스트 뼈대 타입 (기본값: None)

Returns: TwistBoneChain 인스턴스

class GroinBone: View Source

     18class GroinBone:
     19    """
     20    고간 부 본 관련 기능을 위한 클래스
     21    3DS Max에서 고간 부 본을 생성하고 관리하는 기능을 제공합니다.
     22    """
     23
     24    def __init__(self, nameService=None, animService=None, constraintService=None, boneService=None, helperService=None):
     25        """
     26        클래스 초기화.
     27
     28        Args:
     29            nameService: 이름 처리 서비스 (제공되지 않으면 새로 생성)
     30            animService: 애니메이션 서비스 (제공되지 않으면 새로 생성)
     31            constraintService: 제약 서비스 (제공되지 않으면 새로 생성)
     32            bipService: Biped 서비스 (제공되지 않으면 새로 생성)
     33            boneService: 뼈대 서비스 (제공되지 않으면 새로 생성)
     34            twistBoneService: 트위스트 본 서비스 (제공되지 않으면 새로 생성)
     35            helperService: 헬퍼 객체 서비스 (제공되지 않으면 새로 생성)
     36        """
     37        # 서비스 인스턴스 설정 또는 생성
     38        self.name = nameService if nameService else Name()
     39        self.anim = animService if animService else Anim()
     40
     41        # 종속성이 있는 서비스들은 이미 생성된 서비스들을 전달
     42        self.const = constraintService if constraintService else Constraint(nameService=self.name)
     43        self.bone = boneService if boneService else Bone(nameService=self.name, animService=self.anim)
     44        self.helper = helperService if helperService else Helper(nameService=self.name)
     45
     46        # 초기화된 결과를 저장할 변수들
     47        self.pelvis = None
     48        self.lThighTwist = None
     49        self.rThighTwist = None
     50        self.bones = []
     51        self.helpers = []
     52        self.pelvisWeight = 40.0
     53        self.thighWeight = 60.0
     54
     55    def reset(self):
     56        """
     57        클래스의 주요 컴포넌트들을 초기화합니다.
     58        서비스가 아닌 클래스 자체의 작업 데이터를 초기화하는 함수입니다.
     59
     60        Returns:
     61            self: 메소드 체이닝을 위한 자기 자신 반환
     62        """
     63        self.pelvis = None
     64        self.lThighTwist = None
     65        self.rThighTwist = None
     66        self.bones = []
     67        self.helpers = []
     68        self.pelvisWeight = 40.0
     69        self.thighWeight = 60.0
     70
     71        return self
     72
     73    def create_bone(self, inPelvis, inLThighTwist, inRThighTwist, inPelvisWeight=40.0, inThighWeight=60.0):
     74        """
     75        고간 부 본을 생성하는 메소드.
     76
     77        Args:
     78            inPelvis: Biped 객체
     79            inPelvisWeight: 골반 가중치 (기본값: 40.0)
     80            inThighWeight: 허벅지 가중치 (기본값: 60.0)
     81
     82        Returns:
     83            성공 여부 (Boolean)
     84        """
     85        returnVal = {
     86            "Pelvis": None,
     87            "LThighTwist": None,
     88            "RThighTwist": None,
     89            "Bones": [],
     90            "Helpers": [],
     91            "PelvisWeight": inPelvisWeight,
     92            "ThighWeight": inThighWeight
     93        }
     94        if rt.isValidNode(inPelvis) == False or rt.isValidNode(inLThighTwist) == False or rt.isValidNode(inRThighTwist) == False:
     95            rt.messageBox("There is no valid node.")
     96            return False
     97
     98        groinName = "Groin"
     99        if inPelvis.name[0].islower():
    100            groinName = groinName.lower()
    101
    102        groinBaseName = self.name.add_suffix_to_real_name(inPelvis.name, self.name._get_filtering_char(inLThighTwist.name) + groinName)
    103
    104        pelvisHelperName = self.name.replace_name_part("Type", groinBaseName, self.name.get_name_part_value_by_description("Type", "Dummy"))
    105        pelvisHelperName = self.name.replace_name_part("Index", pelvisHelperName, "00")
    106        pelvisHelper = self.helper.create_point(pelvisHelperName)
    107        pelvisHelper.transform = inPelvis.transform
    108        self.anim.rotate_local(pelvisHelper, 0.0, 0.0, -180.0)
    109        pelvisHelper.parent = inPelvis
    110        self.helper.set_shape_to_box(pelvisHelper)
    111
    112        lThighTwistHelperName = self.name.replace_name_part("Type", groinBaseName, self.name.get_name_part_value_by_description("Type", "Dummy"))
    113        lThighTwistHelperName = self.name.replace_name_part("Side", lThighTwistHelperName, self.name.get_name_part_value_by_description("Side", "Left"))
    114        lThighTwistHelperName = self.name.replace_name_part("Index", lThighTwistHelperName, "00")
    115        lThighTwistHelper = self.helper.create_point(lThighTwistHelperName)
    116        lThighTwistHelper.transform = pelvisHelper.transform
    117        lThighTwistHelper.position = inLThighTwist.position
    118        lThighTwistHelper.parent = inLThighTwist
    119        self.helper.set_shape_to_box(lThighTwistHelper)
    120
    121        rThighTwistHelperName = self.name.replace_name_part("Type", groinBaseName, self.name.get_name_part_value_by_description("Type", "Dummy"))
    122        rThighTwistHelperName = self.name.replace_name_part("Side", rThighTwistHelperName, self.name.get_name_part_value_by_description("Side", "Right"))
    123        rThighTwistHelperName = self.name.replace_name_part("Index", rThighTwistHelperName, "00")
    124        rThighTwistHelper = self.helper.create_point(rThighTwistHelperName)
    125        rThighTwistHelper.transform = pelvisHelper.transform
    126        rThighTwistHelper.position = inRThighTwist.position
    127        rThighTwistHelper.parent = inRThighTwist
    128        self.helper.set_shape_to_box(rThighTwistHelper)
    129
    130        groinBoneName = self.name.replace_name_part("Index", groinBaseName, "00")
    131        groinBones = self.bone.create_simple_bone(3.0, groinBoneName, size=2)
    132        groinBones[0].transform = pelvisHelper.transform
    133        groinBones[0].parent = inPelvis
    134
    135        self.const.assign_rot_const_multi(groinBones[0], [pelvisHelper, lThighTwistHelper, rThighTwistHelper])
    136        rotConst = self.const.get_rot_list_controller(groinBones[0])[1]
    137        rotConst.setWeight(1, inPelvisWeight)
    138        rotConst.setWeight(2, inThighWeight/2.0)
    139        rotConst.setWeight(3, inThighWeight/2.0)
    140
    141        # 결과를 멤버 변수에 저장
    142        self.pelvis = inPelvis
    143        self.lThighTwist = inLThighTwist
    144        self.rThighTwist = inRThighTwist
    145        self.bones = groinBones
    146        self.helpers = [pelvisHelper, lThighTwistHelper, rThighTwistHelper]
    147        self.pelvisWeight = inPelvisWeight
    148        self.thighWeight = inThighWeight
    149
    150        returnVal["Pelvis"] = inPelvis
    151        returnVal["LThighTwist"] = inLThighTwist
    152        returnVal["RThighTwist"] = inRThighTwist
    153        returnVal["Bones"] = groinBones
    154        returnVal["Helpers"] = [pelvisHelper, lThighTwistHelper, rThighTwistHelper]
    155        returnVal["PelvisWeight"] = inPelvisWeight
    156        returnVal["ThighWeight"] = inThighWeight
    157
    158        # 메소드 호출 후 데이터 초기화
    159        self.reset()
    160
    161        return returnVal

고간 부 본 관련 기능을 위한 클래스 3DS Max에서 고간 부 본을 생성하고 관리하는 기능을 제공합니다.

GroinBone( nameService=None, animService=None, constraintService=None, boneService=None, helperService=None) View Source

    24    def __init__(self, nameService=None, animService=None, constraintService=None, boneService=None, helperService=None):
    25        """
    26        클래스 초기화.
    27
    28        Args:
    29            nameService: 이름 처리 서비스 (제공되지 않으면 새로 생성)
    30            animService: 애니메이션 서비스 (제공되지 않으면 새로 생성)
    31            constraintService: 제약 서비스 (제공되지 않으면 새로 생성)
    32            bipService: Biped 서비스 (제공되지 않으면 새로 생성)
    33            boneService: 뼈대 서비스 (제공되지 않으면 새로 생성)
    34            twistBoneService: 트위스트 본 서비스 (제공되지 않으면 새로 생성)
    35            helperService: 헬퍼 객체 서비스 (제공되지 않으면 새로 생성)
    36        """
    37        # 서비스 인스턴스 설정 또는 생성
    38        self.name = nameService if nameService else Name()
    39        self.anim = animService if animService else Anim()
    40
    41        # 종속성이 있는 서비스들은 이미 생성된 서비스들을 전달
    42        self.const = constraintService if constraintService else Constraint(nameService=self.name)
    43        self.bone = boneService if boneService else Bone(nameService=self.name, animService=self.anim)
    44        self.helper = helperService if helperService else Helper(nameService=self.name)
    45
    46        # 초기화된 결과를 저장할 변수들
    47        self.pelvis = None
    48        self.lThighTwist = None
    49        self.rThighTwist = None
    50        self.bones = []
    51        self.helpers = []
    52        self.pelvisWeight = 40.0
    53        self.thighWeight = 60.0

클래스 초기화.

Args: nameService: 이름 처리 서비스 (제공되지 않으면 새로 생성) animService: 애니메이션 서비스 (제공되지 않으면 새로 생성) constraintService: 제약 서비스 (제공되지 않으면 새로 생성) bipService: Biped 서비스 (제공되지 않으면 새로 생성) boneService: 뼈대 서비스 (제공되지 않으면 새로 생성) twistBoneService: 트위스트 본 서비스 (제공되지 않으면 새로 생성) helperService: 헬퍼 객체 서비스 (제공되지 않으면 새로 생성)

name
anim
const
bone
helper
pelvis
lThighTwist
rThighTwist
bones
helpers
pelvisWeight
thighWeight
def reset(self): View Source

    55    def reset(self):
    56        """
    57        클래스의 주요 컴포넌트들을 초기화합니다.
    58        서비스가 아닌 클래스 자체의 작업 데이터를 초기화하는 함수입니다.
    59
    60        Returns:
    61            self: 메소드 체이닝을 위한 자기 자신 반환
    62        """
    63        self.pelvis = None
    64        self.lThighTwist = None
    65        self.rThighTwist = None
    66        self.bones = []
    67        self.helpers = []
    68        self.pelvisWeight = 40.0
    69        self.thighWeight = 60.0
    70
    71        return self

클래스의 주요 컴포넌트들을 초기화합니다. 서비스가 아닌 클래스 자체의 작업 데이터를 초기화하는 함수입니다.

Returns: self: 메소드 체이닝을 위한 자기 자신 반환

def create_bone( self, inPelvis, inLThighTwist, inRThighTwist, inPelvisWeight=40.0, inThighWeight=60.0): View Source

     73    def create_bone(self, inPelvis, inLThighTwist, inRThighTwist, inPelvisWeight=40.0, inThighWeight=60.0):
     74        """
     75        고간 부 본을 생성하는 메소드.
     76
     77        Args:
     78            inPelvis: Biped 객체
     79            inPelvisWeight: 골반 가중치 (기본값: 40.0)
     80            inThighWeight: 허벅지 가중치 (기본값: 60.0)
     81
     82        Returns:
     83            성공 여부 (Boolean)
     84        """
     85        returnVal = {
     86            "Pelvis": None,
     87            "LThighTwist": None,
     88            "RThighTwist": None,
     89            "Bones": [],
     90            "Helpers": [],
     91            "PelvisWeight": inPelvisWeight,
     92            "ThighWeight": inThighWeight
     93        }
     94        if rt.isValidNode(inPelvis) == False or rt.isValidNode(inLThighTwist) == False or rt.isValidNode(inRThighTwist) == False:
     95            rt.messageBox("There is no valid node.")
     96            return False
     97
     98        groinName = "Groin"
     99        if inPelvis.name[0].islower():
    100            groinName = groinName.lower()
    101
    102        groinBaseName = self.name.add_suffix_to_real_name(inPelvis.name, self.name._get_filtering_char(inLThighTwist.name) + groinName)
    103
    104        pelvisHelperName = self.name.replace_name_part("Type", groinBaseName, self.name.get_name_part_value_by_description("Type", "Dummy"))
    105        pelvisHelperName = self.name.replace_name_part("Index", pelvisHelperName, "00")
    106        pelvisHelper = self.helper.create_point(pelvisHelperName)
    107        pelvisHelper.transform = inPelvis.transform
    108        self.anim.rotate_local(pelvisHelper, 0.0, 0.0, -180.0)
    109        pelvisHelper.parent = inPelvis
    110        self.helper.set_shape_to_box(pelvisHelper)
    111
    112        lThighTwistHelperName = self.name.replace_name_part("Type", groinBaseName, self.name.get_name_part_value_by_description("Type", "Dummy"))
    113        lThighTwistHelperName = self.name.replace_name_part("Side", lThighTwistHelperName, self.name.get_name_part_value_by_description("Side", "Left"))
    114        lThighTwistHelperName = self.name.replace_name_part("Index", lThighTwistHelperName, "00")
    115        lThighTwistHelper = self.helper.create_point(lThighTwistHelperName)
    116        lThighTwistHelper.transform = pelvisHelper.transform
    117        lThighTwistHelper.position = inLThighTwist.position
    118        lThighTwistHelper.parent = inLThighTwist
    119        self.helper.set_shape_to_box(lThighTwistHelper)
    120
    121        rThighTwistHelperName = self.name.replace_name_part("Type", groinBaseName, self.name.get_name_part_value_by_description("Type", "Dummy"))
    122        rThighTwistHelperName = self.name.replace_name_part("Side", rThighTwistHelperName, self.name.get_name_part_value_by_description("Side", "Right"))
    123        rThighTwistHelperName = self.name.replace_name_part("Index", rThighTwistHelperName, "00")
    124        rThighTwistHelper = self.helper.create_point(rThighTwistHelperName)
    125        rThighTwistHelper.transform = pelvisHelper.transform
    126        rThighTwistHelper.position = inRThighTwist.position
    127        rThighTwistHelper.parent = inRThighTwist
    128        self.helper.set_shape_to_box(rThighTwistHelper)
    129
    130        groinBoneName = self.name.replace_name_part("Index", groinBaseName, "00")
    131        groinBones = self.bone.create_simple_bone(3.0, groinBoneName, size=2)
    132        groinBones[0].transform = pelvisHelper.transform
    133        groinBones[0].parent = inPelvis
    134
    135        self.const.assign_rot_const_multi(groinBones[0], [pelvisHelper, lThighTwistHelper, rThighTwistHelper])
    136        rotConst = self.const.get_rot_list_controller(groinBones[0])[1]
    137        rotConst.setWeight(1, inPelvisWeight)
    138        rotConst.setWeight(2, inThighWeight/2.0)
    139        rotConst.setWeight(3, inThighWeight/2.0)
    140
    141        # 결과를 멤버 변수에 저장
    142        self.pelvis = inPelvis
    143        self.lThighTwist = inLThighTwist
    144        self.rThighTwist = inRThighTwist
    145        self.bones = groinBones
    146        self.helpers = [pelvisHelper, lThighTwistHelper, rThighTwistHelper]
    147        self.pelvisWeight = inPelvisWeight
    148        self.thighWeight = inThighWeight
    149
    150        returnVal["Pelvis"] = inPelvis
    151        returnVal["LThighTwist"] = inLThighTwist
    152        returnVal["RThighTwist"] = inRThighTwist
    153        returnVal["Bones"] = groinBones
    154        returnVal["Helpers"] = [pelvisHelper, lThighTwistHelper, rThighTwistHelper]
    155        returnVal["PelvisWeight"] = inPelvisWeight
    156        returnVal["ThighWeight"] = inThighWeight
    157
    158        # 메소드 호출 후 데이터 초기화
    159        self.reset()
    160
    161        return returnVal

고간 부 본을 생성하는 메소드.

Args: inPelvis: Biped 객체 inPelvisWeight: 골반 가중치 (기본값: 40.0) inThighWeight: 허벅지 가중치 (기본값: 60.0)

Returns: 성공 여부 (Boolean)

class GroinBoneChain: View Source

     44class GroinBoneChain:
     45    def __init__(self, inResult):
     46        """
     47        클래스 초기화.
     48
     49        Args:
     50            bones: 고간 부 본 체인을 구성하는 뼈대 배열 (기본값: None)
     51            helpers: 고간 부 본과 연관된 헬퍼 객체 배열 (기본값: None)
     52            biped_obj: 연관된 Biped 객체 (기본값: None)
     53        """
     54        self.pelvis =inResult["Pelvis"]
     55        self.lThighTwist = inResult["LThighTwist"]
     56        self.rThighTwist = inResult["RThighTwist"]
     57        self.bones = inResult["Bones"]
     58        self.helpers = inResult["Helpers"]
     59        self.pelvisWeight = inResult["PelvisWeight"]
     60        self.thighWeight = inResult["ThighWeight"]
     61
     62    def is_empty(self):
     63        """
     64        체인이 비어있는지 확인
     65
     66        Returns:
     67            본과 헬퍼가 모두 비어있으면 True, 아니면 False
     68        """
     69        return len(self.bones) == 0 and len(self.helpers) == 0
     70
     71    def clear(self):
     72        """체인의 모든 본과 헬퍼 참조 제거"""
     73        self.bones = []
     74        self.helpers = []
     75        self.pelvis = None
     76        self.lThighTwist = None
     77        self.rThighTwist = None
     78        self.pelvisWeight = 40.0  # 기본 골반 가중치
     79        self.thighWeight = 60.0   # 기본 허벅지 가중치
     80
     81    def delete(self):
     82        """
     83        체인의 모든 본과 헬퍼를 3ds Max 씬에서 삭제
     84
     85        Returns:
     86            삭제 성공 여부 (boolean)
     87        """
     88        if self.is_empty():
     89            return False
     90
     91        try:
     92            rt.delete(self.bones)
     93            rt.delete(self.helpers)
     94            return True
     95        except:
     96            return False
     97
     98    def delete_all(self):
     99        """
    100        체인의 모든 본과 헬퍼를 3ds Max 씬에서 삭제
    101
    102        Returns:
    103            삭제 성공 여부 (boolean)
    104        """
    105        if self.is_empty():
    106            return False
    107
    108        try:
    109            rt.delete(self.bones)
    110            rt.delete(self.helpers)
    111            self.clear()
    112            return True
    113        except:
    114            return False
    115
    116    def update_weights(self, pelvisWeight=None, thighWeight=None):
    117        """
    118        고간 부 본의 가중치 업데이트
    119
    120        Args:
    121            pelvisWeight: 골반 가중치 (None인 경우 현재 값 유지)
    122            thighWeight: 허벅지 가중치 (None인 경우 현재 값 유지)
    123
    124        Returns:
    125            업데이트 성공 여부 (boolean)
    126        """
    127        if self.is_empty():
    128            return False
    129
    130        # 새 가중치 설정
    131        if pelvisWeight is not None:
    132            self.pelvisWeight = pelvisWeight
    133        if thighWeight is not None:
    134            self.thighWeight = thighWeight
    135
    136        self.delete()
    137        result = jal.groinBone.create_bone(
    138            self.pelvis,
    139            self.lThighTwist,
    140            self.rThighTwist,
    141            self.pelvisWeight,
    142            self.thighWeight
    143        )
    144        self.bones = result["Bones"]
    145        self.helpers = result["Helpers"]
    146
    147    def get_weights(self):
    148        """
    149        현재 설정된 가중치 값 가져오기
    150
    151        Returns:
    152            (pelvis_weight, thigh_weight) 형태의 튜플
    153        """
    154        return (self.pelvis_weight, self.thigh_weight)
    155
    156    @classmethod
    157    def from_groin_bone_result(cls, inResult):
    158        """
    159        GroinBone 클래스의 결과로부터 GroinBoneChain 인스턴스 생성
    160
    161        Args:
    162            bones: GroinBone 클래스가 생성한 뼈대 배열
    163            helpers: GroinBone 클래스가 생성한 헬퍼 배열
    164            biped_obj: 연관된 Biped 객체 (기본값: None)
    165            pelvisWeight: 골반 가중치 (기본값: 40.0)
    166            thighWeight: 허벅지 가중치 (기본값: 60.0)
    167
    168        Returns:
    169            GroinBoneChain 인스턴스
    170        """
    171        chain = cls(inResult)
    172
    173        return chain

GroinBoneChain(inResult) View Source

    45    def __init__(self, inResult):
    46        """
    47        클래스 초기화.
    48
    49        Args:
    50            bones: 고간 부 본 체인을 구성하는 뼈대 배열 (기본값: None)
    51            helpers: 고간 부 본과 연관된 헬퍼 객체 배열 (기본값: None)
    52            biped_obj: 연관된 Biped 객체 (기본값: None)
    53        """
    54        self.pelvis =inResult["Pelvis"]
    55        self.lThighTwist = inResult["LThighTwist"]
    56        self.rThighTwist = inResult["RThighTwist"]
    57        self.bones = inResult["Bones"]
    58        self.helpers = inResult["Helpers"]
    59        self.pelvisWeight = inResult["PelvisWeight"]
    60        self.thighWeight = inResult["ThighWeight"]

클래스 초기화.

Args: bones: 고간 부 본 체인을 구성하는 뼈대 배열 (기본값: None) helpers: 고간 부 본과 연관된 헬퍼 객체 배열 (기본값: None) biped_obj: 연관된 Biped 객체 (기본값: None)

pelvis
lThighTwist
rThighTwist
bones
helpers
pelvisWeight
thighWeight
def is_empty(self): View Source

    62    def is_empty(self):
    63        """
    64        체인이 비어있는지 확인
    65
    66        Returns:
    67            본과 헬퍼가 모두 비어있으면 True, 아니면 False
    68        """
    69        return len(self.bones) == 0 and len(self.helpers) == 0

체인이 비어있는지 확인

Returns: 본과 헬퍼가 모두 비어있으면 True, 아니면 False

def clear(self): View Source

    71    def clear(self):
    72        """체인의 모든 본과 헬퍼 참조 제거"""
    73        self.bones = []
    74        self.helpers = []
    75        self.pelvis = None
    76        self.lThighTwist = None
    77        self.rThighTwist = None
    78        self.pelvisWeight = 40.0  # 기본 골반 가중치
    79        self.thighWeight = 60.0   # 기본 허벅지 가중치

체인의 모든 본과 헬퍼 참조 제거

def delete(self): View Source

    81    def delete(self):
    82        """
    83        체인의 모든 본과 헬퍼를 3ds Max 씬에서 삭제
    84
    85        Returns:
    86            삭제 성공 여부 (boolean)
    87        """
    88        if self.is_empty():
    89            return False
    90
    91        try:
    92            rt.delete(self.bones)
    93            rt.delete(self.helpers)
    94            return True
    95        except:
    96            return False

체인의 모든 본과 헬퍼를 3ds Max 씬에서 삭제

Returns: 삭제 성공 여부 (boolean)

def delete_all(self): View Source

     98    def delete_all(self):
     99        """
    100        체인의 모든 본과 헬퍼를 3ds Max 씬에서 삭제
    101
    102        Returns:
    103            삭제 성공 여부 (boolean)
    104        """
    105        if self.is_empty():
    106            return False
    107
    108        try:
    109            rt.delete(self.bones)
    110            rt.delete(self.helpers)
    111            self.clear()
    112            return True
    113        except:
    114            return False

체인의 모든 본과 헬퍼를 3ds Max 씬에서 삭제

Returns: 삭제 성공 여부 (boolean)

def update_weights(self, pelvisWeight=None, thighWeight=None): View Source

    116    def update_weights(self, pelvisWeight=None, thighWeight=None):
    117        """
    118        고간 부 본의 가중치 업데이트
    119
    120        Args:
    121            pelvisWeight: 골반 가중치 (None인 경우 현재 값 유지)
    122            thighWeight: 허벅지 가중치 (None인 경우 현재 값 유지)
    123
    124        Returns:
    125            업데이트 성공 여부 (boolean)
    126        """
    127        if self.is_empty():
    128            return False
    129
    130        # 새 가중치 설정
    131        if pelvisWeight is not None:
    132            self.pelvisWeight = pelvisWeight
    133        if thighWeight is not None:
    134            self.thighWeight = thighWeight
    135
    136        self.delete()
    137        result = jal.groinBone.create_bone(
    138            self.pelvis,
    139            self.lThighTwist,
    140            self.rThighTwist,
    141            self.pelvisWeight,
    142            self.thighWeight
    143        )
    144        self.bones = result["Bones"]
    145        self.helpers = result["Helpers"]

고간 부 본의 가중치 업데이트

Args: pelvisWeight: 골반 가중치 (None인 경우 현재 값 유지) thighWeight: 허벅지 가중치 (None인 경우 현재 값 유지)

Returns: 업데이트 성공 여부 (boolean)

def get_weights(self): View Source

    147    def get_weights(self):
    148        """
    149        현재 설정된 가중치 값 가져오기
    150
    151        Returns:
    152            (pelvis_weight, thigh_weight) 형태의 튜플
    153        """
    154        return (self.pelvis_weight, self.thigh_weight)

현재 설정된 가중치 값 가져오기

Returns: (pelvis_weight, thigh_weight) 형태의 튜플

@classmethod
def from_groin_bone_result(cls, inResult): View Source

    156    @classmethod
    157    def from_groin_bone_result(cls, inResult):
    158        """
    159        GroinBone 클래스의 결과로부터 GroinBoneChain 인스턴스 생성
    160
    161        Args:
    162            bones: GroinBone 클래스가 생성한 뼈대 배열
    163            helpers: GroinBone 클래스가 생성한 헬퍼 배열
    164            biped_obj: 연관된 Biped 객체 (기본값: None)
    165            pelvisWeight: 골반 가중치 (기본값: 40.0)
    166            thighWeight: 허벅지 가중치 (기본값: 60.0)
    167
    168        Returns:
    169            GroinBoneChain 인스턴스
    170        """
    171        chain = cls(inResult)
    172
    173        return chain

GroinBone 클래스의 결과로부터 GroinBoneChain 인스턴스 생성

Args: bones: GroinBone 클래스가 생성한 뼈대 배열 helpers: GroinBone 클래스가 생성한 헬퍼 배열 biped_obj: 연관된 Biped 객체 (기본값: None) pelvisWeight: 골반 가중치 (기본값: 40.0) thighWeight: 허벅지 가중치 (기본값: 60.0)

Returns: GroinBoneChain 인스턴스

class AutoClavicle: View Source

     21class AutoClavicle:
     22    """
     23    자동 쇄골(AutoClavicle) 관련 기능을 제공하는 클래스.
     24    MAXScript의 _AutoClavicleBone 구조체 개념을 Python으로 재구현한 클래스이며,
     25    3ds Max의 기능들을 pymxs API를 통해 제어합니다.
     26    """
     27
     28    def __init__(self, nameService=None, animService=None, helperService=None, boneService=None, constraintService=None, bipService=None):
     29        """
     30        클래스 초기화
     31
     32        Args:
     33            nameService: 이름 처리 서비스 (제공되지 않으면 새로 생성)
     34            animService: 애니메이션 서비스 (제공되지 않으면 새로 생성)
     35            helperService: 헬퍼 객체 서비스 (제공되지 않으면 새로 생성)
     36            boneService: 뼈대 서비스 (제공되지 않으면 새로 생성)
     37            constraintService: 제약 서비스 (제공되지 않으면 새로 생성)
     38            bipService: Biped 서비스 (제공되지 않으면 새로 생성)
     39        """
     40        # 서비스 인스턴스 설정 또는 생성
     41        self.name = nameService if nameService else Name()
     42        self.anim = animService if animService else Anim()
     43        # 종속성이 있는 서비스들은 이미 생성된 서비스들을 전달
     44        self.helper = helperService if helperService else Helper(nameService=self.name)
     45        self.bone = boneService if boneService else Bone(nameService=self.name, animService=self.anim)
     46        self.const = constraintService if constraintService else Constraint(nameService=self.name)
     47        self.bip = bipService if bipService else Bip(nameService=self.name, animService=self.anim)
     48
     49        self.boneSize = 2.0
     50
     51        # 초기화된 결과를 저장할 변수들
     52        self.genBones = []
     53        self.genHelpers = []
     54        self.clavicle = None
     55        self.upperArm = None
     56        self.liftScale = 0.8
     57
     58    def reset(self):
     59        """
     60        클래스의 주요 컴포넌트들을 초기화합니다.
     61        서비스가 아닌 클래스 자체의 작업 데이터를 초기화하는 함수입니다.
     62
     63        Returns:
     64            self: 메소드 체이닝을 위한 자기 자신 반환
     65        """
     66        self.genBones = []
     67        self.genHelpers = []
     68        self.clavicle = None
     69        self.upperArm = None
     70        self.liftScale = 0.8
     71
     72        return self
     73
     74    def create_bones(self, inClavicle, inUpperArm, liftScale=0.8):
     75        """
     76        자동 쇄골 뼈를 생성하고 설정합니다.
     77
     78        Args:
     79            inClavicle: 쇄골 뼈 객체
     80            inUpperArm: 상완 뼈 객체
     81            liftScale: 들어올림 스케일 (기본값: 0.8)
     82
     83        Returns:
     84            생성된 자동 쇄골 뼈대 배열 또는 AutoClavicleChain 클래스에 전달할 수 있는 딕셔너리
     85        """
     86        if not rt.isValidNode(inClavicle) or not rt.isValidNode(inUpperArm):
     87            return False
     88
     89        # 리스트 초기화
     90        genBones = []
     91        genHelpers = []
     92
     93        # 쇄골과 상완 사이의 거리 계산
     94        clavicleLength = rt.distance(inClavicle, inUpperArm)
     95        facingDirVec = inUpperArm.transform.position - inClavicle.transform.position
     96        inObjXAxisVec = inClavicle.objectTransform.row1
     97        distanceDir = 1.0 if rt.dot(inObjXAxisVec, facingDirVec) > 0 else -1.0
     98        clavicleLength *= distanceDir
     99
    100        # 자동 쇄골 이름 생성 및 뼈대 생성
    101        autoClavicleName = self.name.replace_name_part("RealName", inClavicle.name, "Auto" + self.name._get_filtering_char(inClavicle.name) + "Clavicle")
    102        if inClavicle.name[0].islower():
    103            autoClavicleName = autoClavicleName.lower()
    104
    105        autoClavicleBone = self.bone.create_nub_bone(autoClavicleName, 2)
    106        autoClavicleBone.name = self.name.remove_name_part("Nub", autoClavicleBone.name)
    107        autoClavicleBone.transform = inClavicle.transform
    108        self.anim.move_local(autoClavicleBone, clavicleLength/2.0, 0.0, 0.0)
    109        autoClavicleBone.parent = inClavicle
    110        genBones.extend(autoClavicleBone)
    111
    112        # 타겟 헬퍼 포인트 생성 (쇄골과 상완용)
    113        rotTargetClavicle = self.helper.create_point(self.name.replace_name_part("Type", autoClavicleName, self.name.get_name_part_value_by_description("Type", "Target")))
    114        rotTargetClavicle.name = self.name.replace_name_part("Index", rotTargetClavicle.name, "0")
    115        rotTargetClavicle.transform = inClavicle.transform
    116        self.anim.move_local(rotTargetClavicle, clavicleLength, 0.0, 0.0)
    117
    118        rotTargetClavicle.parent = inClavicle
    119        genHelpers.append(rotTargetClavicle)
    120
    121        rotTargetUpperArm = self.helper.create_point(self.name.replace_name_part("Type", autoClavicleName, self.name.get_name_part_value_by_description("Type", "Target")))
    122        rotTargetUpperArm.name = self.name.add_suffix_to_real_name(rotTargetUpperArm.name, self.name._get_filtering_char(inClavicle.name) + "arm")
    123        rotTargetUpperArm.transform = inUpperArm.transform
    124        self.anim.move_local(rotTargetUpperArm, (clavicleLength/2.0)*liftScale, 0.0, 0.0)
    125
    126        rotTargetUpperArm.parent = inUpperArm
    127        genHelpers.append(rotTargetUpperArm)
    128
    129        # 회전 헬퍼 포인트 생성
    130        autoClavicleRotHelper = self.helper.create_point(self.name.replace_name_part("Type", autoClavicleName, self.name.get_name_part_value_by_description("Type", "Rotation")))
    131        autoClavicleRotHelper.transform = autoClavicleBone.transform
    132        autoClavicleRotHelper.parent = inClavicle
    133
    134        lookAtConst = self.const.assign_lookat_multi(autoClavicleRotHelper, [rotTargetClavicle, rotTargetUpperArm])
    135
    136        lookAtConst.upnode_world = False
    137        lookAtConst.pickUpNode = inClavicle
    138        lookAtConst.lookat_vector_length = 0.0
    139
    140        genHelpers.append(autoClavicleRotHelper)
    141
    142        # ik 헬퍼 포인트 생성
    143        ikGoal = self.helper.create_point(autoClavicleName, boxToggle=False, crossToggle=True)
    144        ikGoal.transform = inClavicle.transform
    145        self.anim.move_local(ikGoal, clavicleLength, 0.0, 0.0)
    146        ikGoal.name = self.name.replace_name_part("Type", autoClavicleName, self.name.get_name_part_value_by_description("Type", "Target"))
    147        ikGoal.name = self.name.replace_name_part("Index", ikGoal.name, "1")
    148
    149        ikGoal.parent = autoClavicleRotHelper
    150
    151        autClavicleLookAtConst = self.const.assign_lookat(autoClavicleBone, ikGoal)
    152        if clavicleLength < 0:
    153            autClavicleLookAtConst.target_axisFlip = True
    154        autClavicleLookAtConst.upnode_world = False
    155        autClavicleLookAtConst.pickUpNode = inClavicle
    156        autClavicleLookAtConst.lookat_vector_length = 0.0
    157        genHelpers.append(ikGoal)
    158
    159        # 결과를 멤버 변수에 저장
    160        self.genBones = genBones
    161        self.genHelpers = genHelpers
    162        self.clavicle = inClavicle
    163        self.upperArm = inUpperArm
    164        self.liftScale = liftScale
    165
    166        # AutoClavicleChain에 전달할 수 있는 딕셔너리 형태로 결과 반환
    167        result = {
    168            "Bones": genBones,
    169            "Helpers": genHelpers,
    170            "Clavicle": inClavicle,
    171            "UpperArm": inUpperArm,
    172            "LiftScale": liftScale
    173        }
    174
    175        # 메소드 호출 후 데이터 초기화
    176        self.reset()
    177
    178        return result

자동 쇄골(AutoClavicle) 관련 기능을 제공하는 클래스. MAXScript의 _AutoClavicleBone 구조체 개념을 Python으로 재구현한 클래스이며, 3ds Max의 기능들을 pymxs API를 통해 제어합니다.

AutoClavicle( nameService=None, animService=None, helperService=None, boneService=None, constraintService=None, bipService=None) View Source

    28    def __init__(self, nameService=None, animService=None, helperService=None, boneService=None, constraintService=None, bipService=None):
    29        """
    30        클래스 초기화
    31
    32        Args:
    33            nameService: 이름 처리 서비스 (제공되지 않으면 새로 생성)
    34            animService: 애니메이션 서비스 (제공되지 않으면 새로 생성)
    35            helperService: 헬퍼 객체 서비스 (제공되지 않으면 새로 생성)
    36            boneService: 뼈대 서비스 (제공되지 않으면 새로 생성)
    37            constraintService: 제약 서비스 (제공되지 않으면 새로 생성)
    38            bipService: Biped 서비스 (제공되지 않으면 새로 생성)
    39        """
    40        # 서비스 인스턴스 설정 또는 생성
    41        self.name = nameService if nameService else Name()
    42        self.anim = animService if animService else Anim()
    43        # 종속성이 있는 서비스들은 이미 생성된 서비스들을 전달
    44        self.helper = helperService if helperService else Helper(nameService=self.name)
    45        self.bone = boneService if boneService else Bone(nameService=self.name, animService=self.anim)
    46        self.const = constraintService if constraintService else Constraint(nameService=self.name)
    47        self.bip = bipService if bipService else Bip(nameService=self.name, animService=self.anim)
    48
    49        self.boneSize = 2.0
    50
    51        # 초기화된 결과를 저장할 변수들
    52        self.genBones = []
    53        self.genHelpers = []
    54        self.clavicle = None
    55        self.upperArm = None
    56        self.liftScale = 0.8

클래스 초기화

Args: nameService: 이름 처리 서비스 (제공되지 않으면 새로 생성) animService: 애니메이션 서비스 (제공되지 않으면 새로 생성) helperService: 헬퍼 객체 서비스 (제공되지 않으면 새로 생성) boneService: 뼈대 서비스 (제공되지 않으면 새로 생성) constraintService: 제약 서비스 (제공되지 않으면 새로 생성) bipService: Biped 서비스 (제공되지 않으면 새로 생성)

name
anim
helper
bone
const
bip
boneSize
genBones
genHelpers
clavicle
upperArm
liftScale
def reset(self): View Source

    58    def reset(self):
    59        """
    60        클래스의 주요 컴포넌트들을 초기화합니다.
    61        서비스가 아닌 클래스 자체의 작업 데이터를 초기화하는 함수입니다.
    62
    63        Returns:
    64            self: 메소드 체이닝을 위한 자기 자신 반환
    65        """
    66        self.genBones = []
    67        self.genHelpers = []
    68        self.clavicle = None
    69        self.upperArm = None
    70        self.liftScale = 0.8
    71
    72        return self

클래스의 주요 컴포넌트들을 초기화합니다. 서비스가 아닌 클래스 자체의 작업 데이터를 초기화하는 함수입니다.

Returns: self: 메소드 체이닝을 위한 자기 자신 반환

def create_bones(self, inClavicle, inUpperArm, liftScale=0.8): View Source

     74    def create_bones(self, inClavicle, inUpperArm, liftScale=0.8):
     75        """
     76        자동 쇄골 뼈를 생성하고 설정합니다.
     77
     78        Args:
     79            inClavicle: 쇄골 뼈 객체
     80            inUpperArm: 상완 뼈 객체
     81            liftScale: 들어올림 스케일 (기본값: 0.8)
     82
     83        Returns:
     84            생성된 자동 쇄골 뼈대 배열 또는 AutoClavicleChain 클래스에 전달할 수 있는 딕셔너리
     85        """
     86        if not rt.isValidNode(inClavicle) or not rt.isValidNode(inUpperArm):
     87            return False
     88
     89        # 리스트 초기화
     90        genBones = []
     91        genHelpers = []
     92
     93        # 쇄골과 상완 사이의 거리 계산
     94        clavicleLength = rt.distance(inClavicle, inUpperArm)
     95        facingDirVec = inUpperArm.transform.position - inClavicle.transform.position
     96        inObjXAxisVec = inClavicle.objectTransform.row1
     97        distanceDir = 1.0 if rt.dot(inObjXAxisVec, facingDirVec) > 0 else -1.0
     98        clavicleLength *= distanceDir
     99
    100        # 자동 쇄골 이름 생성 및 뼈대 생성
    101        autoClavicleName = self.name.replace_name_part("RealName", inClavicle.name, "Auto" + self.name._get_filtering_char(inClavicle.name) + "Clavicle")
    102        if inClavicle.name[0].islower():
    103            autoClavicleName = autoClavicleName.lower()
    104
    105        autoClavicleBone = self.bone.create_nub_bone(autoClavicleName, 2)
    106        autoClavicleBone.name = self.name.remove_name_part("Nub", autoClavicleBone.name)
    107        autoClavicleBone.transform = inClavicle.transform
    108        self.anim.move_local(autoClavicleBone, clavicleLength/2.0, 0.0, 0.0)
    109        autoClavicleBone.parent = inClavicle
    110        genBones.extend(autoClavicleBone)
    111
    112        # 타겟 헬퍼 포인트 생성 (쇄골과 상완용)
    113        rotTargetClavicle = self.helper.create_point(self.name.replace_name_part("Type", autoClavicleName, self.name.get_name_part_value_by_description("Type", "Target")))
    114        rotTargetClavicle.name = self.name.replace_name_part("Index", rotTargetClavicle.name, "0")
    115        rotTargetClavicle.transform = inClavicle.transform
    116        self.anim.move_local(rotTargetClavicle, clavicleLength, 0.0, 0.0)
    117
    118        rotTargetClavicle.parent = inClavicle
    119        genHelpers.append(rotTargetClavicle)
    120
    121        rotTargetUpperArm = self.helper.create_point(self.name.replace_name_part("Type", autoClavicleName, self.name.get_name_part_value_by_description("Type", "Target")))
    122        rotTargetUpperArm.name = self.name.add_suffix_to_real_name(rotTargetUpperArm.name, self.name._get_filtering_char(inClavicle.name) + "arm")
    123        rotTargetUpperArm.transform = inUpperArm.transform
    124        self.anim.move_local(rotTargetUpperArm, (clavicleLength/2.0)*liftScale, 0.0, 0.0)
    125
    126        rotTargetUpperArm.parent = inUpperArm
    127        genHelpers.append(rotTargetUpperArm)
    128
    129        # 회전 헬퍼 포인트 생성
    130        autoClavicleRotHelper = self.helper.create_point(self.name.replace_name_part("Type", autoClavicleName, self.name.get_name_part_value_by_description("Type", "Rotation")))
    131        autoClavicleRotHelper.transform = autoClavicleBone.transform
    132        autoClavicleRotHelper.parent = inClavicle
    133
    134        lookAtConst = self.const.assign_lookat_multi(autoClavicleRotHelper, [rotTargetClavicle, rotTargetUpperArm])
    135
    136        lookAtConst.upnode_world = False
    137        lookAtConst.pickUpNode = inClavicle
    138        lookAtConst.lookat_vector_length = 0.0
    139
    140        genHelpers.append(autoClavicleRotHelper)
    141
    142        # ik 헬퍼 포인트 생성
    143        ikGoal = self.helper.create_point(autoClavicleName, boxToggle=False, crossToggle=True)
    144        ikGoal.transform = inClavicle.transform
    145        self.anim.move_local(ikGoal, clavicleLength, 0.0, 0.0)
    146        ikGoal.name = self.name.replace_name_part("Type", autoClavicleName, self.name.get_name_part_value_by_description("Type", "Target"))
    147        ikGoal.name = self.name.replace_name_part("Index", ikGoal.name, "1")
    148
    149        ikGoal.parent = autoClavicleRotHelper
    150
    151        autClavicleLookAtConst = self.const.assign_lookat(autoClavicleBone, ikGoal)
    152        if clavicleLength < 0:
    153            autClavicleLookAtConst.target_axisFlip = True
    154        autClavicleLookAtConst.upnode_world = False
    155        autClavicleLookAtConst.pickUpNode = inClavicle
    156        autClavicleLookAtConst.lookat_vector_length = 0.0
    157        genHelpers.append(ikGoal)
    158
    159        # 결과를 멤버 변수에 저장
    160        self.genBones = genBones
    161        self.genHelpers = genHelpers
    162        self.clavicle = inClavicle
    163        self.upperArm = inUpperArm
    164        self.liftScale = liftScale
    165
    166        # AutoClavicleChain에 전달할 수 있는 딕셔너리 형태로 결과 반환
    167        result = {
    168            "Bones": genBones,
    169            "Helpers": genHelpers,
    170            "Clavicle": inClavicle,
    171            "UpperArm": inUpperArm,
    172            "LiftScale": liftScale
    173        }
    174
    175        # 메소드 호출 후 데이터 초기화
    176        self.reset()
    177
    178        return result

자동 쇄골 뼈를 생성하고 설정합니다.

Args: inClavicle: 쇄골 뼈 객체 inUpperArm: 상완 뼈 객체 liftScale: 들어올림 스케일 (기본값: 0.8)

Returns: 생성된 자동 쇄골 뼈대 배열 또는 AutoClavicleChain 클래스에 전달할 수 있는 딕셔너리

class AutoClavicleChain: View Source

     52class AutoClavicleChain:
     53    def __init__(self, inResult):
     54        """
     55        클래스 초기화.
     56
     57        Args:
     58            inResult: AutoClavicle 클래스의 생성 결과 (딕셔너리)
     59        """
     60        self.bones = inResult.get("Bones", [])
     61        self.helpers = inResult.get("Helpers", [])
     62        self.clavicle = inResult.get("Clavicle", None)
     63        self.upperArm = inResult.get("UpperArm", None)
     64        self.liftScale = inResult.get("LiftScale", 0.8)
     65
     66    def get_bones(self):
     67        """
     68        체인의 모든 뼈대 가져오기
     69
     70        Returns:
     71            모든 뼈대 객체의 배열
     72        """
     73        if self.is_empty():
     74            return []
     75
     76        return self.bones
     77
     78    def get_helpers(self):
     79        """
     80        체인의 모든 헬퍼 가져오기
     81
     82        Returns:
     83            모든 헬퍼 객체의 배열
     84        """
     85        if self.is_empty():
     86            return []
     87
     88        return self.helpers
     89
     90    def is_empty(self):
     91        """
     92        체인이 비어있는지 확인
     93
     94        Returns:
     95            체인이 비어있으면 True, 아니면 False
     96        """
     97        return len(self.bones) == 0
     98
     99    def clear(self):
    100        """체인의 모든 뼈대와 헬퍼 참조 제거"""
    101        self.bones = []
    102        self.helpers = []
    103        self.clavicle = None
    104        self.upperArm = None
    105
    106    def delete_all(self):
    107        """
    108        체인의 모든 뼈대와 헬퍼를 3ds Max 씬에서 삭제
    109
    110        Returns:
    111            삭제 성공 여부 (boolean)
    112        """
    113        if self.is_empty():
    114            return False
    115
    116        try:
    117            for bone in self.bones:
    118                if rt.isValidNode(bone):
    119                    rt.delete(bone)
    120
    121            for helper in self.helpers:
    122                if rt.isValidNode(helper):
    123                    rt.delete(helper)
    124
    125            self.clear()
    126            return True
    127        except:
    128            return False
    129
    130    def update_lift_scale(self, newLiftScale=0.8):
    131        """
    132        자동 쇄골의 들어올림 스케일 업데이트
    133
    134        Args:
    135            newLiftScale: 새로운 들어올림 스케일 (기본값: 0.8)
    136
    137        Returns:
    138            업데이트 성공 여부 (boolean)
    139        """
    140        if self.is_empty() or not rt.isValidNode(self.clavicle) or not rt.isValidNode(self.upperArm):
    141            return False
    142
    143        clavicle = self.clavicle
    144        upperArm = self.upperArm
    145
    146        # 기존 본과 헬퍼 삭제
    147        self.delete_all()
    148
    149        # 새로운 LiftScale 값 설정
    150        self.liftScale = newLiftScale
    151        self.clavicle = clavicle
    152        self.upperArm = upperArm
    153
    154        # 재생성
    155        result = jal.autoClavicle.create_bones(self.clavicle, self.upperArm, self.liftScale)
    156        if result:
    157            return True
    158
    159        return False
    160
    161    @classmethod
    162    def from_auto_clavicle_result(cls, inResult):
    163        """
    164        AutoClavicle 클래스의 결과로부터 AutoClavicleChain 인스턴스 생성
    165
    166        Args:
    167            inResult: AutoClavicle 클래스의 메서드가 반환한 결과값 딕셔너리
    168
    169        Returns:
    170            AutoClavicleChain 인스턴스
    171        """
    172        chain = cls(inResult)
    173        return chain

AutoClavicleChain(inResult) View Source

    53    def __init__(self, inResult):
    54        """
    55        클래스 초기화.
    56
    57        Args:
    58            inResult: AutoClavicle 클래스의 생성 결과 (딕셔너리)
    59        """
    60        self.bones = inResult.get("Bones", [])
    61        self.helpers = inResult.get("Helpers", [])
    62        self.clavicle = inResult.get("Clavicle", None)
    63        self.upperArm = inResult.get("UpperArm", None)
    64        self.liftScale = inResult.get("LiftScale", 0.8)

클래스 초기화.

Args: inResult: AutoClavicle 클래스의 생성 결과 (딕셔너리)

bones
helpers
clavicle
upperArm
liftScale
def get_bones(self): View Source

    66    def get_bones(self):
    67        """
    68        체인의 모든 뼈대 가져오기
    69
    70        Returns:
    71            모든 뼈대 객체의 배열
    72        """
    73        if self.is_empty():
    74            return []
    75
    76        return self.bones

체인의 모든 뼈대 가져오기

Returns: 모든 뼈대 객체의 배열

def get_helpers(self): View Source

    78    def get_helpers(self):
    79        """
    80        체인의 모든 헬퍼 가져오기
    81
    82        Returns:
    83            모든 헬퍼 객체의 배열
    84        """
    85        if self.is_empty():
    86            return []
    87
    88        return self.helpers

체인의 모든 헬퍼 가져오기

Returns: 모든 헬퍼 객체의 배열

def is_empty(self): View Source

    90    def is_empty(self):
    91        """
    92        체인이 비어있는지 확인
    93
    94        Returns:
    95            체인이 비어있으면 True, 아니면 False
    96        """
    97        return len(self.bones) == 0

체인이 비어있는지 확인

Returns: 체인이 비어있으면 True, 아니면 False

def clear(self): View Source

     99    def clear(self):
    100        """체인의 모든 뼈대와 헬퍼 참조 제거"""
    101        self.bones = []
    102        self.helpers = []
    103        self.clavicle = None
    104        self.upperArm = None

체인의 모든 뼈대와 헬퍼 참조 제거

def delete_all(self): View Source

    106    def delete_all(self):
    107        """
    108        체인의 모든 뼈대와 헬퍼를 3ds Max 씬에서 삭제
    109
    110        Returns:
    111            삭제 성공 여부 (boolean)
    112        """
    113        if self.is_empty():
    114            return False
    115
    116        try:
    117            for bone in self.bones:
    118                if rt.isValidNode(bone):
    119                    rt.delete(bone)
    120
    121            for helper in self.helpers:
    122                if rt.isValidNode(helper):
    123                    rt.delete(helper)
    124
    125            self.clear()
    126            return True
    127        except:
    128            return False

체인의 모든 뼈대와 헬퍼를 3ds Max 씬에서 삭제

Returns: 삭제 성공 여부 (boolean)

def update_lift_scale(self, newLiftScale=0.8): View Source

    130    def update_lift_scale(self, newLiftScale=0.8):
    131        """
    132        자동 쇄골의 들어올림 스케일 업데이트
    133
    134        Args:
    135            newLiftScale: 새로운 들어올림 스케일 (기본값: 0.8)
    136
    137        Returns:
    138            업데이트 성공 여부 (boolean)
    139        """
    140        if self.is_empty() or not rt.isValidNode(self.clavicle) or not rt.isValidNode(self.upperArm):
    141            return False
    142
    143        clavicle = self.clavicle
    144        upperArm = self.upperArm
    145
    146        # 기존 본과 헬퍼 삭제
    147        self.delete_all()
    148
    149        # 새로운 LiftScale 값 설정
    150        self.liftScale = newLiftScale
    151        self.clavicle = clavicle
    152        self.upperArm = upperArm
    153
    154        # 재생성
    155        result = jal.autoClavicle.create_bones(self.clavicle, self.upperArm, self.liftScale)
    156        if result:
    157            return True
    158
    159        return False

자동 쇄골의 들어올림 스케일 업데이트

Args: newLiftScale: 새로운 들어올림 스케일 (기본값: 0.8)

Returns: 업데이트 성공 여부 (boolean)

@classmethod
def from_auto_clavicle_result(cls, inResult): View Source

    161    @classmethod
    162    def from_auto_clavicle_result(cls, inResult):
    163        """
    164        AutoClavicle 클래스의 결과로부터 AutoClavicleChain 인스턴스 생성
    165
    166        Args:
    167            inResult: AutoClavicle 클래스의 메서드가 반환한 결과값 딕셔너리
    168
    169        Returns:
    170            AutoClavicleChain 인스턴스
    171        """
    172        chain = cls(inResult)
    173        return chain

AutoClavicle 클래스의 결과로부터 AutoClavicleChain 인스턴스 생성

Args: inResult: AutoClavicle 클래스의 메서드가 반환한 결과값 딕셔너리

Returns: AutoClavicleChain 인스턴스

class VolumeBone: View Source

     23class VolumeBone:  # Updated class name to match the new file name
     24    """
     25    관절 부피 유지 본(Volume preserve Bone) 클래스
     26
     27    3ds Max에서 관절의 부피를 유지하기 위해 추가되는 중간본들을 위한 클래스입니다.
     28    이 클래스는 관절이 회전할 때 자동으로 부피감을 유지하도록 하는 보조 본 시스템을 생성하고
     29    관리합니다. 부모 관절과 자식 관절 사이에 부피 유지 본을 배치하여 관절 변형 시 부피 감소를
     30    방지하고 더 자연스러운 움직임을 구현합니다.
     31    """
     32    def __init__(self, nameService=None, animService=None, constraintService=None, boneService=None, helperService=None):
     33        """
     34        클래스 초기화.
     35
     36        필요한 서비스 객체들을 초기화하거나 외부에서 제공받습니다.
     37        각 서비스 객체들은 본 생성, 이름 관리, 애니메이션 제어, 제약 조건 적용 등의
     38        기능을 담당합니다.
     39
     40        Args:
     41            nameService: 이름 처리 서비스 (제공되지 않으면 새로 생성)
     42            animService: 애니메이션 서비스 (제공되지 않으면 새로 생성)
     43            constraintService: 제약 서비스 (제공되지 않으면 새로 생성)
     44            boneService: 뼈대 서비스 (제공되지 않으면 새로 생성)
     45            helperService: 헬퍼 서비스 (제공되지 않으면 새로 생성)
     46        """
     47        # 서비스 인스턴스 설정 또는 생성
     48        self.name = nameService if nameService else Name()
     49        self.anim = animService if animService else Anim()
     50
     51        # 종속성이 있는 서비스들은 이미 생성된 서비스들을 전달
     52        self.const = constraintService if constraintService else Constraint(nameService=self.name)
     53        self.bone = boneService if boneService else Bone(nameService=self.name, animService=self.anim)
     54        self.helper = helperService if helperService else Helper(nameService=self.name)
     55
     56        self.rootBone = None
     57        self.rotHelper = None
     58        self.limb = None
     59        self.limbParent = None
     60        self.bones = []
     61        self.rotAxises = []
     62        self.transAxises = []
     63        self.transScales = []
     64        self.volumeSize = 5.0
     65        self.rotScale = 0.5
     66
     67        self.posScriptExpression = (
     68            "localLimbTm = limb.transform * inverse limbParent.transform\n"
     69            "localDeltaTm = localLimbTm * inverse localRotRefTm\n"
     70            "\n"
     71            "q = localDeltaTm.rotation\n"
     72            "\n"
     73            "eulerRot = (quatToEuler q order:5)\n"
     74            "swizzledRot = (eulerAngles eulerRot.y eulerRot.z eulerRot.x)\n"
     75            "saturatedTwist = abs ((swizzledRot.x*axis.x + swizzledRot.y*axis.y + swizzledRot.z*axis.z)/180.0)\n"
     76            "\n"
     77            "trAxis * saturatedTwist * volumeSize * transScale\n"
     78        )
     79
     80    def reset(self):
     81        """
     82        클래스의 주요 컴포넌트들을 초기화합니다.
     83        서비스가 아닌 클래스 자체의 작업 데이터를 초기화하는 함수입니다.
     84
     85        Returns:
     86            self: 메소드 체이닝을 위한 자기 자신 반환
     87        """
     88        self.rootBone = None
     89        self.rotHelper = None
     90        self.limb = None
     91        self.limbParent = None
     92        self.bones = []
     93        self.rotAxises = []
     94        self.transAxises = []
     95        self.transScales = []
     96        self.volumeSize = 5.0
     97        self.rotScale = 0.5
     98
     99        return self
    100
    101    def create_root_bone(self, inObj, inParent, inRotScale=0.5):
    102        if rt.isValidNode(inObj) == False or rt.isValidNode(inParent) == False:
    103            return False
    104
    105        if rt.isValidNode(self.rootBone) and rt.isValidNode(self.rotHelper):
    106            return self.rootBone
    107
    108        rootBoneName = inObj.name
    109        filteringChar = self.name._get_filtering_char(rootBoneName)
    110        rootBoneName = self.name.add_suffix_to_real_name(rootBoneName, filteringChar+"Vol"+filteringChar+"Root")
    111
    112        rootBone = self.bone.create_nub_bone(rootBoneName, 2)
    113        rootBone.name = self.name.remove_name_part("Nub", rootBone.name)
    114        if rootBone.name[0].islower():
    115            rootBone.name = rootBone.name.lower()
    116            rootBoneName = rootBoneName.lower()
    117
    118        rt.setProperty(rootBone, "transform", inObj.transform)
    119        rootBone.parent = inObj
    120
    121        rotHelper = self.helper.create_point(rootBoneName)
    122        rotHelper.name = self.name.replace_name_part("Type", rotHelper.name, self.name.get_name_part_value_by_description("Type", "Dummy"))
    123        rt.setProperty(rotHelper, "transform", inObj.transform)
    124        rotHelper.parent = inParent
    125
    126        oriConst = self.const.assign_rot_const_multi(rootBone, [inObj, rotHelper])
    127        oriConst.setWeight(1, inRotScale * 100.0)
    128        oriConst.setWeight(2, (1.0 - inRotScale) * 100.0)
    129
    130        self.rootBone = rootBone
    131        self.rotHelper = rotHelper
    132        self.limb = inObj
    133        self.limbParent = inParent
    134
    135        return self.rootBone
    136
    137    def create_bone(self, inObj, inParent, inRotScale=0.5, inVolumeSize=5.0, inRotAxis="Z", inTransAxis="PosY", inTransScale=1.0, useRootBone=True, inRootBone=None):
    138        if rt.isValidNode(inObj) == False or rt.isValidNode(inParent) == False:
    139            return False
    140
    141        if useRootBone:
    142            if rt.isValidNode(self.rootBone) == False and rt.isValidNode(self.rotHelper) == False:
    143                return False
    144            self.rootBone = inRootBone if inRootBone else self.create_root_bone(inObj, inParent, inRotScale)
    145        else:
    146            self.create_root_bone(inObj, inParent, inRotScale)
    147
    148        self.limb = inObj
    149        self.limbParent = inParent
    150
    151        volBoneName = inObj.name
    152        filteringChar = self.name._get_filtering_char(volBoneName)
    153        volBoneName = self.name.add_suffix_to_real_name(volBoneName, filteringChar + "Vol" + filteringChar + inRotAxis + filteringChar+ inTransAxis)
    154
    155        volBone = self.bone.create_nub_bone(volBoneName, 2)
    156        volBone.name = self.name.remove_name_part("Nub", volBone.name)
    157        if volBone.name[0].islower():
    158            volBone.name = volBone.name.lower()
    159            volBoneName = volBoneName.lower()
    160        rt.setProperty(volBone, "transform", self.rootBone.transform)
    161
    162        volBoneTrDir = rt.Point3(0.0, 0.0, 0.0)
    163        if inTransAxis == "PosX":
    164            volBoneTrDir = rt.Point3(1.0, 0.0, 0.0)
    165        elif inTransAxis == "NegX":
    166            volBoneTrDir = rt.Point3(-1.0, 0.0, 0.0)
    167        elif inTransAxis == "PosY":
    168            volBoneTrDir = rt.Point3(0.0, 1.0, 0.0)
    169        elif inTransAxis == "NegY":
    170            volBoneTrDir = rt.Point3(0.0, -1.0, 0.0)
    171        elif inTransAxis == "PosZ":
    172            volBoneTrDir = rt.Point3(0.0, 0.0, 1.0)
    173        elif inTransAxis == "NegZ":
    174            volBoneTrDir = rt.Point3(0.0, 0.0, -1.0)
    175
    176        self.anim.move_local(volBone, volBoneTrDir[0]*inVolumeSize, volBoneTrDir[1]*inVolumeSize, volBoneTrDir[2]*inVolumeSize)
    177        volBone.parent = self.rootBone
    178
    179        rotAxis = rt.Point3(0.0, 0.0, 0.0)
    180        if inRotAxis == "X":
    181            rotAxis = rt.Point3(1.0, 0.0, 0.0)
    182        elif inRotAxis == "Y":
    183            rotAxis = rt.Point3(0.0, 1.0, 0.0)
    184        elif inRotAxis == "Z":
    185            rotAxis = rt.Point3(0.0, 0.0, 1.0)
    186
    187        # localRotRefTm = self.limb.transform * rt.inverse(self.limbParent.transform)
    188        localRotRefTm = self.limb.transform * rt.inverse(self.rotHelper.transform)
    189        volBonePosConst = self.const.assign_pos_script_controller(volBone)
    190        volBonePosConst.addNode("limb", self.limb)
    191        # volBonePosConst.addNode("limbParent", self.limbParent)
    192        volBonePosConst.addNode("limbParent", self.rotHelper)
    193        volBonePosConst.addConstant("axis", rotAxis)
    194        volBonePosConst.addConstant("transScale", rt.Float(inTransScale))
    195        volBonePosConst.addConstant("volumeSize", rt.Float(inVolumeSize))
    196        volBonePosConst.addConstant("localRotRefTm", localRotRefTm)
    197        volBonePosConst.addConstant("trAxis", volBoneTrDir)
    198        volBonePosConst.setExpression(self.posScriptExpression)
    199        volBonePosConst.update()
    200
    201        return True
    202
    203    def create_bones(self, inObj, inParent, inRotScale=0.5, inVolumeSize=5.0, inRotAxises=["Z"], inTransAxises=["PosY"], inTransScales=[1.0]):
    204        """
    205        여러 개의 부피 유지 본을 생성합니다.
    206
    207        Args:
    208            inObj: 본을 생성할 객체
    209            inParent: 부모 객체
    210            inRotScale: 회전 비율
    211            inVolumeSize: 부피 크기
    212            inRotAxises: 회전 축 리스트
    213            inTransAxises: 변환 축 리스트
    214            inTransScales: 변환 비율 리스트
    215
    216        Returns:
    217            dict: VolumeBoneChain 생성을 위한 결과 딕셔너리
    218        """
    219        if rt.isValidNode(inObj) == False or rt.isValidNode(inParent) == False:
    220            return None
    221
    222        if len(inRotAxises) != len(inTransAxises) or len(inRotAxises) != len(inTransScales):
    223            return None
    224
    225        rootBone = self.create_root_bone(inObj, inParent, inRotScale=inRotScale)
    226
    227        # 볼륨 본들 생성
    228        bones = []
    229        for i in range(len(inRotAxises)):
    230            self.create_bone(inObj, inParent, inRotScale, inVolumeSize, inRotAxises[i], inTransAxises[i], inTransScales[i], useRootBone=True, inRootBone=rootBone)
    231
    232            # 생성된 본의 이름 패턴으로 찾기
    233            volBoneName = inObj.name
    234            filteringChar = self.name._get_filtering_char(volBoneName)
    235            volBoneName = self.name.add_suffix_to_real_name(volBoneName,
    236                          filteringChar + "Vol" + filteringChar + inRotAxises[i] +
    237                          filteringChar + inTransAxises[i])
    238
    239            if volBoneName[0].islower():
    240                volBoneName = volBoneName.lower()
    241
    242            volBone = rt.getNodeByName(self.name.remove_name_part("Nub", volBoneName))
    243            if rt.isValidNode(volBone):
    244                bones.append(volBone)
    245
    246        # 클래스 변수에 결과 저장
    247        self.rootBone = rootBone
    248        self.limb = inObj
    249        self.limbParent = inParent
    250        self.bones = bones
    251        self.rotAxises = inRotAxises.copy()
    252        self.transAxises = inTransAxises.copy()
    253        self.transScales = inTransScales.copy()
    254        self.volumeSize = inVolumeSize
    255        self.rotScale = inRotScale
    256
    257        # VolumeBoneChain이 필요로 하는 형태의 결과 딕셔너리 생성
    258        result = {
    259            "RootBone": rootBone,
    260            "RotHelper": self.rotHelper,
    261            "RotScale": inRotScale,
    262            "Limb": inObj,
    263            "LimbParent": inParent,
    264            "Bones": bones,
    265            "RotAxises": inRotAxises,
    266            "TransAxises": inTransAxises,
    267            "TransScales": inTransScales,
    268            "VolumeSize": inVolumeSize
    269        }
    270
    271        # 메소드 호출 후 데이터 초기화
    272        self.reset()
    273
    274        return result

관절 부피 유지 본(Volume preserve Bone) 클래스

3ds Max에서 관절의 부피를 유지하기 위해 추가되는 중간본들을 위한 클래스입니다. 이 클래스는 관절이 회전할 때 자동으로 부피감을 유지하도록 하는 보조 본 시스템을 생성하고 관리합니다. 부모 관절과 자식 관절 사이에 부피 유지 본을 배치하여 관절 변형 시 부피 감소를 방지하고 더 자연스러운 움직임을 구현합니다.

VolumeBone( nameService=None, animService=None, constraintService=None, boneService=None, helperService=None) View Source

    32    def __init__(self, nameService=None, animService=None, constraintService=None, boneService=None, helperService=None):
    33        """
    34        클래스 초기화.
    35
    36        필요한 서비스 객체들을 초기화하거나 외부에서 제공받습니다.
    37        각 서비스 객체들은 본 생성, 이름 관리, 애니메이션 제어, 제약 조건 적용 등의
    38        기능을 담당합니다.
    39
    40        Args:
    41            nameService: 이름 처리 서비스 (제공되지 않으면 새로 생성)
    42            animService: 애니메이션 서비스 (제공되지 않으면 새로 생성)
    43            constraintService: 제약 서비스 (제공되지 않으면 새로 생성)
    44            boneService: 뼈대 서비스 (제공되지 않으면 새로 생성)
    45            helperService: 헬퍼 서비스 (제공되지 않으면 새로 생성)
    46        """
    47        # 서비스 인스턴스 설정 또는 생성
    48        self.name = nameService if nameService else Name()
    49        self.anim = animService if animService else Anim()
    50
    51        # 종속성이 있는 서비스들은 이미 생성된 서비스들을 전달
    52        self.const = constraintService if constraintService else Constraint(nameService=self.name)
    53        self.bone = boneService if boneService else Bone(nameService=self.name, animService=self.anim)
    54        self.helper = helperService if helperService else Helper(nameService=self.name)
    55
    56        self.rootBone = None
    57        self.rotHelper = None
    58        self.limb = None
    59        self.limbParent = None
    60        self.bones = []
    61        self.rotAxises = []
    62        self.transAxises = []
    63        self.transScales = []
    64        self.volumeSize = 5.0
    65        self.rotScale = 0.5
    66
    67        self.posScriptExpression = (
    68            "localLimbTm = limb.transform * inverse limbParent.transform\n"
    69            "localDeltaTm = localLimbTm * inverse localRotRefTm\n"
    70            "\n"
    71            "q = localDeltaTm.rotation\n"
    72            "\n"
    73            "eulerRot = (quatToEuler q order:5)\n"
    74            "swizzledRot = (eulerAngles eulerRot.y eulerRot.z eulerRot.x)\n"
    75            "saturatedTwist = abs ((swizzledRot.x*axis.x + swizzledRot.y*axis.y + swizzledRot.z*axis.z)/180.0)\n"
    76            "\n"
    77            "trAxis * saturatedTwist * volumeSize * transScale\n"
    78        )

클래스 초기화.

필요한 서비스 객체들을 초기화하거나 외부에서 제공받습니다. 각 서비스 객체들은 본 생성, 이름 관리, 애니메이션 제어, 제약 조건 적용 등의 기능을 담당합니다.

Args: nameService: 이름 처리 서비스 (제공되지 않으면 새로 생성) animService: 애니메이션 서비스 (제공되지 않으면 새로 생성) constraintService: 제약 서비스 (제공되지 않으면 새로 생성) boneService: 뼈대 서비스 (제공되지 않으면 새로 생성) helperService: 헬퍼 서비스 (제공되지 않으면 새로 생성)

name
anim
const
bone
helper
rootBone
rotHelper
limb
limbParent
bones
rotAxises
transAxises
transScales
volumeSize
rotScale
posScriptExpression
def reset(self): View Source

    80    def reset(self):
    81        """
    82        클래스의 주요 컴포넌트들을 초기화합니다.
    83        서비스가 아닌 클래스 자체의 작업 데이터를 초기화하는 함수입니다.
    84
    85        Returns:
    86            self: 메소드 체이닝을 위한 자기 자신 반환
    87        """
    88        self.rootBone = None
    89        self.rotHelper = None
    90        self.limb = None
    91        self.limbParent = None
    92        self.bones = []
    93        self.rotAxises = []
    94        self.transAxises = []
    95        self.transScales = []
    96        self.volumeSize = 5.0
    97        self.rotScale = 0.5
    98
    99        return self

클래스의 주요 컴포넌트들을 초기화합니다. 서비스가 아닌 클래스 자체의 작업 데이터를 초기화하는 함수입니다.

Returns: self: 메소드 체이닝을 위한 자기 자신 반환

def create_root_bone(self, inObj, inParent, inRotScale=0.5): View Source

    101    def create_root_bone(self, inObj, inParent, inRotScale=0.5):
    102        if rt.isValidNode(inObj) == False or rt.isValidNode(inParent) == False:
    103            return False
    104
    105        if rt.isValidNode(self.rootBone) and rt.isValidNode(self.rotHelper):
    106            return self.rootBone
    107
    108        rootBoneName = inObj.name
    109        filteringChar = self.name._get_filtering_char(rootBoneName)
    110        rootBoneName = self.name.add_suffix_to_real_name(rootBoneName, filteringChar+"Vol"+filteringChar+"Root")
    111
    112        rootBone = self.bone.create_nub_bone(rootBoneName, 2)
    113        rootBone.name = self.name.remove_name_part("Nub", rootBone.name)
    114        if rootBone.name[0].islower():
    115            rootBone.name = rootBone.name.lower()
    116            rootBoneName = rootBoneName.lower()
    117
    118        rt.setProperty(rootBone, "transform", inObj.transform)
    119        rootBone.parent = inObj
    120
    121        rotHelper = self.helper.create_point(rootBoneName)
    122        rotHelper.name = self.name.replace_name_part("Type", rotHelper.name, self.name.get_name_part_value_by_description("Type", "Dummy"))
    123        rt.setProperty(rotHelper, "transform", inObj.transform)
    124        rotHelper.parent = inParent
    125
    126        oriConst = self.const.assign_rot_const_multi(rootBone, [inObj, rotHelper])
    127        oriConst.setWeight(1, inRotScale * 100.0)
    128        oriConst.setWeight(2, (1.0 - inRotScale) * 100.0)
    129
    130        self.rootBone = rootBone
    131        self.rotHelper = rotHelper
    132        self.limb = inObj
    133        self.limbParent = inParent
    134
    135        return self.rootBone

def create_bone( self, inObj, inParent, inRotScale=0.5, inVolumeSize=5.0, inRotAxis='Z', inTransAxis='PosY', inTransScale=1.0, useRootBone=True, inRootBone=None): View Source

    137    def create_bone(self, inObj, inParent, inRotScale=0.5, inVolumeSize=5.0, inRotAxis="Z", inTransAxis="PosY", inTransScale=1.0, useRootBone=True, inRootBone=None):
    138        if rt.isValidNode(inObj) == False or rt.isValidNode(inParent) == False:
    139            return False
    140
    141        if useRootBone:
    142            if rt.isValidNode(self.rootBone) == False and rt.isValidNode(self.rotHelper) == False:
    143                return False
    144            self.rootBone = inRootBone if inRootBone else self.create_root_bone(inObj, inParent, inRotScale)
    145        else:
    146            self.create_root_bone(inObj, inParent, inRotScale)
    147
    148        self.limb = inObj
    149        self.limbParent = inParent
    150
    151        volBoneName = inObj.name
    152        filteringChar = self.name._get_filtering_char(volBoneName)
    153        volBoneName = self.name.add_suffix_to_real_name(volBoneName, filteringChar + "Vol" + filteringChar + inRotAxis + filteringChar+ inTransAxis)
    154
    155        volBone = self.bone.create_nub_bone(volBoneName, 2)
    156        volBone.name = self.name.remove_name_part("Nub", volBone.name)
    157        if volBone.name[0].islower():
    158            volBone.name = volBone.name.lower()
    159            volBoneName = volBoneName.lower()
    160        rt.setProperty(volBone, "transform", self.rootBone.transform)
    161
    162        volBoneTrDir = rt.Point3(0.0, 0.0, 0.0)
    163        if inTransAxis == "PosX":
    164            volBoneTrDir = rt.Point3(1.0, 0.0, 0.0)
    165        elif inTransAxis == "NegX":
    166            volBoneTrDir = rt.Point3(-1.0, 0.0, 0.0)
    167        elif inTransAxis == "PosY":
    168            volBoneTrDir = rt.Point3(0.0, 1.0, 0.0)
    169        elif inTransAxis == "NegY":
    170            volBoneTrDir = rt.Point3(0.0, -1.0, 0.0)
    171        elif inTransAxis == "PosZ":
    172            volBoneTrDir = rt.Point3(0.0, 0.0, 1.0)
    173        elif inTransAxis == "NegZ":
    174            volBoneTrDir = rt.Point3(0.0, 0.0, -1.0)
    175
    176        self.anim.move_local(volBone, volBoneTrDir[0]*inVolumeSize, volBoneTrDir[1]*inVolumeSize, volBoneTrDir[2]*inVolumeSize)
    177        volBone.parent = self.rootBone
    178
    179        rotAxis = rt.Point3(0.0, 0.0, 0.0)
    180        if inRotAxis == "X":
    181            rotAxis = rt.Point3(1.0, 0.0, 0.0)
    182        elif inRotAxis == "Y":
    183            rotAxis = rt.Point3(0.0, 1.0, 0.0)
    184        elif inRotAxis == "Z":
    185            rotAxis = rt.Point3(0.0, 0.0, 1.0)
    186
    187        # localRotRefTm = self.limb.transform * rt.inverse(self.limbParent.transform)
    188        localRotRefTm = self.limb.transform * rt.inverse(self.rotHelper.transform)
    189        volBonePosConst = self.const.assign_pos_script_controller(volBone)
    190        volBonePosConst.addNode("limb", self.limb)
    191        # volBonePosConst.addNode("limbParent", self.limbParent)
    192        volBonePosConst.addNode("limbParent", self.rotHelper)
    193        volBonePosConst.addConstant("axis", rotAxis)
    194        volBonePosConst.addConstant("transScale", rt.Float(inTransScale))
    195        volBonePosConst.addConstant("volumeSize", rt.Float(inVolumeSize))
    196        volBonePosConst.addConstant("localRotRefTm", localRotRefTm)
    197        volBonePosConst.addConstant("trAxis", volBoneTrDir)
    198        volBonePosConst.setExpression(self.posScriptExpression)
    199        volBonePosConst.update()
    200
    201        return True

def create_bones( self, inObj, inParent, inRotScale=0.5, inVolumeSize=5.0, inRotAxises=['Z'], inTransAxises=['PosY'], inTransScales=[1.0]): View Source

    203    def create_bones(self, inObj, inParent, inRotScale=0.5, inVolumeSize=5.0, inRotAxises=["Z"], inTransAxises=["PosY"], inTransScales=[1.0]):
    204        """
    205        여러 개의 부피 유지 본을 생성합니다.
    206
    207        Args:
    208            inObj: 본을 생성할 객체
    209            inParent: 부모 객체
    210            inRotScale: 회전 비율
    211            inVolumeSize: 부피 크기
    212            inRotAxises: 회전 축 리스트
    213            inTransAxises: 변환 축 리스트
    214            inTransScales: 변환 비율 리스트
    215
    216        Returns:
    217            dict: VolumeBoneChain 생성을 위한 결과 딕셔너리
    218        """
    219        if rt.isValidNode(inObj) == False or rt.isValidNode(inParent) == False:
    220            return None
    221
    222        if len(inRotAxises) != len(inTransAxises) or len(inRotAxises) != len(inTransScales):
    223            return None
    224
    225        rootBone = self.create_root_bone(inObj, inParent, inRotScale=inRotScale)
    226
    227        # 볼륨 본들 생성
    228        bones = []
    229        for i in range(len(inRotAxises)):
    230            self.create_bone(inObj, inParent, inRotScale, inVolumeSize, inRotAxises[i], inTransAxises[i], inTransScales[i], useRootBone=True, inRootBone=rootBone)
    231
    232            # 생성된 본의 이름 패턴으로 찾기
    233            volBoneName = inObj.name
    234            filteringChar = self.name._get_filtering_char(volBoneName)
    235            volBoneName = self.name.add_suffix_to_real_name(volBoneName,
    236                          filteringChar + "Vol" + filteringChar + inRotAxises[i] +
    237                          filteringChar + inTransAxises[i])
    238
    239            if volBoneName[0].islower():
    240                volBoneName = volBoneName.lower()
    241
    242            volBone = rt.getNodeByName(self.name.remove_name_part("Nub", volBoneName))
    243            if rt.isValidNode(volBone):
    244                bones.append(volBone)
    245
    246        # 클래스 변수에 결과 저장
    247        self.rootBone = rootBone
    248        self.limb = inObj
    249        self.limbParent = inParent
    250        self.bones = bones
    251        self.rotAxises = inRotAxises.copy()
    252        self.transAxises = inTransAxises.copy()
    253        self.transScales = inTransScales.copy()
    254        self.volumeSize = inVolumeSize
    255        self.rotScale = inRotScale
    256
    257        # VolumeBoneChain이 필요로 하는 형태의 결과 딕셔너리 생성
    258        result = {
    259            "RootBone": rootBone,
    260            "RotHelper": self.rotHelper,
    261            "RotScale": inRotScale,
    262            "Limb": inObj,
    263            "LimbParent": inParent,
    264            "Bones": bones,
    265            "RotAxises": inRotAxises,
    266            "TransAxises": inTransAxises,
    267            "TransScales": inTransScales,
    268            "VolumeSize": inVolumeSize
    269        }
    270
    271        # 메소드 호출 후 데이터 초기화
    272        self.reset()
    273
    274        return result

여러 개의 부피 유지 본을 생성합니다.

Args: inObj: 본을 생성할 객체 inParent: 부모 객체 inRotScale: 회전 비율 inVolumeSize: 부피 크기 inRotAxises: 회전 축 리스트 inTransAxises: 변환 축 리스트 inTransScales: 변환 비율 리스트

Returns: dict: VolumeBoneChain 생성을 위한 결과 딕셔너리

class VolumeBoneChain: View Source

     64class VolumeBoneChain:
     65    """
     66    볼륨 본 체인 관리 클래스
     67
     68    VolumeBone 클래스로 생성된 볼륨 본들의 집합을 관리하는 클래스입니다.
     69    볼륨 본의 크기 조절, 회전 및 이동 축 변경, 스케일 조정 등의 기능을 제공하며,
     70    여러 개의 볼륨 본을 하나의 논리적 체인으로 관리합니다.
     71    생성된 볼륨 본 체인은 캐릭터 관절의 자연스러운 변형을 위해 사용됩니다.
     72    """
     73
     74    def __init__(self, inResult):
     75        """
     76        볼륨 본 체인 클래스 초기화
     77
     78        VolumeBone 클래스의 create_bones 메서드로부터 생성된 결과 딕셔너리를
     79        받아 볼륨 본 체인을 구성합니다.
     80
     81        Args:
     82            inResult: VolumeBone 클래스의 create_bones 메서드가 반환한 결과 딕셔너리
     83                      (루트 본, 회전 헬퍼, 회전 축, 이동 축, 볼륨 크기 등의 정보 포함)
     84        """
     85        self.rootBone = inResult.get("RootBone", None)
     86        self.rotHelper = inResult.get("RotHelper", None)
     87        self.rotScale = inResult.get("RotScale", 0.0)
     88        self.limb = inResult.get("Limb", None)
     89        self.limbParent = inResult.get("LimbParent", None)
     90        self.bones = inResult.get("Bones", [])
     91        self.rotAxises = inResult.get("RotAxises", [])
     92        self.transAxises = inResult.get("TransAxises", [])
     93        self.transScales = inResult.get("TransScales", [])
     94        self.volumeSize = inResult.get("VolumeSize", 0.0)
     95
     96    def get_volume_size(self):
     97        """
     98        볼륨 뼈대의 크기 가져오기
     99
    100        볼륨 본 생성 시 설정된 크기 값을 반환합니다. 이 값은 관절의 볼륨감 정도를
    101        결정합니다.
    102
    103        Returns:
    104            float: 현재 설정된 볼륨 크기 값
    105        """
    106        return self.volumeSize
    107
    108    def is_empty(self):
    109        """
    110        체인이 비어있는지 확인
    111
    112        볼륨 본 체인에 본이 하나라도 존재하는지 확인합니다.
    113
    114        Returns:
    115            bool: 체인이 비어있으면 True, 하나 이상의 본이 있으면 False
    116        """
    117        return len(self.bones) == 0
    118
    119    def clear(self):
    120        """체인의 모든 뼈대 및 헬퍼 참조 제거"""
    121        self.rootBone = None
    122        self.rotHelper = None
    123        self.rotScale = 0.0
    124        self.limb = None
    125        self.limbParent = None
    126        self.bones = []
    127        self.rotAxises = []
    128        self.transAxises = []
    129        self.transScales = []
    130        self.volumeSize = 0.0
    131
    132    def delete_all(self):
    133        """
    134        체인의 모든 뼈대와 헬퍼를 3ds Max 씬에서 삭제
    135
    136        Returns:
    137            삭제 성공 여부 (boolean)
    138        """
    139        if self.is_empty():
    140            return False
    141
    142        try:
    143            # 루트 본 삭제
    144            if self.rootBone:
    145                rt.delete(self.rootBone)
    146
    147            # 회전 헬퍼 삭제
    148            if self.rotHelper:
    149                rt.delete(self.rotHelper)
    150
    151            # 뼈대 삭제
    152            for bone in self.bones:
    153                rt.delete(bone)
    154
    155            self.rotAxises = []
    156            self.transAxises = []
    157            self.transScales = []
    158
    159            self.clear()
    160            return True
    161        except:
    162            return False
    163
    164    def update_volume_size(self, inNewSize):
    165        """
    166        볼륨 뼈대의 크기 업데이트
    167
    168        Args:
    169            inNewSize: 새로운 볼륨 크기 값
    170
    171        Returns:
    172            업데이트 성공 여부 (boolean)
    173        """
    174        if self.is_empty() or self.limb is None:
    175            return False
    176
    177        try:
    178            # 필요한 값들 백업
    179            limb = self.limb
    180            limbParent = self.limbParent
    181            rotScale = self.rotScale
    182            rotAxises = copy.deepcopy(self.rotAxises)
    183            transAxises = copy.deepcopy(self.transAxises)
    184            transScales = copy.deepcopy(self.transScales)
    185
    186            self.delete_all()
    187            # VolumeBone 클래스를 통해 새로운 볼륨 뼈대 생성
    188            result = jal.volumeBone.create_bones(limb, limbParent, inVolumeSize=inNewSize,
    189                                                 inRotScale=rotScale, inRotAxises=rotAxises,
    190                                                 inTransAxises=transAxises, inTransScales=transScales)
    191
    192            # 속성들 한번에 업데이트
    193            for key, value in result.items():
    194                if hasattr(self, key):
    195                    setattr(self, key, value)
    196
    197            self.volumeSize = inNewSize
    198
    199            return True
    200        except:
    201            return False
    202
    203    def update_rot_axises(self, inNewRotAxises):
    204        """
    205        볼륨 뼈대의 회전 축을 업데이트
    206
    207        Args:
    208            inNewRotAxises: 새로운 회전 축 리스트
    209
    210        Returns:
    211            업데이트 성공 여부 (boolean)
    212        """
    213        if self.is_empty() or self.limb is None:
    214            return False
    215
    216        try:
    217            # 필요한 값들 백업
    218            limb = self.limb
    219            limbParent = self.limbParent
    220            rotScale = self.rotScale
    221            volumeSize = self.volumeSize
    222            transAxises = copy.deepcopy(self.transAxises)
    223            transScales = copy.deepcopy(self.transScales)
    224
    225            self.delete_all()
    226            # VolumeBone 클래스를 통해 새로운 볼륨 뼈대 생성
    227            result = jal.volumeBone.create_bones(limb, limbParent, inVolumeSize=volumeSize,
    228                                                inRotScale=rotScale, inRotAxises=inNewRotAxises,
    229                                                inTransAxises=transAxises, inTransScales=transScales)
    230
    231            # 속성들 한번에 업데이트
    232            for key, value in result.items():
    233                if hasattr(self, key):
    234                    setattr(self, key, value)
    235
    236            return True
    237        except:
    238            return False
    239
    240    def update_trans_axises(self, inNewTransAxises):
    241        """
    242        볼륨 뼈대의 이동 축을 업데이트
    243
    244        Args:
    245            inNewTransAxises: 새로운 이동 축 리스트
    246
    247        Returns:
    248            업데이트 성공 여부 (boolean)
    249        """
    250        if self.is_empty() or self.limb is None:
    251            return False
    252
    253        try:
    254            # 필요한 값들 백업
    255            limb = self.limb
    256            limbParent = self.limbParent
    257            rotScale = self.rotScale
    258            volumeSize = self.volumeSize
    259            rotAxises = copy.deepcopy(self.rotAxises)
    260            transScales = copy.deepcopy(self.transScales)
    261
    262            self.delete_all()
    263            # VolumeBone 클래스를 통해 새로운 볼륨 뼈대 생성
    264            result = jal.volumeBone.create_bones(limb, limbParent, inVolumeSize=volumeSize,
    265                                                inRotScale=rotScale, inRotAxises=rotAxises,
    266                                                inTransAxises=inNewTransAxises, inTransScales=transScales)
    267
    268            # 속성들 한번에 업데이트
    269            for key, value in result.items():
    270                if hasattr(self, key):
    271                    setattr(self, key, value)
    272
    273            return True
    274        except:
    275            return False
    276
    277    def update_trans_scales(self, inNewTransScales):
    278        """
    279        볼륨 뼈대의 이동 스케일을 업데이트
    280
    281        Args:
    282            inNewTransScales: 새로운 이동 스케일 리스트
    283
    284        Returns:
    285            업데이트 성공 여부 (boolean)
    286        """
    287        if self.is_empty() or self.limb is None:
    288            return False
    289
    290        try:
    291            # 필요한 값들 백업
    292            limb = self.limb
    293            limbParent = self.limbParent
    294            rotScale = self.rotScale
    295            volumeSize = self.volumeSize
    296            rotAxises = copy.deepcopy(self.rotAxises)
    297            transAxises = copy.deepcopy(self.transAxises)
    298
    299            self.delete_all()
    300            # VolumeBone 클래스를 통해 새로운 볼륨 뼈대 생성
    301            result = jal.volumeBone.create_bones(limb, limbParent, inVolumeSize=volumeSize,
    302                                                inRotScale=rotScale, inRotAxises=rotAxises,
    303                                                inTransAxises=transAxises, inTransScales=inNewTransScales)
    304
    305            # 속성들 한번에 업데이트
    306            for key, value in result.items():
    307                if hasattr(self, key):
    308                    setattr(self, key, value)
    309
    310            return True
    311        except:
    312            return False
    313
    314    def update_rot_scale(self, inNewRotScale):
    315        """
    316        볼륨 뼈대의 회전 스케일을 업데이트
    317
    318        Args:
    319            inNewRotScale: 새로운 회전 스케일 값
    320
    321        Returns:
    322            업데이트 성공 여부 (boolean)
    323        """
    324        if self.is_empty() or self.limb is None:
    325            return False
    326
    327        try:
    328            # 필요한 값들 백업
    329            limb = self.limb
    330            limbParent = self.limbParent
    331            volumeSize = self.volumeSize
    332            rotAxises = copy.deepcopy(self.rotAxises)
    333            transAxises = copy.deepcopy(self.transAxises)
    334            transScales = copy.deepcopy(self.transScales)
    335
    336            self.delete_all()
    337            # VolumeBone 클래스를 통해 새로운 볼륨 뼈대 생성
    338            result = jal.volumeBone.create_bones(limb, limbParent, inVolumeSize=volumeSize,
    339                                                inRotScale=inNewRotScale, inRotAxises=rotAxises,
    340                                                inTransAxises=transAxises, inTransScales=transScales)
    341
    342            # 속성들 한번에 업데이트
    343            for key, value in result.items():
    344                if hasattr(self, key):
    345                    setattr(self, key, value)
    346
    347            return True
    348        except:
    349            return False
    350
    351    @classmethod
    352    def from_volume_bone_result(cls, inResult):
    353        """
    354        VolumeBone 클래스의 결과로부터 VolumeBoneChain 인스턴스 생성
    355
    356        Args:
    357            inResult: VolumeBone 클래스의 메서드가 반환한 결과 딕셔너리
    358
    359        Returns:
    360            VolumeBoneChain 인스턴스
    361        """
    362        chain = cls(inResult)
    363        return chain

볼륨 본 체인 관리 클래스

VolumeBone 클래스로 생성된 볼륨 본들의 집합을 관리하는 클래스입니다. 볼륨 본의 크기 조절, 회전 및 이동 축 변경, 스케일 조정 등의 기능을 제공하며, 여러 개의 볼륨 본을 하나의 논리적 체인으로 관리합니다. 생성된 볼륨 본 체인은 캐릭터 관절의 자연스러운 변형을 위해 사용됩니다.

VolumeBoneChain(inResult) View Source

    74    def __init__(self, inResult):
    75        """
    76        볼륨 본 체인 클래스 초기화
    77
    78        VolumeBone 클래스의 create_bones 메서드로부터 생성된 결과 딕셔너리를
    79        받아 볼륨 본 체인을 구성합니다.
    80
    81        Args:
    82            inResult: VolumeBone 클래스의 create_bones 메서드가 반환한 결과 딕셔너리
    83                      (루트 본, 회전 헬퍼, 회전 축, 이동 축, 볼륨 크기 등의 정보 포함)
    84        """
    85        self.rootBone = inResult.get("RootBone", None)
    86        self.rotHelper = inResult.get("RotHelper", None)
    87        self.rotScale = inResult.get("RotScale", 0.0)
    88        self.limb = inResult.get("Limb", None)
    89        self.limbParent = inResult.get("LimbParent", None)
    90        self.bones = inResult.get("Bones", [])
    91        self.rotAxises = inResult.get("RotAxises", [])
    92        self.transAxises = inResult.get("TransAxises", [])
    93        self.transScales = inResult.get("TransScales", [])
    94        self.volumeSize = inResult.get("VolumeSize", 0.0)

볼륨 본 체인 클래스 초기화

VolumeBone 클래스의 create_bones 메서드로부터 생성된 결과 딕셔너리를 받아 볼륨 본 체인을 구성합니다.

Args: inResult: VolumeBone 클래스의 create_bones 메서드가 반환한 결과 딕셔너리 (루트 본, 회전 헬퍼, 회전 축, 이동 축, 볼륨 크기 등의 정보 포함)

rootBone
rotHelper
rotScale
limb
limbParent
bones
rotAxises
transAxises
transScales
volumeSize
def get_volume_size(self): View Source

     96    def get_volume_size(self):
     97        """
     98        볼륨 뼈대의 크기 가져오기
     99
    100        볼륨 본 생성 시 설정된 크기 값을 반환합니다. 이 값은 관절의 볼륨감 정도를
    101        결정합니다.
    102
    103        Returns:
    104            float: 현재 설정된 볼륨 크기 값
    105        """
    106        return self.volumeSize

볼륨 뼈대의 크기 가져오기

볼륨 본 생성 시 설정된 크기 값을 반환합니다. 이 값은 관절의 볼륨감 정도를 결정합니다.

Returns: float: 현재 설정된 볼륨 크기 값

def is_empty(self): View Source

    108    def is_empty(self):
    109        """
    110        체인이 비어있는지 확인
    111
    112        볼륨 본 체인에 본이 하나라도 존재하는지 확인합니다.
    113
    114        Returns:
    115            bool: 체인이 비어있으면 True, 하나 이상의 본이 있으면 False
    116        """
    117        return len(self.bones) == 0

체인이 비어있는지 확인

볼륨 본 체인에 본이 하나라도 존재하는지 확인합니다.

Returns: bool: 체인이 비어있으면 True, 하나 이상의 본이 있으면 False

def clear(self): View Source

    119    def clear(self):
    120        """체인의 모든 뼈대 및 헬퍼 참조 제거"""
    121        self.rootBone = None
    122        self.rotHelper = None
    123        self.rotScale = 0.0
    124        self.limb = None
    125        self.limbParent = None
    126        self.bones = []
    127        self.rotAxises = []
    128        self.transAxises = []
    129        self.transScales = []
    130        self.volumeSize = 0.0

체인의 모든 뼈대 및 헬퍼 참조 제거

def delete_all(self): View Source

    132    def delete_all(self):
    133        """
    134        체인의 모든 뼈대와 헬퍼를 3ds Max 씬에서 삭제
    135
    136        Returns:
    137            삭제 성공 여부 (boolean)
    138        """
    139        if self.is_empty():
    140            return False
    141
    142        try:
    143            # 루트 본 삭제
    144            if self.rootBone:
    145                rt.delete(self.rootBone)
    146
    147            # 회전 헬퍼 삭제
    148            if self.rotHelper:
    149                rt.delete(self.rotHelper)
    150
    151            # 뼈대 삭제
    152            for bone in self.bones:
    153                rt.delete(bone)
    154
    155            self.rotAxises = []
    156            self.transAxises = []
    157            self.transScales = []
    158
    159            self.clear()
    160            return True
    161        except:
    162            return False

체인의 모든 뼈대와 헬퍼를 3ds Max 씬에서 삭제

Returns: 삭제 성공 여부 (boolean)

def update_volume_size(self, inNewSize): View Source

    164    def update_volume_size(self, inNewSize):
    165        """
    166        볼륨 뼈대의 크기 업데이트
    167
    168        Args:
    169            inNewSize: 새로운 볼륨 크기 값
    170
    171        Returns:
    172            업데이트 성공 여부 (boolean)
    173        """
    174        if self.is_empty() or self.limb is None:
    175            return False
    176
    177        try:
    178            # 필요한 값들 백업
    179            limb = self.limb
    180            limbParent = self.limbParent
    181            rotScale = self.rotScale
    182            rotAxises = copy.deepcopy(self.rotAxises)
    183            transAxises = copy.deepcopy(self.transAxises)
    184            transScales = copy.deepcopy(self.transScales)
    185
    186            self.delete_all()
    187            # VolumeBone 클래스를 통해 새로운 볼륨 뼈대 생성
    188            result = jal.volumeBone.create_bones(limb, limbParent, inVolumeSize=inNewSize,
    189                                                 inRotScale=rotScale, inRotAxises=rotAxises,
    190                                                 inTransAxises=transAxises, inTransScales=transScales)
    191
    192            # 속성들 한번에 업데이트
    193            for key, value in result.items():
    194                if hasattr(self, key):
    195                    setattr(self, key, value)
    196
    197            self.volumeSize = inNewSize
    198
    199            return True
    200        except:
    201            return False

볼륨 뼈대의 크기 업데이트

Args: inNewSize: 새로운 볼륨 크기 값

Returns: 업데이트 성공 여부 (boolean)

def update_rot_axises(self, inNewRotAxises): View Source

    203    def update_rot_axises(self, inNewRotAxises):
    204        """
    205        볼륨 뼈대의 회전 축을 업데이트
    206
    207        Args:
    208            inNewRotAxises: 새로운 회전 축 리스트
    209
    210        Returns:
    211            업데이트 성공 여부 (boolean)
    212        """
    213        if self.is_empty() or self.limb is None:
    214            return False
    215
    216        try:
    217            # 필요한 값들 백업
    218            limb = self.limb
    219            limbParent = self.limbParent
    220            rotScale = self.rotScale
    221            volumeSize = self.volumeSize
    222            transAxises = copy.deepcopy(self.transAxises)
    223            transScales = copy.deepcopy(self.transScales)
    224
    225            self.delete_all()
    226            # VolumeBone 클래스를 통해 새로운 볼륨 뼈대 생성
    227            result = jal.volumeBone.create_bones(limb, limbParent, inVolumeSize=volumeSize,
    228                                                inRotScale=rotScale, inRotAxises=inNewRotAxises,
    229                                                inTransAxises=transAxises, inTransScales=transScales)
    230
    231            # 속성들 한번에 업데이트
    232            for key, value in result.items():
    233                if hasattr(self, key):
    234                    setattr(self, key, value)
    235
    236            return True
    237        except:
    238            return False

볼륨 뼈대의 회전 축을 업데이트

Args: inNewRotAxises: 새로운 회전 축 리스트

Returns: 업데이트 성공 여부 (boolean)

def update_trans_axises(self, inNewTransAxises): View Source

    240    def update_trans_axises(self, inNewTransAxises):
    241        """
    242        볼륨 뼈대의 이동 축을 업데이트
    243
    244        Args:
    245            inNewTransAxises: 새로운 이동 축 리스트
    246
    247        Returns:
    248            업데이트 성공 여부 (boolean)
    249        """
    250        if self.is_empty() or self.limb is None:
    251            return False
    252
    253        try:
    254            # 필요한 값들 백업
    255            limb = self.limb
    256            limbParent = self.limbParent
    257            rotScale = self.rotScale
    258            volumeSize = self.volumeSize
    259            rotAxises = copy.deepcopy(self.rotAxises)
    260            transScales = copy.deepcopy(self.transScales)
    261
    262            self.delete_all()
    263            # VolumeBone 클래스를 통해 새로운 볼륨 뼈대 생성
    264            result = jal.volumeBone.create_bones(limb, limbParent, inVolumeSize=volumeSize,
    265                                                inRotScale=rotScale, inRotAxises=rotAxises,
    266                                                inTransAxises=inNewTransAxises, inTransScales=transScales)
    267
    268            # 속성들 한번에 업데이트
    269            for key, value in result.items():
    270                if hasattr(self, key):
    271                    setattr(self, key, value)
    272
    273            return True
    274        except:
    275            return False

볼륨 뼈대의 이동 축을 업데이트

Args: inNewTransAxises: 새로운 이동 축 리스트

Returns: 업데이트 성공 여부 (boolean)

def update_trans_scales(self, inNewTransScales): View Source

    277    def update_trans_scales(self, inNewTransScales):
    278        """
    279        볼륨 뼈대의 이동 스케일을 업데이트
    280
    281        Args:
    282            inNewTransScales: 새로운 이동 스케일 리스트
    283
    284        Returns:
    285            업데이트 성공 여부 (boolean)
    286        """
    287        if self.is_empty() or self.limb is None:
    288            return False
    289
    290        try:
    291            # 필요한 값들 백업
    292            limb = self.limb
    293            limbParent = self.limbParent
    294            rotScale = self.rotScale
    295            volumeSize = self.volumeSize
    296            rotAxises = copy.deepcopy(self.rotAxises)
    297            transAxises = copy.deepcopy(self.transAxises)
    298
    299            self.delete_all()
    300            # VolumeBone 클래스를 통해 새로운 볼륨 뼈대 생성
    301            result = jal.volumeBone.create_bones(limb, limbParent, inVolumeSize=volumeSize,
    302                                                inRotScale=rotScale, inRotAxises=rotAxises,
    303                                                inTransAxises=transAxises, inTransScales=inNewTransScales)
    304
    305            # 속성들 한번에 업데이트
    306            for key, value in result.items():
    307                if hasattr(self, key):
    308                    setattr(self, key, value)
    309
    310            return True
    311        except:
    312            return False

볼륨 뼈대의 이동 스케일을 업데이트

Args: inNewTransScales: 새로운 이동 스케일 리스트

Returns: 업데이트 성공 여부 (boolean)

def update_rot_scale(self, inNewRotScale): View Source

    314    def update_rot_scale(self, inNewRotScale):
    315        """
    316        볼륨 뼈대의 회전 스케일을 업데이트
    317
    318        Args:
    319            inNewRotScale: 새로운 회전 스케일 값
    320
    321        Returns:
    322            업데이트 성공 여부 (boolean)
    323        """
    324        if self.is_empty() or self.limb is None:
    325            return False
    326
    327        try:
    328            # 필요한 값들 백업
    329            limb = self.limb
    330            limbParent = self.limbParent
    331            volumeSize = self.volumeSize
    332            rotAxises = copy.deepcopy(self.rotAxises)
    333            transAxises = copy.deepcopy(self.transAxises)
    334            transScales = copy.deepcopy(self.transScales)
    335
    336            self.delete_all()
    337            # VolumeBone 클래스를 통해 새로운 볼륨 뼈대 생성
    338            result = jal.volumeBone.create_bones(limb, limbParent, inVolumeSize=volumeSize,
    339                                                inRotScale=inNewRotScale, inRotAxises=rotAxises,
    340                                                inTransAxises=transAxises, inTransScales=transScales)
    341
    342            # 속성들 한번에 업데이트
    343            for key, value in result.items():
    344                if hasattr(self, key):
    345                    setattr(self, key, value)
    346
    347            return True
    348        except:
    349            return False

볼륨 뼈대의 회전 스케일을 업데이트

Args: inNewRotScale: 새로운 회전 스케일 값

Returns: 업데이트 성공 여부 (boolean)

@classmethod
def from_volume_bone_result(cls, inResult): View Source

    351    @classmethod
    352    def from_volume_bone_result(cls, inResult):
    353        """
    354        VolumeBone 클래스의 결과로부터 VolumeBoneChain 인스턴스 생성
    355
    356        Args:
    357            inResult: VolumeBone 클래스의 메서드가 반환한 결과 딕셔너리
    358
    359        Returns:
    360            VolumeBoneChain 인스턴스
    361        """
    362        chain = cls(inResult)
    363        return chain

VolumeBone 클래스의 결과로부터 VolumeBoneChain 인스턴스 생성

Args: inResult: VolumeBone 클래스의 메서드가 반환한 결과 딕셔너리

Returns: VolumeBoneChain 인스턴스

class KneeBone: View Source

     20class KneeBone:
     21    """
     22    자동 무릎 본(AutoKnee) 관련 기능을 제공하는 클래스.
     23    MAXScript의 _AutoKneeBone 구조체 개념을 Python으로 재구현한 클래스이며,
     24    3ds Max의 기능들을 pymxs API를 통해 제어합니다.
     25
     26    이 클래스는 IK 시스템 기반의 다리 리깅을 자동화하며, 무릎 관절 회전, 비틀림 본 및
     27    중간 본을 생성하여 자연스러운 무릎 움직임을 구현합니다.
     28    """
     29
     30    def __init__(self, nameService=None, animService=None, helperService=None, boneService=None, constraintService=None, volumeBoneService=None):
     31        """
     32        KneeBone 클래스 초기화
     33
     34        Args:
     35            nameService: 이름 처리 서비스 (제공되지 않으면 새로 생성)
     36            animService: 애니메이션 서비스 (제공되지 않으면 새로 생성)
     37            helperService: 헬퍼 객체 서비스 (제공되지 않으면 새로 생성)
     38            boneService: 뼈대 서비스 (제공되지 않으면 새로 생성)
     39            constraintService: 제약 서비스 (제공되지 않으면 새로 생성)
     40            volumeBoneService: 볼륨 본 서비스 (제공되지 않으면 새로 생성)
     41        """
     42        # 서비스 인스턴스 설정 또는 생성
     43        self.name = nameService if nameService else Name()
     44        self.anim = animService if animService else Anim()
     45        # 종속성이 있는 서비스들은 이미 생성된 서비스들을 전달
     46        self.helper = helperService if helperService else Helper(nameService=self.name)
     47        self.bone = boneService if boneService else Bone(nameService=self.name, animService=self.anim)
     48        self.const = constraintService if constraintService else Constraint(nameService=self.name)
     49        self.volumeBone = volumeBoneService if volumeBoneService else VolumeBone(nameService=self.name, animService=self.anim, constraintService=self.const, boneService=self.bone, helperService=self.helper)
     50
     51        self.thigh = None
     52        self.calf = None
     53        self.foot = None
     54
     55        self.lookAtHleper = None
     56        self.thighRotHelper = None
     57        self.calfRotHelper = None
     58
     59        self.thighRotRootHelper = None
     60        self.calfRotRootHelper = None
     61
     62        self.thighTwistBones = []
     63        self.calfTwistBones = []
     64        self.thighTwistHelpers = []
     65        self.calfTwistHelpers = []
     66
     67        self.middleBones = []
     68
     69        self.liftScale = 0.025
     70
     71        self.thighRotScriptExpression = (
     72            "localLimbTm = limb.transform * inverse limbParent.transform\n"
     73            "localDeltaTm = localLimbTm * inverse localRotRefTm\n"
     74            "\n"
     75            "q = localDeltaTm.rotation\n"
     76            "\n"
     77            "axis = [0,0,1]\n"
     78            "\n"
     79            "proj = (dot q.axis axis) * axis\n"
     80            "twist = quat -q.angle proj\n"
     81            "twist = normalize twist\n"
     82            "\n"
     83            "twist\n"
     84        )
     85        self.calfRotScriptExpression = (
     86            "localLimbTm = limb.transform * inverse limbParent.transform\n"
     87            "localDeltaTm = localLimbTm * inverse localRotRefTm\n"
     88            "\n"
     89            "q = localDeltaTm.rotation\n"
     90            "\n"
     91            "axis = [0,0,1]\n"
     92            "\n"
     93            "proj = (dot q.axis axis) * axis\n"
     94            "twist = quat q.angle proj\n"
     95            "twist = normalize twist\n"
     96            "\n"
     97            "twist\n"
     98        )
     99
    100
    101    def create_lookat_helper(self, inThigh, inFoot):
    102        """
    103        무릎 시스템을 위한 LookAt 헬퍼 객체를 생성합니다.
    104
    105        이 헬퍼는 대퇴골(Thigh)에 위치하면서 발(Foot)을 바라보도록 제약됩니다.
    106        무릎 회전의 기반이 되는 방향을 결정하는 역할을 합니다.
    107
    108        Args:
    109            inThigh: 대퇴골 본 객체
    110            inFoot: 발 본 객체
    111
    112        Returns:
    113            bool: 헬퍼 생성 성공 여부
    114        """
    115        if not rt.isValidNode(inThigh) or not rt.isValidNode(inFoot):
    116            return False
    117
    118        filteringChar = self.name._get_filtering_char(inThigh.name)
    119        isLowerName = inThigh.name.islower()
    120
    121        # 서비스 인스턴스 설정 또는 생성
    122        self.thigh = inThigh
    123        self.foot = inFoot
    124
    125        lookAtHelperName = self.name.replace_name_part("Type", inThigh.name, self.name.get_name_part_value_by_description("Type", "LookAt"))
    126        lookAtHelperName = self.name.add_suffix_to_real_name(lookAtHelperName, filteringChar + "Lift")
    127        if isLowerName:
    128            lookAtHelperName = lookAtHelperName.lower()
    129
    130        lookAtHelper = self.helper.create_point(lookAtHelperName)
    131        lookAtHelper.transform = inThigh.transform
    132        lookAtHelper.parent = inThigh
    133        lookAtConst = self.const.assign_lookat(lookAtHelper, inFoot)
    134        lookAtConst.upnode_world = False
    135        lookAtConst.pickUpNode = inThigh
    136        lookAtConst.lookat_vector_length = 0.0
    137
    138        self.lookAtHleper = lookAtHelper
    139
    140    def create_rot_root_heleprs(self, inThigh, inCalf, inFoot):
    141        """
    142        무릎 회전의 기준이 되는 루트 헬퍼 객체들을 생성합니다.
    143
    144        대퇴골과 종아리뼈에 각각 위치하며, 비틀림 계산을 위한 기준점 역할을 합니다.
    145
    146        Args:
    147            inThigh: 대퇴골 본 객체
    148            inCalf: 종아리뼈 본 객체
    149            inFoot: 발 본 객체
    150
    151        Returns:
    152            bool: 헬퍼 생성 성공 여부
    153        """
    154        if not rt.isValidNode(inThigh) or not rt.isValidNode(inCalf) or not rt.isValidNode(inFoot):
    155            return False
    156
    157        filteringChar = self.name._get_filtering_char(inThigh.name)
    158        isLowerName = inThigh.name.islower()
    159
    160        # 서비스 인스턴스 설정 또는 생성
    161        self.thigh = inThigh
    162        self.calf = inCalf
    163        self.foot = inFoot
    164
    165        thighRotRootHelperName = self.name.replace_name_part("Type", inThigh.name, self.name.get_name_part_value_by_description("Type", "Dummy"))
    166        calfRotRootHelperName = self.name.replace_name_part("Type", inCalf.name, self.name.get_name_part_value_by_description("Type", "Dummy"))
    167        thighRotRootHelperName = self.name.add_suffix_to_real_name(thighRotRootHelperName, filteringChar + "Lift")
    168        calfRotRootHelperName = self.name.add_suffix_to_real_name(calfRotRootHelperName, filteringChar + "Lift")
    169        if isLowerName:
    170            thighRotRootHelperName = thighRotRootHelperName.lower()
    171            calfRotRootHelperName = calfRotRootHelperName.lower()
    172
    173        thighRotRootHelper = self.helper.create_point(thighRotRootHelperName, crossToggle=False, boxToggle=True)
    174        thighRotRootHelper.transform = inThigh.transform
    175        thighRotRootHelper.parent = inThigh
    176
    177        calfRotRootHelper = self.helper.create_point(calfRotRootHelperName, crossToggle=False, boxToggle=True)
    178        calfRotRootHelper.transform = inCalf.transform
    179        calfRotRootHelper.position = inFoot.position
    180        calfRotRootHelper.parent = inCalf
    181
    182        self.thighRotRootHelper = thighRotRootHelper
    183        self.calfRotRootHelper = calfRotRootHelper
    184
    185    def create_rot_helper(self, inThigh, inCalf, inFoot):
    186        """
    187        대퇴골과 종아리뼈의 회전을 제어하는 헬퍼 객체들을 생성합니다.
    188
    189        이 헬퍼들은 실제 무릎 움직임에 따른 비틀림 효과를 구현하는 데 사용됩니다.
    190
    191        Args:
    192            inThigh: 대퇴골 본 객체
    193            inCalf: 종아리뼈 본 객체
    194            inFoot: 발 본 객체
    195
    196        Returns:
    197            bool: 헬퍼 생성 성공 여부
    198        """
    199        if not rt.isValidNode(inThigh) or not rt.isValidNode(inCalf):
    200            return False
    201
    202        filteringChar = self.name._get_filtering_char(inThigh.name)
    203        isLowerName = inThigh.name.islower()
    204
    205        # 서비스 인스턴스 설정 또는 생성
    206        self.thigh = inThigh
    207        self.calf = inCalf
    208
    209        thighRotHelperName = self.name.replace_name_part("Type", inThigh.name, self.name.get_name_part_value_by_description("Type", "Rotation"))
    210        calfRotHelperName = self.name.replace_name_part("Type", inCalf.name, self.name.get_name_part_value_by_description("Type", "Rotation"))
    211        thighRotHelperName = self.name.add_suffix_to_real_name(thighRotHelperName, filteringChar + "Lift")
    212        calfRotHelperName = self.name.add_suffix_to_real_name(calfRotHelperName, filteringChar + "Lift")
    213        if isLowerName:
    214            thighRotHelperName = thighRotHelperName.lower()
    215            calfRotHelperName = calfRotHelperName.lower()
    216
    217        thighRotHelper = self.helper.create_point(thighRotHelperName)
    218        thighRotHelper.transform = inThigh.transform
    219        thighRotHelper.parent = inThigh
    220
    221        calfRotHelper = self.helper.create_point(calfRotHelperName)
    222        calfRotHelper.transform = inCalf.transform
    223        calfRotHelper.position = inFoot.transform.position
    224        calfRotHelper.parent = inCalf
    225
    226        self.thighRotHelper = thighRotHelper
    227        self.calfRotHelper = calfRotHelper
    228
    229    def assign_thigh_rot_constraint(self, inLiftScale=0.1):
    230        """
    231        대퇴골 회전 헬퍼에 스크립트 기반 회전 제약을 할당합니다.
    232
    233        LookAt 헬퍼와 대퇴골 회전 루트 헬퍼 사이의 관계를 기반으로 비틀림 회전을 계산합니다.
    234
    235        Args:
    236            inLiftScale: 회전 영향력 스케일 (0.0~1.0)
    237        """
    238        self.liftScale = inLiftScale
    239        localRotRefTm = self.lookAtHleper.transform * rt.inverse(self.thighRotRootHelper.transform)
    240
    241        rotListConst = self.const.assign_rot_list(self.thighRotHelper)
    242        rotScriptConst = rt.Rotation_Script()
    243        rt.setPropertyController(rotListConst, "Available", rotScriptConst)
    244        rotListConst.setActive(rotListConst.count)
    245
    246        rotScriptConst.addConstant("localRotRefTm", localRotRefTm)
    247        rotScriptConst.addNode("limb", self.lookAtHleper)
    248        rotScriptConst.addNode("limbParent", self.thighRotRootHelper)
    249        rotScriptConst.setExpression(self.thighRotScriptExpression)
    250
    251        self.const.set_rot_controllers_weight_in_list(self.thighRotHelper, 1, self.liftScale * 100.0)
    252
    253    def assign_calf_rot_constraint(self, inLiftScale=0.1):
    254        """
    255        종아리뼈 회전 헬퍼에 스크립트 기반 회전 제약을 할당합니다.
    256
    257        LookAt 헬퍼와 대퇴골 회전 루트 헬퍼 사이의 관계를 기반으로 비틀림 회전을 계산합니다.
    258
    259        Args:
    260            inLiftScale: 회전 영향력 스케일 (0.0~1.0)
    261        """
    262        self.liftScale = inLiftScale
    263        localRotRefTm = self.lookAtHleper.transform * rt.inverse(self.thighRotRootHelper.transform)
    264
    265        rotListConst = self.const.assign_rot_list(self.calfRotHelper)
    266        rotScriptConst = rt.Rotation_Script()
    267        rt.setPropertyController(rotListConst, "Available", rotScriptConst)
    268        rotListConst.setActive(rotListConst.count)
    269
    270        rotScriptConst.addConstant("localRotRefTm", localRotRefTm)
    271        rotScriptConst.addNode("limb", self.lookAtHleper)
    272        rotScriptConst.addNode("limbParent", self.thighRotRootHelper)
    273        rotScriptConst.setExpression(self.calfRotScriptExpression)
    274
    275        self.const.set_rot_controllers_weight_in_list(self.calfRotHelper, 1, self.liftScale * 100.0)
    276
    277    def create_middle_bone(self, inThigh, inCalf, inKneePopScale=1.0, inKneeBackScale=1.0):
    278        """
    279        무릎 중간 본을 생성합니다.
    280
    281        이 본들은 무릎이 구부러질 때 앞(Pop)과 뒤(Back)로 움직이는 볼륨감 있는 본들입니다.
    282        무릎 관절의 시각적 품질을 향상시킵니다.
    283
    284        Args:
    285            inThigh: 대퇴골 본 객체
    286            inCalf: 종아리뼈 본 객체
    287            inKneePopScale: 무릎 앞쪽 돌출 스케일 (1.0이 기본값)
    288            inKneeBackScale: 무릎 뒤쪽 돌출 스케일 (1.0이 기본값)
    289
    290        Returns:
    291            bool: 중간 본 생성 성공 여부
    292        """
    293        if not rt.isValidNode(inThigh) or not rt.isValidNode(inCalf):
    294            return False
    295
    296        facingDirVec = inCalf.transform.position - inThigh.transform.position
    297        inObjXAxisVec = inCalf.objectTransform.row1
    298        distanceDir = 1.0 if rt.dot(inObjXAxisVec, facingDirVec) > 0 else -1.0
    299
    300        self.thigh = inThigh
    301        self.calf = inCalf
    302
    303        transScales = []
    304        if distanceDir > 0:
    305            transScales.append(inKneePopScale)
    306            transScales.append(inKneeBackScale)
    307        else:
    308            transScales.append(inKneeBackScale)
    309            transScales.append(inKneePopScale)
    310
    311        result = self.volumeBone.create_bones(self.calf, self.thigh, inVolumeSize=5.0, inRotAxises=["Z", "Z"], inTransAxises=["PosY", "NegY"], inTransScales=transScales)
    312
    313        calfName = self.name.get_name_part("RealName", inCalf.name)
    314        isLower = calfName[0].islower()
    315        replaceName = "Knee"
    316        if isLower:
    317            replaceName = replaceName.lower()
    318
    319        for item in result["Bones"]:
    320            item.name.replace(calfName, replaceName)
    321
    322        result["rootBone"].name.replace(calfName, replaceName)
    323        result["RotHelper"].name.replace(calfName, replaceName)
    324
    325        # 결과 저장
    326        if result and "Bones" in result:
    327            self.middleBones.extend(result["Bones"])
    328
    329        return result
    330
    331    def create_twist_bones(self, inThigh, inCalf):
    332        """
    333        대퇴골과 종아리뼈에 연결된 비틀림 본들에 대한 리프팅 본과 헬퍼를 생성합니다.
    334
    335        기존 비틀림 본들을 찾아 각각에 대응하는 리프팅 본과 헬퍼를 생성하여
    336        무릎 구부림에 따라 자연스럽게 회전하도록 제약을 설정합니다.
    337
    338        Args:
    339            inThigh: 대퇴골 본 객체
    340            inCalf: 종아리뼈 본 객체
    341
    342        Returns:
    343            bool: 비틀림 본 생성 성공 여부
    344        """
    345        if not rt.isValidNode(inThigh) or not rt.isValidNode(inCalf):
    346            return False
    347
    348        filteringChar = self.name._get_filtering_char(inThigh.name)
    349        isLowerName = inThigh.name.islower()
    350
    351        # 서비스 인스턴스 설정 또는 생성
    352        self.thigh = inThigh
    353        self.calf = inCalf
    354
    355        oriThighTwistBones = []
    356        oriClafTwistBones = []
    357        thighChildren = inThigh.children
    358        calfChildren = inCalf.children
    359
    360        if len(thighChildren) < 1 or len(calfChildren) < 1:
    361            return False
    362
    363        for item in thighChildren:
    364            testName = item.name.lower()
    365            if testName.find("twist") != -1:
    366                oriThighTwistBones.append(item)
    367
    368        for item in calfChildren:
    369            testName = item.name.lower()
    370            if testName.find("twist") != -1:
    371                oriClafTwistBones.append(item)
    372
    373        for item in oriThighTwistBones:
    374            liftTwistBoneName = self.name.add_suffix_to_real_name(item.name, filteringChar + "Lift")
    375            liftTwistHelperName = self.name.add_suffix_to_real_name(item.name, filteringChar + "Lift")
    376            if isLowerName:
    377                liftTwistBoneName = liftTwistBoneName.lower()
    378                liftTwistHelperName = liftTwistHelperName.lower()
    379
    380            liftTwistBone = self.bone.create_nub_bone(liftTwistBoneName, 2)
    381            liftTwistBone.name = self.name.remove_name_part("Nub", liftTwistBone.name)
    382            liftTwistBone.name = self.name.replace_name_part("Index", liftTwistBone.name, self.name.get_name("Index", oriThighTwistBones.name))
    383
    384            rt.setProperty(liftTwistBone, "transform", item.transform)
    385            liftTwistBone.parent = item
    386
    387            liftTwistHelper = self.helper.create_point(liftTwistHelperName)
    388            liftTwistHelper.name = self.name.replace_name_part("Type", liftTwistHelper.name, self.name.get_name_part_value_by_description("Type", "Position"))
    389
    390            rt.setProperty(liftTwistHelper, "transform", item.transform)
    391            liftTwistHelper.parent = self.thighRotHelper
    392
    393            liftTwistBonePosConst = self.const.assign_pos_const(liftTwistBone, liftTwistHelper)
    394
    395            self.thighTwistBones.append(liftTwistBone)
    396            self.thighTwistHelpers.append(liftTwistHelper)
    397
    398        for item in oriClafTwistBones:
    399            liftTwistBoneName = self.name.add_suffix_to_real_name(item.name, filteringChar + "Lift")
    400            liftTwistHelperName = self.name.add_suffix_to_real_name(item.name, filteringChar + "Lift")
    401            if isLowerName:
    402                liftTwistBoneName = liftTwistBoneName.lower()
    403                liftTwistHelperName = liftTwistHelperName.lower()
    404
    405            liftTwistBone = self.bone.create_nub_bone(liftTwistBoneName, 2)
    406            liftTwistBone.name = self.name.remove_name_part("Nub", liftTwistBone.name)
    407            liftTwistBone.name = self.name.replace_name_part("Index", liftTwistBone.name, self.name.get_name("Index", oriClafTwistBones.name))
    408
    409            rt.setProperty(liftTwistBone, "transform", item.transform)
    410            liftTwistBone.parent = item
    411
    412            liftTwistHelper = self.helper.create_point(liftTwistHelperName)
    413            liftTwistHelper.name = self.name.replace_name_part("Type", liftTwistHelper.name, self.name.get_name_part_value_by_description("Type", "Position"))
    414
    415            rt.setProperty(liftTwistHelper, "transform", item.transform)
    416            liftTwistHelper.parent = self.calfRotHelper
    417
    418            liftTwistBonePosConst = self.const.assign_pos_const(liftTwistBone, liftTwistHelper)
    419
    420            self.calfTwistBones.append(liftTwistBone)
    421            self.calfTwistHelpers.append(liftTwistHelper)
    422
    423    def create_bone(self, inThigh, inCalf, inFoot, inLiftScale=0.05, inKneePopScale=1.0, inKneeBackScale=1.0):
    424        """
    425        자동 무릎 본 시스템의 모든 요소를 생성하는 주요 메서드입니다.
    426
    427        이 메서드는 다음 단계들을 순차적으로 실행합니다:
    428        1. LookAt 헬퍼 생성
    429        2. 회전 루트 헬퍼 생성
    430        3. 회전 헬퍼 생성
    431        4. 대퇴골과 종아리뼈 회전 제약 설정
    432        5. 무릎 중간 본 생성
    433        6. 비틀림 본 생성 및 제약 설정
    434
    435        Args:
    436            inThigh: 대퇴골 본 객체
    437            inCalf: 종아리뼈 본 객체
    438            inFoot: 발 본 객체
    439            inLiftScale: 회전 영향력 스케일 (0.0~1.0)
    440            inKneePopScale: 무릎 앞쪽 돌출 스케일 (1.0이 기본값)
    441            inKneeBackScale: 무릎 뒤쪽 돌출 스케일 (1.0이 기본값)
    442
    443        Returns:
    444            bool: 자동 무릎 본 시스템 생성 성공 여부
    445        """
    446        if not rt.isValidNode(inThigh) or not rt.isValidNode(inCalf) or not rt.isValidNode(inFoot):
    447            return False
    448
    449        self.create_lookat_helper(inThigh, inFoot)
    450        self.create_rot_root_heleprs(inThigh, inCalf, inFoot)
    451        self.create_rot_helper(inThigh, inCalf, inFoot)
    452        self.assign_thigh_rot_constraint(inLiftScale=inLiftScale)
    453        self.assign_calf_rot_constraint(inLiftScale=inLiftScale)
    454        self.create_middle_bone(inThigh, inCalf, inKneePopScale=inKneePopScale, inKneeBackScale=inKneeBackScale)
    455        self.create_twist_bones(inThigh, inCalf)
    456
    457        # 결과를 딕셔너리 형태로 준비
    458        result = {
    459            "Thigh": inThigh,
    460            "Calf": inCalf,
    461            "Foot": inFoot,
    462            "LookAtHelper": self.lookAtHleper,
    463            "ThighRotHelper": self.thighRotHelper,
    464            "CalfRotHelper": self.calfRotHelper,
    465            "ThighRotRootHelper": self.thighRotRootHelper,
    466            "CalfRotRootHelper": self.calfRotRootHelper,
    467            "ThighTwistBones": self.thighTwistBones,
    468            "CalfTwistBones": self.calfTwistBones,
    469            "ThighTwistHelpers": self.thighTwistHelpers,
    470            "CalfTwistHelpers": self.calfTwistHelpers,
    471            "MiddleBones": self.middleBones,
    472            "LiftScale": inLiftScale,
    473            "KneePopScale": inKneePopScale,
    474            "KneeBackScale": inKneeBackScale
    475        }
    476
    477        # 메소드 호출 후 데이터 초기화
    478        self.reset()
    479
    480        return result
    481
    482    def reset(self):
    483        """
    484        클래스의 주요 컴포넌트들을 초기화합니다.
    485        서비스가 아닌 클래스 자체의 작업 데이터를 초기화하는 함수입니다.
    486
    487        Returns:
    488            self: 메소드 체이닝을 위한 자기 자신 반환
    489        """
    490        self.thigh = None
    491        self.calf = None
    492        self.foot = None
    493
    494        self.lookAtHleper = None
    495        self.thighRotHelper = None
    496        self.calfRotHelper = None
    497
    498        self.thighRotRootHelper = None
    499        self.calfRotRootHelper = None
    500
    501        self.thighTwistBones = []
    502        self.calfTwistBones = []
    503        self.thighTwistHelpers = []
    504        self.calfTwistHelpers = []
    505
    506        self.middleBones = []
    507
    508        self.liftScale = 0.025
    509
    510        return self

자동 무릎 본(AutoKnee) 관련 기능을 제공하는 클래스. MAXScript의 _AutoKneeBone 구조체 개념을 Python으로 재구현한 클래스이며, 3ds Max의 기능들을 pymxs API를 통해 제어합니다.

이 클래스는 IK 시스템 기반의 다리 리깅을 자동화하며, 무릎 관절 회전, 비틀림 본 및 중간 본을 생성하여 자연스러운 무릎 움직임을 구현합니다.

KneeBone( nameService=None, animService=None, helperService=None, boneService=None, constraintService=None, volumeBoneService=None) View Source

    30    def __init__(self, nameService=None, animService=None, helperService=None, boneService=None, constraintService=None, volumeBoneService=None):
    31        """
    32        KneeBone 클래스 초기화
    33
    34        Args:
    35            nameService: 이름 처리 서비스 (제공되지 않으면 새로 생성)
    36            animService: 애니메이션 서비스 (제공되지 않으면 새로 생성)
    37            helperService: 헬퍼 객체 서비스 (제공되지 않으면 새로 생성)
    38            boneService: 뼈대 서비스 (제공되지 않으면 새로 생성)
    39            constraintService: 제약 서비스 (제공되지 않으면 새로 생성)
    40            volumeBoneService: 볼륨 본 서비스 (제공되지 않으면 새로 생성)
    41        """
    42        # 서비스 인스턴스 설정 또는 생성
    43        self.name = nameService if nameService else Name()
    44        self.anim = animService if animService else Anim()
    45        # 종속성이 있는 서비스들은 이미 생성된 서비스들을 전달
    46        self.helper = helperService if helperService else Helper(nameService=self.name)
    47        self.bone = boneService if boneService else Bone(nameService=self.name, animService=self.anim)
    48        self.const = constraintService if constraintService else Constraint(nameService=self.name)
    49        self.volumeBone = volumeBoneService if volumeBoneService else VolumeBone(nameService=self.name, animService=self.anim, constraintService=self.const, boneService=self.bone, helperService=self.helper)
    50
    51        self.thigh = None
    52        self.calf = None
    53        self.foot = None
    54
    55        self.lookAtHleper = None
    56        self.thighRotHelper = None
    57        self.calfRotHelper = None
    58
    59        self.thighRotRootHelper = None
    60        self.calfRotRootHelper = None
    61
    62        self.thighTwistBones = []
    63        self.calfTwistBones = []
    64        self.thighTwistHelpers = []
    65        self.calfTwistHelpers = []
    66
    67        self.middleBones = []
    68
    69        self.liftScale = 0.025
    70
    71        self.thighRotScriptExpression = (
    72            "localLimbTm = limb.transform * inverse limbParent.transform\n"
    73            "localDeltaTm = localLimbTm * inverse localRotRefTm\n"
    74            "\n"
    75            "q = localDeltaTm.rotation\n"
    76            "\n"
    77            "axis = [0,0,1]\n"
    78            "\n"
    79            "proj = (dot q.axis axis) * axis\n"
    80            "twist = quat -q.angle proj\n"
    81            "twist = normalize twist\n"
    82            "\n"
    83            "twist\n"
    84        )
    85        self.calfRotScriptExpression = (
    86            "localLimbTm = limb.transform * inverse limbParent.transform\n"
    87            "localDeltaTm = localLimbTm * inverse localRotRefTm\n"
    88            "\n"
    89            "q = localDeltaTm.rotation\n"
    90            "\n"
    91            "axis = [0,0,1]\n"
    92            "\n"
    93            "proj = (dot q.axis axis) * axis\n"
    94            "twist = quat q.angle proj\n"
    95            "twist = normalize twist\n"
    96            "\n"
    97            "twist\n"
    98        )

KneeBone 클래스 초기화

Args: nameService: 이름 처리 서비스 (제공되지 않으면 새로 생성) animService: 애니메이션 서비스 (제공되지 않으면 새로 생성) helperService: 헬퍼 객체 서비스 (제공되지 않으면 새로 생성) boneService: 뼈대 서비스 (제공되지 않으면 새로 생성) constraintService: 제약 서비스 (제공되지 않으면 새로 생성) volumeBoneService: 볼륨 본 서비스 (제공되지 않으면 새로 생성)

name
anim
helper
bone
const
volumeBone
thigh
calf
foot
lookAtHleper
thighRotHelper
calfRotHelper
thighRotRootHelper
calfRotRootHelper
thighTwistBones
calfTwistBones
thighTwistHelpers
calfTwistHelpers
middleBones
liftScale
thighRotScriptExpression
calfRotScriptExpression
def create_lookat_helper(self, inThigh, inFoot): View Source

    101    def create_lookat_helper(self, inThigh, inFoot):
    102        """
    103        무릎 시스템을 위한 LookAt 헬퍼 객체를 생성합니다.
    104
    105        이 헬퍼는 대퇴골(Thigh)에 위치하면서 발(Foot)을 바라보도록 제약됩니다.
    106        무릎 회전의 기반이 되는 방향을 결정하는 역할을 합니다.
    107
    108        Args:
    109            inThigh: 대퇴골 본 객체
    110            inFoot: 발 본 객체
    111
    112        Returns:
    113            bool: 헬퍼 생성 성공 여부
    114        """
    115        if not rt.isValidNode(inThigh) or not rt.isValidNode(inFoot):
    116            return False
    117
    118        filteringChar = self.name._get_filtering_char(inThigh.name)
    119        isLowerName = inThigh.name.islower()
    120
    121        # 서비스 인스턴스 설정 또는 생성
    122        self.thigh = inThigh
    123        self.foot = inFoot
    124
    125        lookAtHelperName = self.name.replace_name_part("Type", inThigh.name, self.name.get_name_part_value_by_description("Type", "LookAt"))
    126        lookAtHelperName = self.name.add_suffix_to_real_name(lookAtHelperName, filteringChar + "Lift")
    127        if isLowerName:
    128            lookAtHelperName = lookAtHelperName.lower()
    129
    130        lookAtHelper = self.helper.create_point(lookAtHelperName)
    131        lookAtHelper.transform = inThigh.transform
    132        lookAtHelper.parent = inThigh
    133        lookAtConst = self.const.assign_lookat(lookAtHelper, inFoot)
    134        lookAtConst.upnode_world = False
    135        lookAtConst.pickUpNode = inThigh
    136        lookAtConst.lookat_vector_length = 0.0
    137
    138        self.lookAtHleper = lookAtHelper

무릎 시스템을 위한 LookAt 헬퍼 객체를 생성합니다.

이 헬퍼는 대퇴골(Thigh)에 위치하면서 발(Foot)을 바라보도록 제약됩니다. 무릎 회전의 기반이 되는 방향을 결정하는 역할을 합니다.

Args: inThigh: 대퇴골 본 객체 inFoot: 발 본 객체

Returns: bool: 헬퍼 생성 성공 여부

def create_rot_root_heleprs(self, inThigh, inCalf, inFoot): View Source

    140    def create_rot_root_heleprs(self, inThigh, inCalf, inFoot):
    141        """
    142        무릎 회전의 기준이 되는 루트 헬퍼 객체들을 생성합니다.
    143
    144        대퇴골과 종아리뼈에 각각 위치하며, 비틀림 계산을 위한 기준점 역할을 합니다.
    145
    146        Args:
    147            inThigh: 대퇴골 본 객체
    148            inCalf: 종아리뼈 본 객체
    149            inFoot: 발 본 객체
    150
    151        Returns:
    152            bool: 헬퍼 생성 성공 여부
    153        """
    154        if not rt.isValidNode(inThigh) or not rt.isValidNode(inCalf) or not rt.isValidNode(inFoot):
    155            return False
    156
    157        filteringChar = self.name._get_filtering_char(inThigh.name)
    158        isLowerName = inThigh.name.islower()
    159
    160        # 서비스 인스턴스 설정 또는 생성
    161        self.thigh = inThigh
    162        self.calf = inCalf
    163        self.foot = inFoot
    164
    165        thighRotRootHelperName = self.name.replace_name_part("Type", inThigh.name, self.name.get_name_part_value_by_description("Type", "Dummy"))
    166        calfRotRootHelperName = self.name.replace_name_part("Type", inCalf.name, self.name.get_name_part_value_by_description("Type", "Dummy"))
    167        thighRotRootHelperName = self.name.add_suffix_to_real_name(thighRotRootHelperName, filteringChar + "Lift")
    168        calfRotRootHelperName = self.name.add_suffix_to_real_name(calfRotRootHelperName, filteringChar + "Lift")
    169        if isLowerName:
    170            thighRotRootHelperName = thighRotRootHelperName.lower()
    171            calfRotRootHelperName = calfRotRootHelperName.lower()
    172
    173        thighRotRootHelper = self.helper.create_point(thighRotRootHelperName, crossToggle=False, boxToggle=True)
    174        thighRotRootHelper.transform = inThigh.transform
    175        thighRotRootHelper.parent = inThigh
    176
    177        calfRotRootHelper = self.helper.create_point(calfRotRootHelperName, crossToggle=False, boxToggle=True)
    178        calfRotRootHelper.transform = inCalf.transform
    179        calfRotRootHelper.position = inFoot.position
    180        calfRotRootHelper.parent = inCalf
    181
    182        self.thighRotRootHelper = thighRotRootHelper
    183        self.calfRotRootHelper = calfRotRootHelper

무릎 회전의 기준이 되는 루트 헬퍼 객체들을 생성합니다.

대퇴골과 종아리뼈에 각각 위치하며, 비틀림 계산을 위한 기준점 역할을 합니다.

Args: inThigh: 대퇴골 본 객체 inCalf: 종아리뼈 본 객체 inFoot: 발 본 객체

Returns: bool: 헬퍼 생성 성공 여부

def create_rot_helper(self, inThigh, inCalf, inFoot): View Source

    185    def create_rot_helper(self, inThigh, inCalf, inFoot):
    186        """
    187        대퇴골과 종아리뼈의 회전을 제어하는 헬퍼 객체들을 생성합니다.
    188
    189        이 헬퍼들은 실제 무릎 움직임에 따른 비틀림 효과를 구현하는 데 사용됩니다.
    190
    191        Args:
    192            inThigh: 대퇴골 본 객체
    193            inCalf: 종아리뼈 본 객체
    194            inFoot: 발 본 객체
    195
    196        Returns:
    197            bool: 헬퍼 생성 성공 여부
    198        """
    199        if not rt.isValidNode(inThigh) or not rt.isValidNode(inCalf):
    200            return False
    201
    202        filteringChar = self.name._get_filtering_char(inThigh.name)
    203        isLowerName = inThigh.name.islower()
    204
    205        # 서비스 인스턴스 설정 또는 생성
    206        self.thigh = inThigh
    207        self.calf = inCalf
    208
    209        thighRotHelperName = self.name.replace_name_part("Type", inThigh.name, self.name.get_name_part_value_by_description("Type", "Rotation"))
    210        calfRotHelperName = self.name.replace_name_part("Type", inCalf.name, self.name.get_name_part_value_by_description("Type", "Rotation"))
    211        thighRotHelperName = self.name.add_suffix_to_real_name(thighRotHelperName, filteringChar + "Lift")
    212        calfRotHelperName = self.name.add_suffix_to_real_name(calfRotHelperName, filteringChar + "Lift")
    213        if isLowerName:
    214            thighRotHelperName = thighRotHelperName.lower()
    215            calfRotHelperName = calfRotHelperName.lower()
    216
    217        thighRotHelper = self.helper.create_point(thighRotHelperName)
    218        thighRotHelper.transform = inThigh.transform
    219        thighRotHelper.parent = inThigh
    220
    221        calfRotHelper = self.helper.create_point(calfRotHelperName)
    222        calfRotHelper.transform = inCalf.transform
    223        calfRotHelper.position = inFoot.transform.position
    224        calfRotHelper.parent = inCalf
    225
    226        self.thighRotHelper = thighRotHelper
    227        self.calfRotHelper = calfRotHelper

대퇴골과 종아리뼈의 회전을 제어하는 헬퍼 객체들을 생성합니다.

이 헬퍼들은 실제 무릎 움직임에 따른 비틀림 효과를 구현하는 데 사용됩니다.

Args: inThigh: 대퇴골 본 객체 inCalf: 종아리뼈 본 객체 inFoot: 발 본 객체

Returns: bool: 헬퍼 생성 성공 여부

def assign_thigh_rot_constraint(self, inLiftScale=0.1): View Source

    229    def assign_thigh_rot_constraint(self, inLiftScale=0.1):
    230        """
    231        대퇴골 회전 헬퍼에 스크립트 기반 회전 제약을 할당합니다.
    232
    233        LookAt 헬퍼와 대퇴골 회전 루트 헬퍼 사이의 관계를 기반으로 비틀림 회전을 계산합니다.
    234
    235        Args:
    236            inLiftScale: 회전 영향력 스케일 (0.0~1.0)
    237        """
    238        self.liftScale = inLiftScale
    239        localRotRefTm = self.lookAtHleper.transform * rt.inverse(self.thighRotRootHelper.transform)
    240
    241        rotListConst = self.const.assign_rot_list(self.thighRotHelper)
    242        rotScriptConst = rt.Rotation_Script()
    243        rt.setPropertyController(rotListConst, "Available", rotScriptConst)
    244        rotListConst.setActive(rotListConst.count)
    245
    246        rotScriptConst.addConstant("localRotRefTm", localRotRefTm)
    247        rotScriptConst.addNode("limb", self.lookAtHleper)
    248        rotScriptConst.addNode("limbParent", self.thighRotRootHelper)
    249        rotScriptConst.setExpression(self.thighRotScriptExpression)
    250
    251        self.const.set_rot_controllers_weight_in_list(self.thighRotHelper, 1, self.liftScale * 100.0)

대퇴골 회전 헬퍼에 스크립트 기반 회전 제약을 할당합니다.

LookAt 헬퍼와 대퇴골 회전 루트 헬퍼 사이의 관계를 기반으로 비틀림 회전을 계산합니다.

Args: inLiftScale: 회전 영향력 스케일 (0.0~1.0)

def assign_calf_rot_constraint(self, inLiftScale=0.1): View Source

    253    def assign_calf_rot_constraint(self, inLiftScale=0.1):
    254        """
    255        종아리뼈 회전 헬퍼에 스크립트 기반 회전 제약을 할당합니다.
    256
    257        LookAt 헬퍼와 대퇴골 회전 루트 헬퍼 사이의 관계를 기반으로 비틀림 회전을 계산합니다.
    258
    259        Args:
    260            inLiftScale: 회전 영향력 스케일 (0.0~1.0)
    261        """
    262        self.liftScale = inLiftScale
    263        localRotRefTm = self.lookAtHleper.transform * rt.inverse(self.thighRotRootHelper.transform)
    264
    265        rotListConst = self.const.assign_rot_list(self.calfRotHelper)
    266        rotScriptConst = rt.Rotation_Script()
    267        rt.setPropertyController(rotListConst, "Available", rotScriptConst)
    268        rotListConst.setActive(rotListConst.count)
    269
    270        rotScriptConst.addConstant("localRotRefTm", localRotRefTm)
    271        rotScriptConst.addNode("limb", self.lookAtHleper)
    272        rotScriptConst.addNode("limbParent", self.thighRotRootHelper)
    273        rotScriptConst.setExpression(self.calfRotScriptExpression)
    274
    275        self.const.set_rot_controllers_weight_in_list(self.calfRotHelper, 1, self.liftScale * 100.0)

종아리뼈 회전 헬퍼에 스크립트 기반 회전 제약을 할당합니다.

LookAt 헬퍼와 대퇴골 회전 루트 헬퍼 사이의 관계를 기반으로 비틀림 회전을 계산합니다.

Args: inLiftScale: 회전 영향력 스케일 (0.0~1.0)

def create_middle_bone(self, inThigh, inCalf, inKneePopScale=1.0, inKneeBackScale=1.0): View Source

    277    def create_middle_bone(self, inThigh, inCalf, inKneePopScale=1.0, inKneeBackScale=1.0):
    278        """
    279        무릎 중간 본을 생성합니다.
    280
    281        이 본들은 무릎이 구부러질 때 앞(Pop)과 뒤(Back)로 움직이는 볼륨감 있는 본들입니다.
    282        무릎 관절의 시각적 품질을 향상시킵니다.
    283
    284        Args:
    285            inThigh: 대퇴골 본 객체
    286            inCalf: 종아리뼈 본 객체
    287            inKneePopScale: 무릎 앞쪽 돌출 스케일 (1.0이 기본값)
    288            inKneeBackScale: 무릎 뒤쪽 돌출 스케일 (1.0이 기본값)
    289
    290        Returns:
    291            bool: 중간 본 생성 성공 여부
    292        """
    293        if not rt.isValidNode(inThigh) or not rt.isValidNode(inCalf):
    294            return False
    295
    296        facingDirVec = inCalf.transform.position - inThigh.transform.position
    297        inObjXAxisVec = inCalf.objectTransform.row1
    298        distanceDir = 1.0 if rt.dot(inObjXAxisVec, facingDirVec) > 0 else -1.0
    299
    300        self.thigh = inThigh
    301        self.calf = inCalf
    302
    303        transScales = []
    304        if distanceDir > 0:
    305            transScales.append(inKneePopScale)
    306            transScales.append(inKneeBackScale)
    307        else:
    308            transScales.append(inKneeBackScale)
    309            transScales.append(inKneePopScale)
    310
    311        result = self.volumeBone.create_bones(self.calf, self.thigh, inVolumeSize=5.0, inRotAxises=["Z", "Z"], inTransAxises=["PosY", "NegY"], inTransScales=transScales)
    312
    313        calfName = self.name.get_name_part("RealName", inCalf.name)
    314        isLower = calfName[0].islower()
    315        replaceName = "Knee"
    316        if isLower:
    317            replaceName = replaceName.lower()
    318
    319        for item in result["Bones"]:
    320            item.name.replace(calfName, replaceName)
    321
    322        result["rootBone"].name.replace(calfName, replaceName)
    323        result["RotHelper"].name.replace(calfName, replaceName)
    324
    325        # 결과 저장
    326        if result and "Bones" in result:
    327            self.middleBones.extend(result["Bones"])
    328
    329        return result

무릎 중간 본을 생성합니다.

이 본들은 무릎이 구부러질 때 앞(Pop)과 뒤(Back)로 움직이는 볼륨감 있는 본들입니다. 무릎 관절의 시각적 품질을 향상시킵니다.

Args: inThigh: 대퇴골 본 객체 inCalf: 종아리뼈 본 객체 inKneePopScale: 무릎 앞쪽 돌출 스케일 (1.0이 기본값) inKneeBackScale: 무릎 뒤쪽 돌출 스케일 (1.0이 기본값)

Returns: bool: 중간 본 생성 성공 여부

def create_twist_bones(self, inThigh, inCalf): View Source

    331    def create_twist_bones(self, inThigh, inCalf):
    332        """
    333        대퇴골과 종아리뼈에 연결된 비틀림 본들에 대한 리프팅 본과 헬퍼를 생성합니다.
    334
    335        기존 비틀림 본들을 찾아 각각에 대응하는 리프팅 본과 헬퍼를 생성하여
    336        무릎 구부림에 따라 자연스럽게 회전하도록 제약을 설정합니다.
    337
    338        Args:
    339            inThigh: 대퇴골 본 객체
    340            inCalf: 종아리뼈 본 객체
    341
    342        Returns:
    343            bool: 비틀림 본 생성 성공 여부
    344        """
    345        if not rt.isValidNode(inThigh) or not rt.isValidNode(inCalf):
    346            return False
    347
    348        filteringChar = self.name._get_filtering_char(inThigh.name)
    349        isLowerName = inThigh.name.islower()
    350
    351        # 서비스 인스턴스 설정 또는 생성
    352        self.thigh = inThigh
    353        self.calf = inCalf
    354
    355        oriThighTwistBones = []
    356        oriClafTwistBones = []
    357        thighChildren = inThigh.children
    358        calfChildren = inCalf.children
    359
    360        if len(thighChildren) < 1 or len(calfChildren) < 1:
    361            return False
    362
    363        for item in thighChildren:
    364            testName = item.name.lower()
    365            if testName.find("twist") != -1:
    366                oriThighTwistBones.append(item)
    367
    368        for item in calfChildren:
    369            testName = item.name.lower()
    370            if testName.find("twist") != -1:
    371                oriClafTwistBones.append(item)
    372
    373        for item in oriThighTwistBones:
    374            liftTwistBoneName = self.name.add_suffix_to_real_name(item.name, filteringChar + "Lift")
    375            liftTwistHelperName = self.name.add_suffix_to_real_name(item.name, filteringChar + "Lift")
    376            if isLowerName:
    377                liftTwistBoneName = liftTwistBoneName.lower()
    378                liftTwistHelperName = liftTwistHelperName.lower()
    379
    380            liftTwistBone = self.bone.create_nub_bone(liftTwistBoneName, 2)
    381            liftTwistBone.name = self.name.remove_name_part("Nub", liftTwistBone.name)
    382            liftTwistBone.name = self.name.replace_name_part("Index", liftTwistBone.name, self.name.get_name("Index", oriThighTwistBones.name))
    383
    384            rt.setProperty(liftTwistBone, "transform", item.transform)
    385            liftTwistBone.parent = item
    386
    387            liftTwistHelper = self.helper.create_point(liftTwistHelperName)
    388            liftTwistHelper.name = self.name.replace_name_part("Type", liftTwistHelper.name, self.name.get_name_part_value_by_description("Type", "Position"))
    389
    390            rt.setProperty(liftTwistHelper, "transform", item.transform)
    391            liftTwistHelper.parent = self.thighRotHelper
    392
    393            liftTwistBonePosConst = self.const.assign_pos_const(liftTwistBone, liftTwistHelper)
    394
    395            self.thighTwistBones.append(liftTwistBone)
    396            self.thighTwistHelpers.append(liftTwistHelper)
    397
    398        for item in oriClafTwistBones:
    399            liftTwistBoneName = self.name.add_suffix_to_real_name(item.name, filteringChar + "Lift")
    400            liftTwistHelperName = self.name.add_suffix_to_real_name(item.name, filteringChar + "Lift")
    401            if isLowerName:
    402                liftTwistBoneName = liftTwistBoneName.lower()
    403                liftTwistHelperName = liftTwistHelperName.lower()
    404
    405            liftTwistBone = self.bone.create_nub_bone(liftTwistBoneName, 2)
    406            liftTwistBone.name = self.name.remove_name_part("Nub", liftTwistBone.name)
    407            liftTwistBone.name = self.name.replace_name_part("Index", liftTwistBone.name, self.name.get_name("Index", oriClafTwistBones.name))
    408
    409            rt.setProperty(liftTwistBone, "transform", item.transform)
    410            liftTwistBone.parent = item
    411
    412            liftTwistHelper = self.helper.create_point(liftTwistHelperName)
    413            liftTwistHelper.name = self.name.replace_name_part("Type", liftTwistHelper.name, self.name.get_name_part_value_by_description("Type", "Position"))
    414
    415            rt.setProperty(liftTwistHelper, "transform", item.transform)
    416            liftTwistHelper.parent = self.calfRotHelper
    417
    418            liftTwistBonePosConst = self.const.assign_pos_const(liftTwistBone, liftTwistHelper)
    419
    420            self.calfTwistBones.append(liftTwistBone)
    421            self.calfTwistHelpers.append(liftTwistHelper)

대퇴골과 종아리뼈에 연결된 비틀림 본들에 대한 리프팅 본과 헬퍼를 생성합니다.

기존 비틀림 본들을 찾아 각각에 대응하는 리프팅 본과 헬퍼를 생성하여 무릎 구부림에 따라 자연스럽게 회전하도록 제약을 설정합니다.

Args: inThigh: 대퇴골 본 객체 inCalf: 종아리뼈 본 객체

Returns: bool: 비틀림 본 생성 성공 여부

def create_bone( self, inThigh, inCalf, inFoot, inLiftScale=0.05, inKneePopScale=1.0, inKneeBackScale=1.0): View Source

    423    def create_bone(self, inThigh, inCalf, inFoot, inLiftScale=0.05, inKneePopScale=1.0, inKneeBackScale=1.0):
    424        """
    425        자동 무릎 본 시스템의 모든 요소를 생성하는 주요 메서드입니다.
    426
    427        이 메서드는 다음 단계들을 순차적으로 실행합니다:
    428        1. LookAt 헬퍼 생성
    429        2. 회전 루트 헬퍼 생성
    430        3. 회전 헬퍼 생성
    431        4. 대퇴골과 종아리뼈 회전 제약 설정
    432        5. 무릎 중간 본 생성
    433        6. 비틀림 본 생성 및 제약 설정
    434
    435        Args:
    436            inThigh: 대퇴골 본 객체
    437            inCalf: 종아리뼈 본 객체
    438            inFoot: 발 본 객체
    439            inLiftScale: 회전 영향력 스케일 (0.0~1.0)
    440            inKneePopScale: 무릎 앞쪽 돌출 스케일 (1.0이 기본값)
    441            inKneeBackScale: 무릎 뒤쪽 돌출 스케일 (1.0이 기본값)
    442
    443        Returns:
    444            bool: 자동 무릎 본 시스템 생성 성공 여부
    445        """
    446        if not rt.isValidNode(inThigh) or not rt.isValidNode(inCalf) or not rt.isValidNode(inFoot):
    447            return False
    448
    449        self.create_lookat_helper(inThigh, inFoot)
    450        self.create_rot_root_heleprs(inThigh, inCalf, inFoot)
    451        self.create_rot_helper(inThigh, inCalf, inFoot)
    452        self.assign_thigh_rot_constraint(inLiftScale=inLiftScale)
    453        self.assign_calf_rot_constraint(inLiftScale=inLiftScale)
    454        self.create_middle_bone(inThigh, inCalf, inKneePopScale=inKneePopScale, inKneeBackScale=inKneeBackScale)
    455        self.create_twist_bones(inThigh, inCalf)
    456
    457        # 결과를 딕셔너리 형태로 준비
    458        result = {
    459            "Thigh": inThigh,
    460            "Calf": inCalf,
    461            "Foot": inFoot,
    462            "LookAtHelper": self.lookAtHleper,
    463            "ThighRotHelper": self.thighRotHelper,
    464            "CalfRotHelper": self.calfRotHelper,
    465            "ThighRotRootHelper": self.thighRotRootHelper,
    466            "CalfRotRootHelper": self.calfRotRootHelper,
    467            "ThighTwistBones": self.thighTwistBones,
    468            "CalfTwistBones": self.calfTwistBones,
    469            "ThighTwistHelpers": self.thighTwistHelpers,
    470            "CalfTwistHelpers": self.calfTwistHelpers,
    471            "MiddleBones": self.middleBones,
    472            "LiftScale": inLiftScale,
    473            "KneePopScale": inKneePopScale,
    474            "KneeBackScale": inKneeBackScale
    475        }
    476
    477        # 메소드 호출 후 데이터 초기화
    478        self.reset()
    479
    480        return result

자동 무릎 본 시스템의 모든 요소를 생성하는 주요 메서드입니다.

이 메서드는 다음 단계들을 순차적으로 실행합니다:

1. LookAt 헬퍼 생성
2. 회전 루트 헬퍼 생성
3. 회전 헬퍼 생성
4. 대퇴골과 종아리뼈 회전 제약 설정
5. 무릎 중간 본 생성
6. 비틀림 본 생성 및 제약 설정

Args: inThigh: 대퇴골 본 객체 inCalf: 종아리뼈 본 객체 inFoot: 발 본 객체 inLiftScale: 회전 영향력 스케일 (0.0~1.0) inKneePopScale: 무릎 앞쪽 돌출 스케일 (1.0이 기본값) inKneeBackScale: 무릎 뒤쪽 돌출 스케일 (1.0이 기본값)

Returns: bool: 자동 무릎 본 시스템 생성 성공 여부

def reset(self): View Source

    482    def reset(self):
    483        """
    484        클래스의 주요 컴포넌트들을 초기화합니다.
    485        서비스가 아닌 클래스 자체의 작업 데이터를 초기화하는 함수입니다.
    486
    487        Returns:
    488            self: 메소드 체이닝을 위한 자기 자신 반환
    489        """
    490        self.thigh = None
    491        self.calf = None
    492        self.foot = None
    493
    494        self.lookAtHleper = None
    495        self.thighRotHelper = None
    496        self.calfRotHelper = None
    497
    498        self.thighRotRootHelper = None
    499        self.calfRotRootHelper = None
    500
    501        self.thighTwistBones = []
    502        self.calfTwistBones = []
    503        self.thighTwistHelpers = []
    504        self.calfTwistHelpers = []
    505
    506        self.middleBones = []
    507
    508        self.liftScale = 0.025
    509
    510        return self

클래스의 주요 컴포넌트들을 초기화합니다. 서비스가 아닌 클래스 자체의 작업 데이터를 초기화하는 함수입니다.

Returns: self: 메소드 체이닝을 위한 자기 자신 반환

class Hip: View Source

     20class Hip:
     21    """
     22    Hip 관련 기능을 제공하는 클래스.
     23    MAXScript의 _Hip 구조체 개념을 Python으로 재구현한 클래스이며,
     24    3ds Max의 기능들을 pymxs API를 통해 제어합니다.
     25    """
     26
     27    def __init__(self, nameService=None, animService=None, helperService=None, boneService=None, constraintService=None):
     28        """
     29        클래스 초기화.
     30
     31        Args:
     32            nameService: 이름 처리 서비스 (제공되지 않으면 새로 생성)
     33            animService: 애니메이션 서비스 (제공되지 않으면 새로 생성)
     34            helperService: 헬퍼 객체 관련 서비스 (제공되지 않으면 새로 생성)
     35            boneService: 뼈대 관련 서비스 (제공되지 않으면 새로 생성)
     36            constraintService: 제약 관련 서비스 (제공되지 않으면 새로 생성)
     37            bipService: Biped 관련 서비스 (제공되지 않으면 새로 생성)
     38        """
     39        # 서비스 인스턴스 설정 또는 생성
     40        self.name = nameService if nameService else Name()
     41        self.anim = animService if animService else Anim()
     42
     43        # 종속성이 있는 서비스들은 이미 생성된 서비스들을 전달
     44        self.helper = helperService if helperService else Helper(nameService=self.name)
     45        self.const = constraintService if constraintService else Constraint(nameService=self.name, helperService=self.helper)
     46        self.bone = boneService if boneService else Bone(nameService=self.name, animService=self.anim, helperService=self.helper, constraintService=self.const)
     47
     48        # 기본 속성 초기화
     49        self.pelvisWeight = 0.6
     50        self.thighWeight = 0.4
     51        self.pushAmount = 10
     52
     53        self.pelvis = None
     54        self.thigh = None
     55        self.thighTwist = None
     56        self.calf = None
     57
     58        self.pelvisHelper = None
     59        self.thighHelper = None
     60        self.thighTwistHelper = None
     61        self.thighRotHelper = None
     62        self.thighPosHelper = None
     63        self.thighRotRootHelper = None
     64
     65        self.helpers = []
     66        self.bones = []
     67
     68        self.posScriptExpression = (
     69            "localLimbTm = limb.transform * inverse limbParent.transform\n"
     70            "localDeltaTm = localLimbTm * inverse localRotRefTm\n"
     71            "\n"
     72            "q = localDeltaTm.rotation\n"
     73            "\n"
     74            "eulerRot = (quatToEuler q order:5)\n"
     75            "swizzledRot = (eulerAngles eulerRot.y eulerRot.z eulerRot.x)\n"
     76            "\n"
     77            "axis = [0,0,1]\n"
     78            "\n"
     79            "saturatedTwistZ = (swizzledRot.x*axis.x + swizzledRot.y*axis.y + swizzledRot.z*axis.z)/180.0\n"
     80            "pushScaleY = amax 0.0 saturatedTwistZ\n"
     81            "\n"
     82            "axis = [0,1,0]\n"
     83            "saturatedTwistY = (swizzledRot.x*axis.x + swizzledRot.y*axis.y + swizzledRot.z*axis.z)/180.0\n"
     84            "pushScaleZ = amax 0.0 saturatedTwistY\n"
     85            "\n"
     86            "\n"
     87            "[0, pushAmount * pushScaleY, -pushAmount * pushScaleZ]\n"
     88        )
     89
     90    def reset(self):
     91        """
     92        클래스의 주요 컴포넌트들을 초기화합니다.
     93        서비스가 아닌 클래스 자체의 작업 데이터를 초기화하는 함수입니다.
     94
     95        Returns:
     96            self: 메소드 체이닝을 위한 자기 자신 반환
     97        """
     98        self.pelvisWeight = 0.6
     99        self.thighWeight = 0.4
    100        self.pushAmount = 10
    101
    102        self.pelvis = None
    103        self.thigh = None
    104        self.thighTwist = None
    105        self.calf = None
    106
    107        self.pelvisHelper = None
    108        self.thighHelper = None
    109        self.thighTwistHelper = None
    110        self.thighRotHelper = None
    111        self.thighPosHelper = None
    112        self.thighRotRootHelper = None
    113
    114        self.helpers = []
    115        self.bones = []
    116
    117        return self
    118
    119    def create_helper(self, inPelvis, inThigh, inThighTwist):
    120        if not rt.isValidNode(inPelvis) or not rt.isValidNode(inThigh) or not rt.isValidNode(inThighTwist):
    121            return False
    122
    123        self.pelvis = inPelvis
    124        self.thigh = inThigh
    125        self.thighTwist = inThighTwist
    126
    127        filteringChar = self.name._get_filtering_char(inThigh.name)
    128        isLower = inThigh.name[0].islower()
    129
    130        pelvisHelperName = self.name.replace_name_part("RealName", inThigh.name, self.name.get_RealName(inPelvis.name)+filteringChar+"Hip")
    131        pelvisHelperName = self.name.replace_name_part("Type", pelvisHelperName, self.name.get_name_part_value_by_description("Type", "Dummy"))
    132        pelvisHelper = self.helper.create_point(pelvisHelperName)
    133        rt.setProperty(pelvisHelper, "transform", inThigh.transform)
    134        pelvisHelper.parent = inPelvis
    135
    136        tihgTwistHeleprName = self.name.replace_name_part("RealName", inThigh.name, self.name.get_RealName(inThighTwist.name)+filteringChar+"Hip")
    137        tihgTwistHeleprName = self.name.replace_name_part("Type", tihgTwistHeleprName, self.name.get_name_part_value_by_description("Type", "Dummy"))
    138        thighTwistHelper = self.helper.create_point(tihgTwistHeleprName)
    139        rt.setProperty(thighTwistHelper, "transform", inThighTwist.transform)
    140        thighTwistHelper.parent = inThighTwist
    141
    142        tihghRotHelperName = self.name.replace_name_part("RealName", inThigh.name, self.name.get_RealName(inThigh.name)+filteringChar+"Hip")
    143        tihghRotHelperName = self.name.replace_name_part("Type", tihghRotHelperName, self.name.get_name_part_value_by_description("Type", "Rotation"))
    144        thighRotHelper = self.helper.create_point(tihghRotHelperName)
    145        rt.setProperty(thighRotHelper, "transform", inThighTwist.transform)
    146        thighRotHelper.parent = inThigh
    147
    148        thighPosHelperName = self.name.replace_name_part("RealName", inThigh.name, self.name.get_RealName(inThigh.name)+filteringChar+"Hip")
    149        thighPosHelperName = self.name.replace_name_part("Type", thighPosHelperName, self.name.get_name_part_value_by_description("Type", "Position"))
    150        thighPosHelper = self.helper.create_point(thighPosHelperName)
    151        rt.setProperty(thighPosHelper, "transform", inThighTwist.transform)
    152        thighPosHelper.parent = thighRotHelper
    153
    154        thighRotRootHelperName = self.name.replace_name_part("RealName", inThigh.name, self.name.get_RealName(inThigh.name)+filteringChar+"Hip")
    155        thighRotRootHelperName = self.name.replace_name_part("Type", thighRotRootHelperName, self.name.get_name_part_value_by_description("Type", "Dummy"))
    156        thighRotRootHelper = self.helper.create_point(thighRotRootHelperName)
    157        rt.setProperty(thighRotRootHelper, "transform", thighRotHelper.transform)
    158        thighRotRootHelper.parent = inThighTwist
    159
    160        if isLower:
    161            pelvisHelper.name = pelvisHelper.name.lower()
    162            thighTwistHelper.name = thighTwistHelper.name.lower()
    163            thighRotHelper.name = thighRotHelper.name.lower()
    164            thighPosHelper.name = thighPosHelper.name.lower()
    165            thighRotRootHelper.name = thighRotRootHelper.name.lower()
    166
    167        self.pelvisHelper = pelvisHelper
    168        self.thighTwistHelper = thighTwistHelper
    169        self.thighRotHelper = thighRotHelper
    170        self.thighPosHelper = thighPosHelper
    171        self.thighRotRootHelper = thighRotRootHelper
    172
    173        self.helpers.append(pelvisHelper)
    174        self.helpers.append(thighTwistHelper)
    175        self.helpers.append(thighRotHelper)
    176        self.helpers.append(thighPosHelper)
    177        self.helpers.append(thighRotRootHelper)
    178
    179    def assing_constraint(self, inCalf, inPelvisWeight=0.6, inThighWeight=0.4, inPushAmount=5.0):
    180        self.calf = inCalf
    181        self.pelvisWeight = inPelvisWeight
    182        self.thighWeight = inThighWeight
    183        self.pushAmount = rt.Float(inPushAmount)
    184
    185        facingDirVec = self.calf.transform.position - self.thigh.transform.position
    186        inObjXAxisVec = self.thigh.objectTransform.row1
    187        distanceDir = -1.0 if rt.dot(inObjXAxisVec, facingDirVec) > 0 else 1.0
    188
    189        rotConst = self.const.assign_rot_const_multi(self.thighRotHelper, [self.pelvisHelper, self.thighTwistHelper])
    190        rotConst.setWeight(1, self.pelvisWeight * 100.0)
    191        rotConst.setWeight(2, self.thighWeight * 100.0)
    192
    193        localRotRefTm = self.thighRotHelper.transform * rt.inverse(self.thighRotRootHelper.transform)
    194        posConst = self.const.assign_pos_script_controller(self.thighPosHelper)
    195        posConst.addNode("limb", self.thighRotHelper)
    196        posConst.addNode("limbParent", self.thighRotRootHelper)
    197        posConst.addConstant("localRotRefTm", localRotRefTm)
    198        posConst.addConstant("pushAmount", self.pushAmount*distanceDir)
    199        posConst.setExpression(self.posScriptExpression)
    200        posConst.update()
    201
    202    def create_bone(self, inPelvis, inThigh, inThighTwist, inCalf, pushAmount=5.0, inPelvisWeight=0.6, inThighWeight=0.4):
    203        if not rt.isValidNode(inPelvis) or not rt.isValidNode(inThigh) or not rt.isValidNode(inThighTwist):
    204            return False
    205
    206        self.create_helper(inPelvis, inThigh, inThighTwist)
    207        self.assing_constraint(inCalf, inPelvisWeight, inThighWeight, inPushAmount=pushAmount)
    208
    209        isLower = inThigh.name[0].islower()
    210        hipBoneName = self.name.replace_name_part("RealName", inThigh.name, "Hip")
    211        hipBone = self.bone.create_nub_bone(hipBoneName, 2)
    212        hipBone.name = self.name.remove_name_part("Nub", hipBone.name)
    213        if isLower:
    214            hipBone.name = hipBone.name.lower()
    215
    216        rt.setProperty(hipBone, "transform", inThighTwist.transform)
    217        hipBone.parent = inThigh
    218
    219        self.const.assign_rot_const(hipBone, self.thighRotHelper)
    220        self.const.assign_pos_const(hipBone, self.thighPosHelper)
    221
    222        self.bones.append(hipBone)
    223
    224        # 결과를 딕셔너리 형태로 준비
    225        result = {
    226            "Pelvis": inPelvis,
    227            "Thigh": inThigh,
    228            "ThighTwist": inThighTwist,
    229            "Bones": self.bones,
    230            "Helpers": self.helpers,
    231            "PelvisWeight": inPelvisWeight,
    232            "ThighWeight": inThighWeight,
    233            "PushAmount": pushAmount
    234        }
    235
    236        # 메소드 호출 후 데이터 초기화
    237        self.reset()
    238
    239        return result

Hip 관련 기능을 제공하는 클래스. MAXScript의 _Hip 구조체 개념을 Python으로 재구현한 클래스이며, 3ds Max의 기능들을 pymxs API를 통해 제어합니다.

Hip( nameService=None, animService=None, helperService=None, boneService=None, constraintService=None) View Source

    27    def __init__(self, nameService=None, animService=None, helperService=None, boneService=None, constraintService=None):
    28        """
    29        클래스 초기화.
    30
    31        Args:
    32            nameService: 이름 처리 서비스 (제공되지 않으면 새로 생성)
    33            animService: 애니메이션 서비스 (제공되지 않으면 새로 생성)
    34            helperService: 헬퍼 객체 관련 서비스 (제공되지 않으면 새로 생성)
    35            boneService: 뼈대 관련 서비스 (제공되지 않으면 새로 생성)
    36            constraintService: 제약 관련 서비스 (제공되지 않으면 새로 생성)
    37            bipService: Biped 관련 서비스 (제공되지 않으면 새로 생성)
    38        """
    39        # 서비스 인스턴스 설정 또는 생성
    40        self.name = nameService if nameService else Name()
    41        self.anim = animService if animService else Anim()
    42
    43        # 종속성이 있는 서비스들은 이미 생성된 서비스들을 전달
    44        self.helper = helperService if helperService else Helper(nameService=self.name)
    45        self.const = constraintService if constraintService else Constraint(nameService=self.name, helperService=self.helper)
    46        self.bone = boneService if boneService else Bone(nameService=self.name, animService=self.anim, helperService=self.helper, constraintService=self.const)
    47
    48        # 기본 속성 초기화
    49        self.pelvisWeight = 0.6
    50        self.thighWeight = 0.4
    51        self.pushAmount = 10
    52
    53        self.pelvis = None
    54        self.thigh = None
    55        self.thighTwist = None
    56        self.calf = None
    57
    58        self.pelvisHelper = None
    59        self.thighHelper = None
    60        self.thighTwistHelper = None
    61        self.thighRotHelper = None
    62        self.thighPosHelper = None
    63        self.thighRotRootHelper = None
    64
    65        self.helpers = []
    66        self.bones = []
    67
    68        self.posScriptExpression = (
    69            "localLimbTm = limb.transform * inverse limbParent.transform\n"
    70            "localDeltaTm = localLimbTm * inverse localRotRefTm\n"
    71            "\n"
    72            "q = localDeltaTm.rotation\n"
    73            "\n"
    74            "eulerRot = (quatToEuler q order:5)\n"
    75            "swizzledRot = (eulerAngles eulerRot.y eulerRot.z eulerRot.x)\n"
    76            "\n"
    77            "axis = [0,0,1]\n"
    78            "\n"
    79            "saturatedTwistZ = (swizzledRot.x*axis.x + swizzledRot.y*axis.y + swizzledRot.z*axis.z)/180.0\n"
    80            "pushScaleY = amax 0.0 saturatedTwistZ\n"
    81            "\n"
    82            "axis = [0,1,0]\n"
    83            "saturatedTwistY = (swizzledRot.x*axis.x + swizzledRot.y*axis.y + swizzledRot.z*axis.z)/180.0\n"
    84            "pushScaleZ = amax 0.0 saturatedTwistY\n"
    85            "\n"
    86            "\n"
    87            "[0, pushAmount * pushScaleY, -pushAmount * pushScaleZ]\n"
    88        )

클래스 초기화.

Args: nameService: 이름 처리 서비스 (제공되지 않으면 새로 생성) animService: 애니메이션 서비스 (제공되지 않으면 새로 생성) helperService: 헬퍼 객체 관련 서비스 (제공되지 않으면 새로 생성) boneService: 뼈대 관련 서비스 (제공되지 않으면 새로 생성) constraintService: 제약 관련 서비스 (제공되지 않으면 새로 생성) bipService: Biped 관련 서비스 (제공되지 않으면 새로 생성)

name
anim
helper
const
bone
pelvisWeight
thighWeight
pushAmount
pelvis
thigh
thighTwist
calf
pelvisHelper
thighHelper
thighTwistHelper
thighRotHelper
thighPosHelper
thighRotRootHelper
helpers
bones
posScriptExpression
def reset(self): View Source

     90    def reset(self):
     91        """
     92        클래스의 주요 컴포넌트들을 초기화합니다.
     93        서비스가 아닌 클래스 자체의 작업 데이터를 초기화하는 함수입니다.
     94
     95        Returns:
     96            self: 메소드 체이닝을 위한 자기 자신 반환
     97        """
     98        self.pelvisWeight = 0.6
     99        self.thighWeight = 0.4
    100        self.pushAmount = 10
    101
    102        self.pelvis = None
    103        self.thigh = None
    104        self.thighTwist = None
    105        self.calf = None
    106
    107        self.pelvisHelper = None
    108        self.thighHelper = None
    109        self.thighTwistHelper = None
    110        self.thighRotHelper = None
    111        self.thighPosHelper = None
    112        self.thighRotRootHelper = None
    113
    114        self.helpers = []
    115        self.bones = []
    116
    117        return self

클래스의 주요 컴포넌트들을 초기화합니다. 서비스가 아닌 클래스 자체의 작업 데이터를 초기화하는 함수입니다.

Returns: self: 메소드 체이닝을 위한 자기 자신 반환

def create_helper(self, inPelvis, inThigh, inThighTwist): View Source

    119    def create_helper(self, inPelvis, inThigh, inThighTwist):
    120        if not rt.isValidNode(inPelvis) or not rt.isValidNode(inThigh) or not rt.isValidNode(inThighTwist):
    121            return False
    122
    123        self.pelvis = inPelvis
    124        self.thigh = inThigh
    125        self.thighTwist = inThighTwist
    126
    127        filteringChar = self.name._get_filtering_char(inThigh.name)
    128        isLower = inThigh.name[0].islower()
    129
    130        pelvisHelperName = self.name.replace_name_part("RealName", inThigh.name, self.name.get_RealName(inPelvis.name)+filteringChar+"Hip")
    131        pelvisHelperName = self.name.replace_name_part("Type", pelvisHelperName, self.name.get_name_part_value_by_description("Type", "Dummy"))
    132        pelvisHelper = self.helper.create_point(pelvisHelperName)
    133        rt.setProperty(pelvisHelper, "transform", inThigh.transform)
    134        pelvisHelper.parent = inPelvis
    135
    136        tihgTwistHeleprName = self.name.replace_name_part("RealName", inThigh.name, self.name.get_RealName(inThighTwist.name)+filteringChar+"Hip")
    137        tihgTwistHeleprName = self.name.replace_name_part("Type", tihgTwistHeleprName, self.name.get_name_part_value_by_description("Type", "Dummy"))
    138        thighTwistHelper = self.helper.create_point(tihgTwistHeleprName)
    139        rt.setProperty(thighTwistHelper, "transform", inThighTwist.transform)
    140        thighTwistHelper.parent = inThighTwist
    141
    142        tihghRotHelperName = self.name.replace_name_part("RealName", inThigh.name, self.name.get_RealName(inThigh.name)+filteringChar+"Hip")
    143        tihghRotHelperName = self.name.replace_name_part("Type", tihghRotHelperName, self.name.get_name_part_value_by_description("Type", "Rotation"))
    144        thighRotHelper = self.helper.create_point(tihghRotHelperName)
    145        rt.setProperty(thighRotHelper, "transform", inThighTwist.transform)
    146        thighRotHelper.parent = inThigh
    147
    148        thighPosHelperName = self.name.replace_name_part("RealName", inThigh.name, self.name.get_RealName(inThigh.name)+filteringChar+"Hip")
    149        thighPosHelperName = self.name.replace_name_part("Type", thighPosHelperName, self.name.get_name_part_value_by_description("Type", "Position"))
    150        thighPosHelper = self.helper.create_point(thighPosHelperName)
    151        rt.setProperty(thighPosHelper, "transform", inThighTwist.transform)
    152        thighPosHelper.parent = thighRotHelper
    153
    154        thighRotRootHelperName = self.name.replace_name_part("RealName", inThigh.name, self.name.get_RealName(inThigh.name)+filteringChar+"Hip")
    155        thighRotRootHelperName = self.name.replace_name_part("Type", thighRotRootHelperName, self.name.get_name_part_value_by_description("Type", "Dummy"))
    156        thighRotRootHelper = self.helper.create_point(thighRotRootHelperName)
    157        rt.setProperty(thighRotRootHelper, "transform", thighRotHelper.transform)
    158        thighRotRootHelper.parent = inThighTwist
    159
    160        if isLower:
    161            pelvisHelper.name = pelvisHelper.name.lower()
    162            thighTwistHelper.name = thighTwistHelper.name.lower()
    163            thighRotHelper.name = thighRotHelper.name.lower()
    164            thighPosHelper.name = thighPosHelper.name.lower()
    165            thighRotRootHelper.name = thighRotRootHelper.name.lower()
    166
    167        self.pelvisHelper = pelvisHelper
    168        self.thighTwistHelper = thighTwistHelper
    169        self.thighRotHelper = thighRotHelper
    170        self.thighPosHelper = thighPosHelper
    171        self.thighRotRootHelper = thighRotRootHelper
    172
    173        self.helpers.append(pelvisHelper)
    174        self.helpers.append(thighTwistHelper)
    175        self.helpers.append(thighRotHelper)
    176        self.helpers.append(thighPosHelper)
    177        self.helpers.append(thighRotRootHelper)

def assing_constraint( self, inCalf, inPelvisWeight=0.6, inThighWeight=0.4, inPushAmount=5.0): View Source

    179    def assing_constraint(self, inCalf, inPelvisWeight=0.6, inThighWeight=0.4, inPushAmount=5.0):
    180        self.calf = inCalf
    181        self.pelvisWeight = inPelvisWeight
    182        self.thighWeight = inThighWeight
    183        self.pushAmount = rt.Float(inPushAmount)
    184
    185        facingDirVec = self.calf.transform.position - self.thigh.transform.position
    186        inObjXAxisVec = self.thigh.objectTransform.row1
    187        distanceDir = -1.0 if rt.dot(inObjXAxisVec, facingDirVec) > 0 else 1.0
    188
    189        rotConst = self.const.assign_rot_const_multi(self.thighRotHelper, [self.pelvisHelper, self.thighTwistHelper])
    190        rotConst.setWeight(1, self.pelvisWeight * 100.0)
    191        rotConst.setWeight(2, self.thighWeight * 100.0)
    192
    193        localRotRefTm = self.thighRotHelper.transform * rt.inverse(self.thighRotRootHelper.transform)
    194        posConst = self.const.assign_pos_script_controller(self.thighPosHelper)
    195        posConst.addNode("limb", self.thighRotHelper)
    196        posConst.addNode("limbParent", self.thighRotRootHelper)
    197        posConst.addConstant("localRotRefTm", localRotRefTm)
    198        posConst.addConstant("pushAmount", self.pushAmount*distanceDir)
    199        posConst.setExpression(self.posScriptExpression)
    200        posConst.update()

def create_bone( self, inPelvis, inThigh, inThighTwist, inCalf, pushAmount=5.0, inPelvisWeight=0.6, inThighWeight=0.4): View Source

    202    def create_bone(self, inPelvis, inThigh, inThighTwist, inCalf, pushAmount=5.0, inPelvisWeight=0.6, inThighWeight=0.4):
    203        if not rt.isValidNode(inPelvis) or not rt.isValidNode(inThigh) or not rt.isValidNode(inThighTwist):
    204            return False
    205
    206        self.create_helper(inPelvis, inThigh, inThighTwist)
    207        self.assing_constraint(inCalf, inPelvisWeight, inThighWeight, inPushAmount=pushAmount)
    208
    209        isLower = inThigh.name[0].islower()
    210        hipBoneName = self.name.replace_name_part("RealName", inThigh.name, "Hip")
    211        hipBone = self.bone.create_nub_bone(hipBoneName, 2)
    212        hipBone.name = self.name.remove_name_part("Nub", hipBone.name)
    213        if isLower:
    214            hipBone.name = hipBone.name.lower()
    215
    216        rt.setProperty(hipBone, "transform", inThighTwist.transform)
    217        hipBone.parent = inThigh
    218
    219        self.const.assign_rot_const(hipBone, self.thighRotHelper)
    220        self.const.assign_pos_const(hipBone, self.thighPosHelper)
    221
    222        self.bones.append(hipBone)
    223
    224        # 결과를 딕셔너리 형태로 준비
    225        result = {
    226            "Pelvis": inPelvis,
    227            "Thigh": inThigh,
    228            "ThighTwist": inThighTwist,
    229            "Bones": self.bones,
    230            "Helpers": self.helpers,
    231            "PelvisWeight": inPelvisWeight,
    232            "ThighWeight": inThighWeight,
    233            "PushAmount": pushAmount
    234        }
    235
    236        # 메소드 호출 후 데이터 초기화
    237        self.reset()
    238
    239        return result

class Container(PySide2.QtWidgets.QWidget): View Source

    109class Container(QtWidgets.QWidget):
    110    """Class for creating a collapsible group similar to how it is implement in Maya
    111
    112        Examples:
    113            Simple example of how to add a Container to a QVBoxLayout and attach a QGridLayout
    114
    115            >>> layout = QtWidgets.QVBoxLayout()
    116            >>> container = Container("Group")
    117            >>> layout.addWidget(container)
    118            >>> content_layout = QtWidgets.QGridLayout(container.contentWidget)
    119            >>> content_layout.addWidget(QtWidgets.QPushButton("Button"))
    120    """
    121    def __init__(self, name, color_background=True):
    122        """Container Class Constructor to initialize the object
    123
    124        Args:
    125            name (str): Name for the header
    126            color_background (bool): whether or not to color the background lighter like in maya
    127        """
    128        super(Container, self).__init__()
    129        layout = QtWidgets.QVBoxLayout(self)
    130        layout.setContentsMargins(0, 2, 0, 0)
    131        layout.setSpacing(0)
    132        self._content_widget = QtWidgets.QWidget()
    133        if color_background:
    134            self._content_widget.setStyleSheet('''
    135                .QWidget{
    136                    background-color: rgb(81, 81, 81);
    137                }
    138            ''')
    139        header = Header(name, self._content_widget)
    140        layout.addWidget(header)
    141        layout.addWidget(self._content_widget)
    142
    143        # assign header methods to instance attributes so they can be called outside of this class
    144        self.collapse = header.collapse
    145        self.expand = header.expand
    146        self.toggle = header.mousePressEvent
    147
    148    @property
    149    def contentWidget(self):
    150        """Getter for the content widget
    151
    152        Returns: Content widget
    153        """
    154        return self._content_widget

Class for creating a collapsible group similar to how it is implement in Maya

Examples: Simple example of how to add a Container to a QVBoxLayout and attach a QGridLayout

    >>> layout = QtWidgets.QVBoxLayout()
    >>> container = Container("Group")
    >>> layout.addWidget(container)
    >>> content_layout = QtWidgets.QGridLayout(container.contentWidget)
    >>> content_layout.addWidget(QtWidgets.QPushButton("Button"))

Container(name, color_background=True) View Source

    121    def __init__(self, name, color_background=True):
    122        """Container Class Constructor to initialize the object
    123
    124        Args:
    125            name (str): Name for the header
    126            color_background (bool): whether or not to color the background lighter like in maya
    127        """
    128        super(Container, self).__init__()
    129        layout = QtWidgets.QVBoxLayout(self)
    130        layout.setContentsMargins(0, 2, 0, 0)
    131        layout.setSpacing(0)
    132        self._content_widget = QtWidgets.QWidget()
    133        if color_background:
    134            self._content_widget.setStyleSheet('''
    135                .QWidget{
    136                    background-color: rgb(81, 81, 81);
    137                }
    138            ''')
    139        header = Header(name, self._content_widget)
    140        layout.addWidget(header)
    141        layout.addWidget(self._content_widget)
    142
    143        # assign header methods to instance attributes so they can be called outside of this class
    144        self.collapse = header.collapse
    145        self.expand = header.expand
    146        self.toggle = header.mousePressEvent

Container Class Constructor to initialize the object

Args: name (str): Name for the header color_background (bool): whether or not to color the background lighter like in maya

collapse
expand
toggle
contentWidget View Source

    148    @property
    149    def contentWidget(self):
    150        """Getter for the content widget
    151
    152        Returns: Content widget
    153        """
    154        return self._content_widget

Getter for the content widget

Returns: Content widget

staticMetaObject = <PySide2.QtCore.QMetaObject object>