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]
37class Header: 38 """ 39 JalLib.max 패키지의 헤더 모듈 40 3DS Max에서 사용하는 다양한 기능을 제공하는 클래스들을 초기화하고 관리합니다. 41 """ 42 _instance = None 43 44 @classmethod 45 def get_instance(cls): 46 """싱글톤 패턴을 구현한 인스턴스 접근 메소드""" 47 if cls._instance is None: 48 cls._instance = Header() 49 return cls._instance 50 51 def __init__(self): 52 """ 53 Header 클래스 초기화 54 """ 55 self.configDir = os.path.join(os.path.dirname(__file__), "ConfigFiles") 56 self.nameConfigDir = os.path.join(self.configDir, "3DSMaxNamingConfig.json") 57 58 self.name = Name(configPath=self.nameConfigDir) 59 self.anim = Anim() 60 61 self.helper = Helper(nameService=self.name) 62 self.constraint = Constraint(nameService=self.name, helperService=self.helper) 63 self.bone = Bone(nameService=self.name, animService=self.anim, helperService=self.helper, constraintService=self.constraint) 64 65 self.mirror = Mirror(nameService=self.name, boneService=self.bone) 66 self.layer = Layer() 67 self.align = Align() 68 self.sel = Select(nameService=self.name, boneService=self.bone) 69 self.link = Link() 70 71 self.bip = Bip(animService=self.anim, nameService=self.name, boneService=self.bone) 72 self.skin = Skin() 73 74 self.twistBone = TwistBone(nameService=self.name, animService=self.anim, constraintService=self.constraint, bipService=self.bip, boneService=self.bone) 75 self.groinBone = GroinBone(nameService=self.name, animService=self.anim, constraintService=self.constraint, boneService=self.bone, helperService=self.helper) 76 self.autoClavicle = AutoClavicle(nameService=self.name, animService=self.anim, helperService=self.helper, boneService=self.bone, constraintService=self.constraint, bipService=self.bip) 77 self.volumeBone = VolumeBone(nameService=self.name, animService=self.anim, constraintService=self.constraint, boneService=self.bone, helperService=self.helper) 78 self.kneeBone = KneeBone(nameService=self.name, animService=self.anim, constraintService=self.constraint, boneService=self.bone, helperService=self.helper, volumeBoneService=self.volumeBone) 79 self.hip = Hip(nameService=self.name, animService=self.anim, helperService=self.helper, boneService=self.bone, constraintService=self.constraint) 80 81 self.morph = Morph() 82 83 self.tools = [] 84 85 def update_nameConifg(self, configPath): 86 """ 87 이름 설정을 업데이트합니다. 88 89 Args: 90 configPath: ConfigPath 인스턴스 91 """ 92 self.name.load_from_config_file(configPath) 93 94 def add_tool(self, tool): 95 """ 96 도구를 추가합니다. 97 98 Args: 99 tool: 추가할 도구 100 """ 101 if tool in self.tools: 102 self.tools.remove(tool) 103 104 self.tools.append(tool)
JalLib.max 패키지의 헤더 모듈 3DS Max에서 사용하는 다양한 기능을 제공하는 클래스들을 초기화하고 관리합니다.
51 def __init__(self): 52 """ 53 Header 클래스 초기화 54 """ 55 self.configDir = os.path.join(os.path.dirname(__file__), "ConfigFiles") 56 self.nameConfigDir = os.path.join(self.configDir, "3DSMaxNamingConfig.json") 57 58 self.name = Name(configPath=self.nameConfigDir) 59 self.anim = Anim() 60 61 self.helper = Helper(nameService=self.name) 62 self.constraint = Constraint(nameService=self.name, helperService=self.helper) 63 self.bone = Bone(nameService=self.name, animService=self.anim, helperService=self.helper, constraintService=self.constraint) 64 65 self.mirror = Mirror(nameService=self.name, boneService=self.bone) 66 self.layer = Layer() 67 self.align = Align() 68 self.sel = Select(nameService=self.name, boneService=self.bone) 69 self.link = Link() 70 71 self.bip = Bip(animService=self.anim, nameService=self.name, boneService=self.bone) 72 self.skin = Skin() 73 74 self.twistBone = TwistBone(nameService=self.name, animService=self.anim, constraintService=self.constraint, bipService=self.bip, boneService=self.bone) 75 self.groinBone = GroinBone(nameService=self.name, animService=self.anim, constraintService=self.constraint, boneService=self.bone, helperService=self.helper) 76 self.autoClavicle = AutoClavicle(nameService=self.name, animService=self.anim, helperService=self.helper, boneService=self.bone, constraintService=self.constraint, bipService=self.bip) 77 self.volumeBone = VolumeBone(nameService=self.name, animService=self.anim, constraintService=self.constraint, boneService=self.bone, helperService=self.helper) 78 self.kneeBone = KneeBone(nameService=self.name, animService=self.anim, constraintService=self.constraint, boneService=self.bone, helperService=self.helper, volumeBoneService=self.volumeBone) 79 self.hip = Hip(nameService=self.name, animService=self.anim, helperService=self.helper, boneService=self.bone, constraintService=self.constraint) 80 81 self.morph = Morph() 82 83 self.tools = []
Header 클래스 초기화
44 @classmethod 45 def get_instance(cls): 46 """싱글톤 패턴을 구현한 인스턴스 접근 메소드""" 47 if cls._instance is None: 48 cls._instance = Header() 49 return cls._instance
싱글톤 패턴을 구현한 인스턴스 접근 메소드
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 특화 기능 제공
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) 설정 파일이 제공되면 해당 파일에서 설정을 로드함
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 부분의 사전 정의 값 목록
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 부분의 사전 정의 값 목록
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 부분의 사전 정의 값 목록
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 부분의 사전 정의 값 목록
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 부분의 사전 정의 값 목록
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
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
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
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
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
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
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
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
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
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
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: 변경된 문자열
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: 변경된 문자열
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: 변경된 문자열
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: 변경된 문자열
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: 변경된 문자열
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: 변경된 문자열
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: 변경된 문자열
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 부분이 제거된 문자열
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 부분이 제거된 문자열
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 부분이 제거된 문자열
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 부분이 제거된 문자열
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 부분이 제거된 문자열
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 부분이 제거된 문자열
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: 고유한 이름 문자열
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: 양수)
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: 이름 기준으로 정렬된 객체 배열
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: 미러링된 이름 문자열
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: 부모 이름 문자열
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: 더미 이름 문자열
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 이름 문자열
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 이름 문자열
491 def get_target_value(self): 492 """ 493 타겟 이름 문자열 반환 494 495 Returns: 496 타겟 이름 문자열 497 """ 498 return self.get_name_part_value_by_description("Type", "Target")
타겟 이름 문자열 반환
Returns: 타겟 이름 문자열
Inherited Members
- pyjallib.naming.Naming
- get_padding_num
- get_name_part
- get_name_part_index
- get_name_part_predefined_values
- is_in_name_part_predefined_values
- get_name_part_value_by_description
- pick_name
- get_name
- combine
- get_RealName
- get_non_RealName
- convert_name_to_array
- convert_to_dictionary
- convert_to_description
- convert_to_korean_description
- has_name_part
- add_prefix_to_name_part
- add_suffix_to_name_part
- add_prefix_to_real_name
- add_suffix_to_real_name
- convert_digit_into_padding_string
- set_index_padding_num
- get_index_padding_num
- increase_index
- get_index_as_digit
- sort_by_index
- get_string
- replace_filtering_char
- replace_name_part
- remove_name_part
- load_from_config_file
- load_default_config
- get_config_path
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를 통해 제어합니다.
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축 회전 각도 (도 단위)
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축 이동 거리
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 : 초기화할 객체
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 : 변환을 고정할 객체
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 : 끝 프레임 (기본값: 애니메이션 범위의 끝)
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 : 끝 프레임 (기본값: 애니메이션 범위의 끝)
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 : 평균 위치 계산 대상 객체 배열
반환: 계산된 평균 위치를 적용한 변환 행렬
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 : 평균 회전 계산 대상 객체 배열
반환: 계산된 평균 회전을 적용한 변환 행렬
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 : 수집된 키프레임들을 저장할 리스트 (참조로 전달)
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 : 키프레임을 검색할 객체
반환: 객체에 적용된 키프레임들의 리스트
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 : 키프레임을 검색할 객체
반환: [시작 키프레임, 끝 키프레임] (키가 없으면 빈 리스트 반환)
472 def delete_all_keys(self, inObj): 473 """ 474 객체에 적용된 모든 키프레임을 삭제함. 475 476 매개변수: 477 inObj : 삭제 대상 객체 478 """ 479 rt.deleteKeys(inObj, rt.Name('allKeys'))
객체에 적용된 모든 키프레임을 삭제함.
매개변수: inObj : 삭제 대상 객체
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 : 애니메이션이 없는 경우
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이면 전체 객체)
반환: 애니메이션이 적용된 객체들의 리스트
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이면 전체 객체)
반환: 애니메이션이 적용된 재질을 가진 객체들의 리스트
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이면 전체 객체)
반환: 애니메이션이 적용된 변환 데이터를 가진 객체들의 리스트
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 : 변환 값을 저장할 객체
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" (적용할 변환 공간)
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의 기능을 직접 접근합니다.
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 서비스 인스턴스 (제공되지 않으면 새로 생성)
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: 생성된 포인트 헬퍼
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: 생성된 빈 포인트 헬퍼
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 값
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: 생성된 헬퍼 이름 배열 [포인트 이름, 타겟 이름]
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: [헬퍼 크기, 십자 표시 여부, 박스 표시 여부]
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: 생성된 헬퍼 배열
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
부모 헬퍼 생성
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 헬퍼 배열
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: 설정된 객체
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: 설정된 객체
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: 대상 객체
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: 대상 객체
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: 대상 객체
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: 대상 객체
442 def get_shape(self, inObj): 443 """ 444 헬퍼 객체의 시각적 형태 속성을 가져옵니다. 445 inObj (object): 형태 정보를 가져올 대상 3ds Max 헬퍼 객체. 446 dict: 헬퍼의 형태 속성을 나타내는 딕셔너리. 447 - "size" (float): 크기 448 - "centermarker" (bool): 센터 마커 활성화 여부 449 - "axistripod" (bool): 축 삼각대 활성화 여부 450 - "cross" (bool): 십자 표시 활성화 여부 451 - "box" (bool): 박스 표시 활성화 여부 452 `inObj`가 `rt.ExposeTm` 또는 `rt.Point` 타입의 객체인 경우 해당 객체의 453 속성값을 반영하며, 그렇지 않은 경우 미리 정의된 기본값을 반환합니다. 454 """ 455 returnDict = { 456 "size": 2.0, 457 "centermarker": False, 458 "axistripod": False, 459 "cross": True, 460 "box": False 461 } 462 if rt.classOf(inObj) == rt.ExposeTm or rt.classOf(inObj) == rt.Point: 463 returnDict["size"] = inObj.size 464 returnDict["centermarker"] = inObj.centermarker 465 returnDict["axistripod"] = inObj.axistripod 466 returnDict["cross"] = inObj.cross 467 returnDict["box"] = inObj.box 468 469 return returnDict
헬퍼 객체의 시각적 형태 속성을 가져옵니다.
inObj (object): 형태 정보를 가져올 대상 3ds Max 헬퍼 객체.
dict: 헬퍼의 형태 속성을 나타내는 딕셔너리.
- "size" (float): 크기
- "centermarker" (bool): 센터 마커 활성화 여부
- "axistripod" (bool): 축 삼각대 활성화 여부
- "cross" (bool): 십자 표시 활성화 여부
- "box" (bool): 박스 표시 활성화 여부
inObj
가 rt.ExposeTm
또는 rt.Point
타입의 객체인 경우 해당 객체의
속성값을 반영하며, 그렇지 않은 경우 미리 정의된 기본값을 반환합니다.
471 def set_shape(self, inObj, inShapeDict): 472 """ 473 헬퍼 객체의 표시 형태를 설정합니다. 474 `rt.ExposeTm` 또는 `rt.Point` 타입의 헬퍼 객체에 대해 크기, 센터 마커, 축 삼각대, 십자, 박스 표시 여부를 설정합니다. 475 inObj (rt.ExposeTm | rt.Point): 설정을 적용할 헬퍼 객체입니다. 476 inShapeDict (dict): 헬퍼의 형태를 정의하는 딕셔너리입니다. 477 다음 키와 값을 포함해야 합니다: 478 - "size" (float | int): 헬퍼의 크기. 479 - "centermarker" (bool): 센터 마커 표시 여부 (True/False). 480 - "axistripod" (bool): 축 삼각대(axis tripod) 표시 여부 (True/False). 481 - "cross" (bool): 십자(cross) 표시 여부 (True/False). 482 - "box" (bool): 박스(box) 표시 여부 (True/False). 483 rt.ExposeTm | rt.Point | None: 형태가 설정된 객체를 반환합니다. 484 만약 `inObj`가 `rt.ExposeTm` 또는 `rt.Point` 타입이 아닐 경우, 485 아무 작업도 수행하지 않고 `None`을 반환합니다. 486 """ 487 if rt.classOf(inObj) == rt.ExposeTm or rt.classOf(inObj) == rt.Point: 488 inObj.size = inShapeDict["size"] 489 inObj.centermarker = inShapeDict["centermarker"] 490 inObj.axistripod = inShapeDict["axistripod"] 491 inObj.cross = inShapeDict["cross"] 492 inObj.box = inShapeDict["box"] 493 494 return inObj
헬퍼 객체의 표시 형태를 설정합니다.
rt.ExposeTm
또는 rt.Point
타입의 헬퍼 객체에 대해 크기, 센터 마커, 축 삼각대, 십자, 박스 표시 여부를 설정합니다.
inObj (rt.ExposeTm | rt.Point): 설정을 적용할 헬퍼 객체입니다.
inShapeDict (dict): 헬퍼의 형태를 정의하는 딕셔너리입니다.
다음 키와 값을 포함해야 합니다:
- "size" (float | int): 헬퍼의 크기.
- "centermarker" (bool): 센터 마커 표시 여부 (True/False).
- "axistripod" (bool): 축 삼각대(axis tripod) 표시 여부 (True/False).
- "cross" (bool): 십자(cross) 표시 여부 (True/False).
- "box" (bool): 박스(box) 표시 여부 (True/False).
rt.ExposeTm | rt.Point | None: 형태가 설정된 객체를 반환합니다.
만약 inObj
가 rt.ExposeTm
또는 rt.Point
타입이 아닐 경우,
아무 작업도 수행하지 않고 None
을 반환합니다.
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를 통해 제어합니다.
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: 헬퍼 객체 관련 서비스 (제공되지 않으면 새로 생성)
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
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
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)
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: 위치 리스트 컨트롤러
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)
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: 위치 제약 컨트롤러
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
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
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
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
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)
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: 회전 리스트 컨트롤러
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)
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: 회전 제약 컨트롤러
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
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
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
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)
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 제약 컨트롤러
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
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
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: 생성된 회전 스크립트 컨트롤러
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
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 (실패 시)
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: 컨트롤러 이름 배열
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: 컨트롤러 가중치 배열
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
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
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: 컨트롤러 이름 배열
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: 컨트롤러 가중치 배열
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
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
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를 통해 제어합니다.
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: 제약 서비스 (제공되지 않으면 새로 생성)
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 체인을 제거할 뼈대 객체
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
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: 어셈블리에 추가할 자식 뼈대
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: 계층 구조에 따라 정렬된 뼈대 배열
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
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
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: 그 외의 경우
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: 그 외의 경우
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 뼈대
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 뼈대
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 뼈대
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 (실패 시)
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: 생성된 뼈대 배열
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: 생성된 스트레치 뼈대 배열
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: 생성된 스트레치 뼈대 배열
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: 뼈대 형태 속성 배열
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: 실패
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)
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: 핀을 비활성화할 뼈대 객체
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: 설정할 크기
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: 설정할 테이퍼 값
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: 삭제할 뼈대 배열
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: 자식이 없는 경우
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: 자식 객체 배열
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: 선택된 자식 객체 배열
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: 뼈대 끝 위치 좌표
674 def link_skin_bone(self, inSkinBone, inOriBone): 675 """ 676 스킨 뼈대를 원본 뼈대에 연결. 677 678 Args: 679 inSkinBone: 연결할 스킨 뼈대 680 inOriBone: 원본 뼈대 681 """ 682 self.anim.save_xform(inSkinBone) 683 self.anim.set_xform(inSkinBone, space="World") 684 685 self.anim.save_xform(inOriBone) 686 687 rt.setPropertyController(inSkinBone.controller, "Scale", rt.scaleXYZ()) 688 689 linkConst = rt.link_constraint() 690 linkConst.addTarget(inOriBone, 0) 691 692 inSkinBone.controller = linkConst 693 694 self.anim.set_xform(inSkinBone, space="World")
스킨 뼈대를 원본 뼈대에 연결.
Args: inSkinBone: 연결할 스킨 뼈대 inOriBone: 원본 뼈대
696 def link_skin_bones(self, inSkinBoneArray, inOriBoneArray): 697 """ 698 스킨 뼈대 배열을 원본 뼈대 배열에 연결. 699 700 Args: 701 inSkinBoneArray: 연결할 스킨 뼈대 배열 702 inOriBoneArray: 원본 뼈대 배열 703 704 Returns: 705 True: 성공 706 False: 실패 707 """ 708 if len(inSkinBoneArray) != len(inOriBoneArray): 709 print("Error: Skin bone array and original bone array must have the same length.") 710 return False 711 712 skinBoneDict = {} 713 oriBoneDict = {} 714 715 # 스킨 뼈대 딕셔너리 생성 (이름과 패턴화된 이름을 함께 저장) 716 for item in inSkinBoneArray: 717 # 아이템 저장 718 skinBoneDict[item.name] = item 719 # 언더스코어를 별표로 변환한 패턴 생성 720 namePattern = self.name.remove_name_part("Base", item.name) 721 namePattern = namePattern.replace("_", "*") 722 skinBoneDict[item.name + "_Pattern"] = namePattern 723 724 # 원본 뼈대 딕셔너리 생성 (이름과 패턴화된 이름을 함께 저장) 725 for item in inOriBoneArray: 726 # 아이템 저장 727 oriBoneDict[item.name] = item 728 # 공백을 별표로 변환한 패턴 생성 729 namePattern = self.name.remove_name_part("Base", item.name) 730 namePattern = namePattern.replace(" ", "*") 731 oriBoneDict[item.name + "_Pattern"] = namePattern 732 733 # 정렬된 배열 생성 734 sortedSkinBoneArray = [] 735 sortedOriBoneArray = [] 736 737 # 같은 패턴을 가진 뼈대들을 찾아 매칭 738 for skinName, skinBone in [(k, v) for k, v in skinBoneDict.items() if not k.endswith("_Pattern")]: 739 skinPattern = skinBoneDict[skinName + "_Pattern"] 740 741 for oriName, oriBone in [(k, v) for k, v in oriBoneDict.items() if not k.endswith("_Pattern")]: 742 oriPattern = oriBoneDict[oriName + "_Pattern"] 743 744 if rt.matchPattern(skinName, pattern=oriPattern): 745 sortedSkinBoneArray.append(skinBone) 746 sortedOriBoneArray.append(oriBone) 747 break 748 # 링크 연결 수행 749 for i in range(len(sortedSkinBoneArray)): 750 self.link_skin_bone(sortedSkinBoneArray[i], sortedOriBoneArray[i]) 751 752 return True
스킨 뼈대 배열을 원본 뼈대 배열에 연결.
Args: inSkinBoneArray: 연결할 스킨 뼈대 배열 inOriBoneArray: 원본 뼈대 배열
Returns: True: 성공 False: 실패
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: 생성된 스킨 뼈대 배열
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: 생성된 스킨 뼈대 배열
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 (실패 시)
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
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
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
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
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: 활성화할 뼈대 객체
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: 비활성화할 뼈대 객체
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)
선택된 모든 뼈대 활성화.
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)
선택된 모든 뼈대 비활성화.
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: 길이를 고정할 뼈대 객체
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: 길이 고정을 해제할 뼈대 객체
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를 통해 제어합니다.
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 서비스 인스턴스 (제공되지 않으면 새로 생성)
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: 미러링된 변환 행렬
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: 미러링된 객체 (복제본 또는 원본)
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: 미러링된 객체 배열
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: 미러링된 객체 배열
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: 미러링된 뼈대 배열
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: 미러링된 객체 배열
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의 레이어 관리 기능을 제어합니다.
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
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: 레이어에 포함된 노드 배열 또는 빈 배열
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 (없는 경우)
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: 레이어에 포함된 노드 배열
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
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: 생성된 레이어
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: 성공 여부
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: 성공 여부
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
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: 유효 여부
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를 통해 제어합니다.
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)으로 설정됩니다.
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
선택된 객체들을 마지막 선택된 객체의 트랜스폼으로 정렬.
모든 객체의 트랜스폼은 마지막 선택된 객체의 트랜스폼을 가지게 됩니다.
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)
선택된 객체들을 마지막 선택된 객체의 위치로 정렬 (회전은 유지).
위치는 마지막 선택된 객체를 따르고, 회전은 원래 객체의 회전을 유지합니다.
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)
선택된 객체들을 마지막 선택된 객체의 회전으로 정렬 (위치는 유지).
회전은 마지막 선택된 객체를 따르고, 위치는 원래 객체의 위치를 유지합니다.
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를 통해 제어합니다.
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 서비스 인스턴스 (제공되지 않으면 새로 생성)
35 def set_selectionSet_to_all(self): 36 """ 37 모든 유형의 객체를 선택하도록 필터 설정 38 """ 39 rt.SetSelectFilter(1)
모든 유형의 객체를 선택하도록 필터 설정
53 def set_selectionSet_to_helper(self): 54 """ 55 헬퍼 객체만 선택하도록 필터 설정 56 """ 57 rt.SetSelectFilter(6)
헬퍼 객체만 선택하도록 필터 설정
59 def set_selectionSet_to_point(self): 60 """ 61 포인트 객체만 선택하도록 필터 설정 62 """ 63 rt.SetSelectFilter(10)
포인트 객체만 선택하도록 필터 설정
65 def set_selectionSet_to_spline(self): 66 """ 67 스플라인 객체만 선택하도록 필터 설정 68 """ 69 rt.SetSelectFilter(3)
스플라인 객체만 선택하도록 필터 설정
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 객체만 필터링하여 선택
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)
현재 선택 항목에서 뼈대 객체만 필터링하여 선택
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)만 필터링하여 선택
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 객체만 필터링하여 선택
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)
현재 선택 항목에서 스플라인 객체만 필터링하여 선택
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: 선택된 자식 객체 리스트
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: [계층이 없는 객체 배열, 계층이 있는 객체 배열]
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: 독립적인 객체 배열
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: 계층 구조를 가진 객체 배열
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: 계층 순서대로 정렬된 객체 배열
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: 인덱스 순서대로 정렬된 객체 배열
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: 정렬된 객체 배열
12class Link: 13 """ 14 객체 연결(링크) 관련 기능을 위한 클래스 15 MAXScript의 _Link 구조체를 Python 클래스로 변환 16 17 pymxs 모듈을 통해 3ds Max의 객체 간 부모-자식 관계를 관리합니다. 18 """ 19 20 def __init__(self): 21 """ 22 초기화 함수 23 """ 24 pass 25 26 def link_to_last_sel(self): 27 """ 28 선택된 객체들을 마지막 선택 객체에 링크(부모로 지정) 29 30 Returns: 31 None 32 """ 33 # 선택된 객체가 2개 이상인 경우에만 처리 34 if rt.selection.count > 1: 35 # 첫 번째부터 마지막 직전까지의 모든 객체를 마지막 객체에 링크 36 for i in range(rt.selection.count - 1): 37 rt.selection[i].parent = rt.selection[rt.selection.count - 1] 38 39 def link_to_first_sel(self): 40 """ 41 선택된 객체들을 첫 번째 선택 객체에 링크(부모로 지정) 42 43 Returns: 44 None 45 """ 46 # 선택된 객체가 2개 이상인 경우에만 처리 47 if rt.selection.count > 1: 48 # 두 번째부터 마지막까지의 모든 객체를 첫 번째 객체에 링크 49 for i in range(1, rt.selection.count): 50 rt.selection[i].parent = rt.selection[0] 51 52 def unlink_selection(self): 53 """ 54 선택된 모든 객체의 부모 관계 해제 55 56 Returns: 57 None 58 """ 59 # 선택된 객체가 있는 경우에만 처리 60 if rt.selection.count > 0: 61 # 모든 선택 객체의 부모 관계 해제 62 for item in rt.selection: 63 item.parent = None 64 65 def unlink_children(self): 66 """ 67 선택된 객체의 모든 자식 객체의 부모 관계 해제 68 69 Returns: 70 None 71 """ 72 # 정확히 하나의 객체가 선택된 경우에만 처리 73 if rt.selection.count == 1: 74 # 선택된 객체의 모든 자식 객체의 부모 관계 해제 75 selObjs = rt.getCurrentSelection() 76 childrenObjs = selObjs[0].children 77 targetChildren = [child for child in childrenObjs] 78 for child in targetChildren: 79 child.parent = None
객체 연결(링크) 관련 기능을 위한 클래스 MAXScript의 _Link 구조체를 Python 클래스로 변환
pymxs 모듈을 통해 3ds Max의 객체 간 부모-자식 관계를 관리합니다.
26 def link_to_last_sel(self): 27 """ 28 선택된 객체들을 마지막 선택 객체에 링크(부모로 지정) 29 30 Returns: 31 None 32 """ 33 # 선택된 객체가 2개 이상인 경우에만 처리 34 if rt.selection.count > 1: 35 # 첫 번째부터 마지막 직전까지의 모든 객체를 마지막 객체에 링크 36 for i in range(rt.selection.count - 1): 37 rt.selection[i].parent = rt.selection[rt.selection.count - 1]
선택된 객체들을 마지막 선택 객체에 링크(부모로 지정)
Returns: None
39 def link_to_first_sel(self): 40 """ 41 선택된 객체들을 첫 번째 선택 객체에 링크(부모로 지정) 42 43 Returns: 44 None 45 """ 46 # 선택된 객체가 2개 이상인 경우에만 처리 47 if rt.selection.count > 1: 48 # 두 번째부터 마지막까지의 모든 객체를 첫 번째 객체에 링크 49 for i in range(1, rt.selection.count): 50 rt.selection[i].parent = rt.selection[0]
선택된 객체들을 첫 번째 선택 객체에 링크(부모로 지정)
Returns: None
52 def unlink_selection(self): 53 """ 54 선택된 모든 객체의 부모 관계 해제 55 56 Returns: 57 None 58 """ 59 # 선택된 객체가 있는 경우에만 처리 60 if rt.selection.count > 0: 61 # 모든 선택 객체의 부모 관계 해제 62 for item in rt.selection: 63 item.parent = None
선택된 모든 객체의 부모 관계 해제
Returns: None
65 def unlink_children(self): 66 """ 67 선택된 객체의 모든 자식 객체의 부모 관계 해제 68 69 Returns: 70 None 71 """ 72 # 정확히 하나의 객체가 선택된 경우에만 처리 73 if rt.selection.count == 1: 74 # 선택된 객체의 모든 자식 객체의 부모 관계 해제 75 selObjs = rt.getCurrentSelection() 76 childrenObjs = selObjs[0].children 77 targetChildren = [child for child in childrenObjs] 78 for child in targetChildren: 79 child.parent = None
선택된 객체의 모든 자식 객체의 부모 관계 해제
Returns: None
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를 통해 제어합니다.
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 서비스 인스턴스 (제공되지 않으면 새로 생성)
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 리스트
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 이름 리스트
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 객체 리스트
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
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
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 관련 모든 객체 리스트
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의 노드 객체 리스트
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 객체 리스트
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 노드 리스트
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 노드 리스트
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
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
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 노드 리스트
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 파일 경로
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 파일 경로
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 파일 경로
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 객체
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 객체
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: 삭제할 컬렉션 이름
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 객체
443 def link_base_skeleton(self, skinBoneBaseName="b"): 444 """ 445 기본 스켈레톤 링크 (Biped와 일반 뼈대 연결) 446 """ 447 rt.setWaitCursor() 448 449 bipSkel = self.get_bips() 450 baseSkel = [None] * len(bipSkel) 451 452 for i in range(len(bipSkel)): 453 baseSkeletonName = self.name.replace_base(bipSkel[i].name, skinBoneBaseName) 454 baseSkeletonName = self.name.replace_filteringChar(baseSkeletonName, "_") 455 baseSkelObj = rt.getNodeByName(baseSkeletonName) 456 if rt.isValidObj(baseSkelObj): 457 baseSkel[i] = baseSkelObj 458 459 self.anim.save_xform(bipSkel[i]) 460 self.anim.set_xform(bipSkel[i]) 461 462 self.anim.save_xform(baseSkel[i]) 463 self.anim.set_xform(baseSkel[i]) 464 465 for i in range(len(baseSkel)): 466 if baseSkel[i] is not None: 467 baseSkel[i].scale.controller = rt.scaleXYZ() 468 baseSkel[i].controller = rt.link_constraint() 469 470 self.anim.set_xform([baseSkel[i]], space="World") 471 baseSkel[i].transform.controller.AddTarget(bipSkel[i], 0) 472 473 for i in range(len(baseSkel)): 474 if baseSkel[i] is not None: 475 baseSkel[i].boneEnable = True 476 477 rt.setArrowCursor()
기본 스켈레톤 링크 (Biped와 일반 뼈대 연결)
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()
기본 스켈레톤 링크 해제
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 객체
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를 통해 제어합니다.
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: 없는 경우
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: 아닌 경우
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: 스킨 모디파이어 배열
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: 실패한 경우
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)
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)
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: 누락된 본 배열
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: 저장된 파일 경로
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 배열
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
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: 본 배열
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: 스킨 모디파이어 인덱스 배열
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: 선택된 버텍스 배열
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: 선택된 버텍스 배열
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 배열, 가중치 배열]
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: 버텍스 리스트
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
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를 통해 제어합니다.
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)
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)
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: 모프 채널 수
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 객체의 리스트
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)
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: 타겟 객체 배열
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: 채널 이름 리스트
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: 채널 이름 (없으면 빈 문자열)
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)
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)
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)
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)
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)
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)
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)
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: 리셋할 객체
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: 추출된 객체 배열
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) 두 가지 타입으로 생성이 가능하며, 각각 다른 회전 표현식을 사용하여 자연스러운 회전 움직임을 구현합니다.
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이며, 제공되지 않으면 새로 생성됩니다.
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: 메소드 체이닝을 위한 자기 자신 반환
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": 생성된 트위스트 뼈대 개수
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": 생성된 트위스트 뼈대 개수
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
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)
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 (인덱스가 범위를 벗어난 경우)
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 (체인이 비어있는 경우)
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 (체인이 비어있는 경우)
98 def get_count(self): 99 """ 100 체인의 트위스트 뼈대 개수 가져오기 101 102 Returns: 103 뼈대 개수 104 """ 105 return self.twistNum
체인의 트위스트 뼈대 개수 가져오기
Returns: 뼈대 개수
107 def is_empty(self): 108 """ 109 체인이 비어있는지 확인 110 111 Returns: 112 체인이 비어있으면 True, 아니면 False 113 """ 114 return len(self.bones) == 0
체인이 비어있는지 확인
Returns: 체인이 비어있으면 True, 아니면 False
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)
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
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 인스턴스
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에서 고간 부 본을 생성하고 관리하는 기능을 제공합니다.
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: 헬퍼 객체 서비스 (제공되지 않으면 새로 생성)
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: 메소드 체이닝을 위한 자기 자신 반환
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)
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
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)
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
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 # 기본 허벅지 가중치
체인의 모든 본과 헬퍼 참조 제거
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)
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)
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)
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) 형태의 튜플
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 인스턴스
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를 통해 제어합니다.
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 서비스 (제공되지 않으면 새로 생성)
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: 메소드 체이닝을 위한 자기 자신 반환
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 클래스에 전달할 수 있는 딕셔너리
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
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 클래스의 생성 결과 (딕셔너리)
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: 모든 뼈대 객체의 배열
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: 모든 헬퍼 객체의 배열
90 def is_empty(self): 91 """ 92 체인이 비어있는지 확인 93 94 Returns: 95 체인이 비어있으면 True, 아니면 False 96 """ 97 return len(self.bones) == 0
체인이 비어있는지 확인
Returns: 체인이 비어있으면 True, 아니면 False
99 def clear(self): 100 """체인의 모든 뼈대와 헬퍼 참조 제거""" 101 self.bones = [] 102 self.helpers = [] 103 self.clavicle = None 104 self.upperArm = None
체인의 모든 뼈대와 헬퍼 참조 제거
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)
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)
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 인스턴스
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에서 관절의 부피를 유지하기 위해 추가되는 중간본들을 위한 클래스입니다. 이 클래스는 관절이 회전할 때 자동으로 부피감을 유지하도록 하는 보조 본 시스템을 생성하고 관리합니다. 부모 관절과 자식 관절 사이에 부피 유지 본을 배치하여 관절 변형 시 부피 감소를 방지하고 더 자연스러운 움직임을 구현합니다.
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: 헬퍼 서비스 (제공되지 않으면 새로 생성)
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: 메소드 체이닝을 위한 자기 자신 반환
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
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
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 생성을 위한 결과 딕셔너리
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 클래스로 생성된 볼륨 본들의 집합을 관리하는 클래스입니다. 볼륨 본의 크기 조절, 회전 및 이동 축 변경, 스케일 조정 등의 기능을 제공하며, 여러 개의 볼륨 본을 하나의 논리적 체인으로 관리합니다. 생성된 볼륨 본 체인은 캐릭터 관절의 자연스러운 변형을 위해 사용됩니다.
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 메서드가 반환한 결과 딕셔너리 (루트 본, 회전 헬퍼, 회전 축, 이동 축, 볼륨 크기 등의 정보 포함)
96 def get_volume_size(self): 97 """ 98 볼륨 뼈대의 크기 가져오기 99 100 볼륨 본 생성 시 설정된 크기 값을 반환합니다. 이 값은 관절의 볼륨감 정도를 101 결정합니다. 102 103 Returns: 104 float: 현재 설정된 볼륨 크기 값 105 """ 106 return self.volumeSize
볼륨 뼈대의 크기 가져오기
볼륨 본 생성 시 설정된 크기 값을 반환합니다. 이 값은 관절의 볼륨감 정도를 결정합니다.
Returns: float: 현재 설정된 볼륨 크기 값
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
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
체인의 모든 뼈대 및 헬퍼 참조 제거
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)
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)
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)
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)
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)
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)
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 인스턴스
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 시스템 기반의 다리 리깅을 자동화하며, 무릎 관절 회전, 비틀림 본 및 중간 본을 생성하여 자연스러운 무릎 움직임을 구현합니다.
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: 볼륨 본 서비스 (제공되지 않으면 새로 생성)
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: 헬퍼 생성 성공 여부
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: 헬퍼 생성 성공 여부
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: 헬퍼 생성 성공 여부
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)
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)
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: 중간 본 생성 성공 여부
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: 비틀림 본 생성 성공 여부
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
자동 무릎 본 시스템의 모든 요소를 생성하는 주요 메서드입니다.
이 메서드는 다음 단계들을 순차적으로 실행합니다:
- LookAt 헬퍼 생성
- 회전 루트 헬퍼 생성
- 회전 헬퍼 생성
- 대퇴골과 종아리뼈 회전 제약 설정
- 무릎 중간 본 생성
- 비틀림 본 생성 및 제약 설정
Args: inThigh: 대퇴골 본 객체 inCalf: 종아리뼈 본 객체 inFoot: 발 본 객체 inLiftScale: 회전 영향력 스케일 (0.0~1.0) inKneePopScale: 무릎 앞쪽 돌출 스케일 (1.0이 기본값) inKneeBackScale: 무릎 뒤쪽 돌출 스케일 (1.0이 기본값)
Returns: bool: 자동 무릎 본 시스템 생성 성공 여부
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: 메소드 체이닝을 위한 자기 자신 반환
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를 통해 제어합니다.
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 관련 서비스 (제공되지 않으면 새로 생성)
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: 메소드 체이닝을 위한 자기 자신 반환
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)
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()
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
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"))
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