pyjallib.max

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

 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 Name(pyjallib.naming.Naming):
 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)
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):
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):
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):
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):
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):
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):
 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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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: 타겟 이름 문자열

class Anim:
 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()
22    def __init__(self):
23        """클래스 초기화 (현재 특별한 초기화 동작은 없음)"""
24        pass

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

def rotate_local(self, inObj, rx, ry, rz, dontAffectChildren=False):
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):
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):
 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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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'):
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:
 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)
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)):
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):
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):
 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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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): 박스 표시 활성화 여부 inObjrt.ExposeTm 또는 rt.Point 타입의 객체인 경우 해당 객체의 속성값을 반영하며, 그렇지 않은 경우 미리 정의된 기본값을 반환합니다.

def set_shape(self, inObj, inShapeDict):
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: 형태가 설정된 객체를 반환합니다. 만약 inObjrt.ExposeTm 또는 rt.Point 타입이 아닐 경우, 아무 작업도 수행하지 않고 None을 반환합니다.

class Constraint:
  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)
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):
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):
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):
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):
 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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
 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:
  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)
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):
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):
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):
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):
 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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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 create_skin_bone( self, inBoneArray, skipNub=True, mesh=True, link=True, skinBoneBaseName=''):
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=''):
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=''):
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):
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 create_skin_bone_from_bip_for_ue5manny( self, inBoneArray, skipNub=True, mesh=False, link=True, isHuman=False, skinBoneBaseName=''):
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):
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):
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):
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):
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):
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):
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):
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):
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:
 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)
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):
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):
 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):
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):
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):
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):
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:
 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()
21    def __init__(self):
22        """
23        초기화 함수
24        """
25        pass

초기화 함수

def reset_layer(self):
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):
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):
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):
 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):
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):
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):
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):
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):
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):
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:
 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()
19    def __init__(self):
20        """클래스 초기화 (현재 특별한 초기화 동작은 없음)"""
21        pass

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

def align_to_last_sel_center(self):
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):
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):
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):
 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:
 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)
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):
35    def set_selectionSet_to_all(self):
36        """
37        모든 유형의 객체를 선택하도록 필터 설정
38        """
39        rt.SetSelectFilter(1)

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

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

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

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

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

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

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

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

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

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

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

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

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

def filter_bip(self):
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):
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):
 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):
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):
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):
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):
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):
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):
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):
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):
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):
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 Bip:
 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)
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):
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):
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):
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):
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):
 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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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 convert_name_for_ue5(self, inBipRoot, inBipNameConfigFile):
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:
 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()
31    def __init__(self):
32        """
33        클래스 초기화
34        """
35        self.skin_match_list = []

클래스 초기화

skin_match_list
def has_skin(self, obj=None):
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):
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):
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):
 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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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:
 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()
31    def __init__(self):
32        """클래스 초기화"""
33        self.channelMaxViewNum = 100

클래스 초기화

channelMaxViewNum
def get_modifier_index(self, inObj):
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):
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):
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):
 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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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):
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:
 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)
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):
 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):
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):
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:
 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)
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):
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):
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):
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):
 98    def get_count(self):
 99        """
100        체인의 트위스트 뼈대 개수 가져오기
101        
102        Returns:
103            뼈대 개수
104        """
105        return self.twistNum

체인의 트위스트 뼈대 개수 가져오기

Returns: 뼈대 개수

def is_empty(self):
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):
116    def clear(self):
117        """체인의 모든 뼈대 제거"""
118        self.bones = []

체인의 모든 뼈대 제거

def delete_all(self):
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):
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):
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:
 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)
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):
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):
 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:
 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)
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):
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):
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):
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):
 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):
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):
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):
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:
 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)
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):
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):
 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:
 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)
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):
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):
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):
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):
 99    def clear(self):
100        """체인의 모든 뼈대와 헬퍼 참조 제거"""
101        self.bones = []
102        self.helpers = []
103        self.clavicle = None
104        self.upperArm = None

체인의 모든 뼈대와 헬퍼 참조 제거

def delete_all(self):
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):
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):
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:
 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)
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):
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):
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):
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]):
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:
 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)
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):
 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):
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):
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):
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):
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):
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):
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):
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):
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):
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:
 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)
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):
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):
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):
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):
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):
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):
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):
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):
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):
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:
 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)
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):
 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):
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):
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):
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):
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)
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
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>