pyjallib 0.1.12__py3-none-any.whl → 0.1.14__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,639 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ Root Motion 모듈
6
+ 3DS Max에서 Root Motion을 처리하는 기능을 제공
7
+ """
8
+
9
+ from pymxs import runtime as rt
10
+ from pymxs import attime, animate, undo
11
+
12
+ from .name import Name
13
+ from .anim import Anim
14
+ from .helper import Helper
15
+ from .constraint import Constraint
16
+ from .bip import Bip
17
+
18
+ class RootMotion:
19
+ """
20
+ Root Motion 관련 기능을 위한 클래스
21
+ 3DS Max에서 Root Motion을 처리하는 기능을 제공합니다.
22
+ """
23
+
24
+ def __init__(self, nameService=None, animService=None, constraintService=None, helperService=None, bipService=None):
25
+ """
26
+ 클래스 초기화.
27
+
28
+ Args:
29
+ nameService: 이름 처리 서비스 (제공되지 않으면 새로 생성)
30
+ animService: 애니메이션 서비스 (제공되지 않으면 새로 생성)
31
+ constraintService: 제약 서비스 (제공되지 않으면 새로 생성)
32
+ bipService: Biped 서비스 (제공되지 않으면 새로 생성)
33
+ helperService: 헬퍼 객체 서비스 (제공되지 않으면 새로 생성)
34
+ """
35
+ self.name = nameService if nameService else Name()
36
+ self.anim = animService if animService else Anim()
37
+ self.const = constraintService if constraintService else Constraint(nameService=self.name)
38
+ self.bip = bipService if bipService else Bip(nameService=self.name, animService=self.anim)
39
+ self.helper = helperService if helperService else Helper(nameService=self.name)
40
+
41
+ # Root Motion 관련 변수 초기화
42
+ self.rootNode = None
43
+ self.pelvis = None
44
+ self.lFoot = None
45
+ self.rFoot = None
46
+ self.floorThreshold = 2.0 # 바닥 접촉 임계값 기본값
47
+ self.footSpeedThreshold = 1.0 # 발 속도 임계값 기본값
48
+ self.fps = 60.0
49
+ self.keepZAtZero = True # Z축을 0으로 유지할지 여부
50
+ self.followZRotation = False # XY 회전을 잠글지 여부
51
+ self.accelerationThreshold = 3.0 # 가속도 변화 임계값 (기본값: convert_keyframe_data_for_locomotion의 기본값)
52
+ self.keySmoothness = 10.0 # 키 부드러움 (기본값: apply_keyframes_locomotion_mode의 기본값)
53
+ self.directionThreshold = 0.005 # 방향 감지 임계값 (기본값: convert_keyframe_data_for_locomotion의 기본값)
54
+ self.accelerationFrameRange = 1 # 가속도 계산 프레임 범위 (기본값: convert_keyframe_data_for_locomotion의 기본값)
55
+
56
+ def is_foot_planted(self, footBone, frameTime, floorThreshold=2.0, fps=60.0, footSpeedThreshold=0.1):
57
+ """
58
+ 발이 바닥에 고정되어 있는지 확인하는 함수
59
+
60
+ Args:
61
+ footBone (node): 발 본 객체
62
+ frameTime (int): 현재 프레임 시간
63
+ floorThreshold (float): 바닥 접촉 임계값 (기본값: 2.0)
64
+ fps (float): 초당 프레임 수 (기본값: 60.0)
65
+ footSpeedThreshold (float): 발 속도 임계값 (기본값: 0.1)
66
+
67
+ Returns:
68
+ bool: 발이 바닥에 고정되어 있으면 True, 그렇지 않으면 False
69
+ """
70
+ footPosCurrentWorld = footBone.transform.position
71
+ footPosPrevWorld = footBone.transform.position
72
+ isPlanted = False
73
+ frameIntervalSec = 1.0 / fps if fps > 0 else 0.0
74
+
75
+ with attime(frameTime):
76
+ footPosCurrentWorld = footBone.transform.position
77
+
78
+ if frameTime > int(rt.animationRange.start):
79
+ with attime(frameTime -1):
80
+ footPosPrevWorld = footBone.transform.position
81
+
82
+ distMovedXY = rt.distance(rt.Point2(footPosCurrentWorld.x, footPosCurrentWorld.y),
83
+ rt.Point2(footPosPrevWorld.x, footPosPrevWorld.y))
84
+ if frameIntervalSec > 0.0:
85
+ footSpeedXY = distMovedXY
86
+ else:
87
+ footSpeedXY = 0.0
88
+ else:
89
+ footSpeedXY = 0.0
90
+
91
+ if footPosCurrentWorld.z <= floorThreshold and footSpeedXY <= footSpeedThreshold:
92
+ isPlanted = True
93
+
94
+ return isPlanted
95
+
96
+ def create_root_motion_from_bounding_box(self, bipCom, rootBone, startFrame, endFrame, floorThreshold=2.0, footSpeedThreshold=1.0, keepZAtZero=True, followZRotation=False):
97
+ """
98
+ Root Motion을 Bounding Box를 기반으로 생성하는 함수 (키프레임 데이터만 생성)
99
+
100
+ Args:
101
+ bipCom (node): Biped COM 객체
102
+ rootBone (node): 루트 본 객체
103
+ startFrame (int): 시작 프레임
104
+ endFrame (int): 끝 프레임
105
+ floorThreshold (float): 바닥 접촉 임계값 (기본값: 2.0)
106
+ footSpeedThreshold (float): 발 속도 임계값 (기본값: 1.0)
107
+ keepZAtZero (bool): Z축을 0으로 유지할지 여부 (기본값: True)
108
+ followZRotation (bool): Z축 회전을 따라갈지 여부 (기본값: False)
109
+
110
+ Returns:
111
+ dict: 키프레임 데이터 딕셔너리 (실패시 None)
112
+ """
113
+ # 입력 검증
114
+ if not rt.isValidNode(rootBone) or startFrame >= endFrame or not rt.isValidNode(bipCom):
115
+ return None
116
+
117
+ self.rootNode = rootBone
118
+ # 발 본 가져오기
119
+ lToes_nodes = self.bip.get_grouped_nodes(bipCom, "lToes")
120
+ rToes_nodes = self.bip.get_grouped_nodes(bipCom, "rToes")
121
+ if not lToes_nodes or not rToes_nodes:
122
+ return None
123
+
124
+ self.lFoot = lToes_nodes[0]
125
+ self.rFoot = rToes_nodes[0]
126
+ self.pelvis = self.bip.get_grouped_nodes(bipCom, "pelvis")[0]
127
+
128
+ # 필요한 Biped 노드 그룹들을 수집
129
+ node_groups = ["pelvis", "lLeg", "rLeg", "spine", "neck", "head"]
130
+ allBipNodes = []
131
+
132
+ for group in node_groups:
133
+ nodes = self.bip.get_grouped_nodes(bipCom, group)
134
+ allBipNodes.extend(nodes)
135
+ # 유효한 노드만 필터링
136
+ allBipNodes = [node for node in allBipNodes if node and rt.isValidNode(node)]
137
+ if not allBipNodes:
138
+ return None
139
+
140
+ self.floorThreshold = floorThreshold
141
+ self.footSpeedThreshold = footSpeedThreshold
142
+ self.keepZAtZero = keepZAtZero
143
+ self.followZRotation = followZRotation
144
+
145
+ # 시작 프레임에서 상대적 위치 계산
146
+ with attime(startFrame): # 바운딩 박스 계산
147
+ initialBbox = rt.box3()
148
+ for obj in allBipNodes:
149
+ initialBbox += obj.boundingBox
150
+ # 바운딩 박스 유효성 확인
151
+ if initialBbox.min == initialBbox.max:
152
+ return None
153
+
154
+ initialBboxCenter = initialBbox.center
155
+ initialBboxSize = initialBbox.max - initialBbox.min
156
+ initialRootPos = bipCom.transform.position
157
+ initialZOffset = -bipCom.transform.position.z
158
+ initialRot = self.rootNode.transform.rotation
159
+
160
+ # 상대적 오프셋 계산 (0으로 나누기 방지)
161
+ MIN_SIZE = 0.001
162
+ relativeOffsetX = (initialRootPos.x - initialBboxCenter.x) / initialBboxSize.x if abs(initialBboxSize.x) > MIN_SIZE else 0.0
163
+ relativeOffsetY = (initialRootPos.y - initialBboxCenter.y) / initialBboxSize.y if abs(initialBboxSize.y) > MIN_SIZE else 0.0
164
+
165
+ # 키프레임 데이터 수집
166
+ keyframe_data = {}
167
+
168
+ for t in range(startFrame, endFrame + 1):
169
+ isLFootPlanted = self.is_foot_planted(self.lFoot, t, self.floorThreshold, self.fps, self.footSpeedThreshold)
170
+ isRFootPlanted = self.is_foot_planted(self.rFoot, t, self.floorThreshold, self.fps, self.footSpeedThreshold)
171
+
172
+ # 양발이 모두 땅에 붙어있지 않을 때만 루트 모션 계산
173
+ if not (isLFootPlanted and isRFootPlanted):
174
+ # 현재 프레임의 바운딩 박스 계산
175
+ with attime(t):
176
+ currentBbox = rt.box3()
177
+ validNodeCount = 0
178
+
179
+ for obj in allBipNodes:
180
+ currentBbox += obj.boundingBox
181
+ validNodeCount += 1
182
+ # 유효한 바운딩 박스 확인
183
+ if validNodeCount == 0 or currentBbox.min == currentBbox.max:
184
+ continue
185
+
186
+ currentBboxCenter = currentBbox.center
187
+ currentBboxSize = currentBbox.max - currentBbox.min
188
+ # 새로운 루트 위치 계산
189
+ if self.keepZAtZero:
190
+ newRootPos = rt.Point3(
191
+ currentBboxCenter.x + (relativeOffsetX * currentBboxSize.x),
192
+ currentBboxCenter.y + (relativeOffsetY * currentBboxSize.y),
193
+ 0.0 # Z축은 0으로 유지
194
+ )
195
+ else:
196
+ newRootPos = rt.Point3(
197
+ currentBboxCenter.x + (relativeOffsetX * currentBboxSize.x),
198
+ currentBboxCenter.y + (relativeOffsetY * currentBboxSize.y),
199
+ self.pelvis.transform.position.z + initialZOffset # Z축은 현재 펠비스 위치에 오프셋 추가
200
+ )
201
+ # 로테이션 계산
202
+ if self.followZRotation:
203
+ # 펠비스의 Z축 회전을 따라감
204
+ newRootRot = rt.EulerAngles(0, 0, rt.quatToEuler(bipCom.transform.rotation).z)
205
+ else:
206
+ # 회전 없음 (기본값)
207
+ newRootRot = rt.quatToEuler(initialRot)
208
+
209
+ # 딕셔너리에 위치와 회전 정보 저장
210
+ keyframe_data[t] = {
211
+ 'position': newRootPos,
212
+ 'rotation': newRootRot,
213
+ 'bipComPos': bipCom.transform.position,
214
+ 'bipComRot': bipCom.transform.rotation
215
+ }
216
+
217
+ return keyframe_data
218
+
219
+ def convert_keyframe_data_for_locomotion(self, bipCom, keyframe_data, acceleration_threshold=3.0, direction_threshold=0.005, acceleration_frame_range=1, followZRotation=False):
220
+ """
221
+ 로코모션 모드에 맞게 키프레임 데이터를 변환하는 함수
222
+
223
+ Args:
224
+ bipCom (node): Biped COM 객체
225
+ keyframe_data (dict): 키프레임 데이터 딕셔너리
226
+ acceleration_threshold (float): 가속도 변화 임계값 (기본값: 3.0)
227
+ direction_threshold (float): 방향 감지 임계값 (기본값: 0.005, 0.0~1.0).
228
+ Strict activation uses (1.0 - direction_threshold).
229
+ acceleration_frame_range (int): 가속도 계산을 위한 프레임 범위 (기본값: 1)
230
+ followZRotation (bool): Z축 회전을 따라갈지 여부 (기본값: False)
231
+
232
+ Returns:
233
+ dict: 변환된 키프레임 데이터 딕셔너리
234
+ """
235
+ if not keyframe_data or not rt.isValidNode(bipCom):
236
+ return {}
237
+
238
+ # Update instance attributes with provided parameters
239
+ self.accelerationThreshold = acceleration_threshold
240
+ self.directionThreshold = direction_threshold
241
+ self.accelerationFrameRange = acceleration_frame_range
242
+ self.followZRotation = followZRotation
243
+
244
+ converted_data = {}
245
+ frame_list = sorted(keyframe_data.keys())
246
+
247
+ min_frames_needed = 2 * self.accelerationFrameRange + 1 # Use instance attribute
248
+ if len(frame_list) < min_frames_needed:
249
+ print(f"Warning: Need at least {min_frames_needed} frames for acceleration calculation with range {self.accelerationFrameRange}")
250
+ # Return empty or partially processed if preferred, for now returning empty
251
+ return {}
252
+
253
+ # self.accelerationThreshold is already set above
254
+
255
+ first_frame = frame_list[0]
256
+ first_bipcom_pos = keyframe_data[first_frame]['bipComPos']
257
+
258
+ world_forward = rt.Point3(0, -1, 0)
259
+ world_backward = rt.Point3(0, 1, 0)
260
+ world_right = rt.Point3(-1, 0, 0)
261
+ world_left = rt.Point3(1, 0, 0)
262
+
263
+ prev_primary_direction = ""
264
+ strict_activation_thresh = 1.0 - self.directionThreshold # Use instance attribute
265
+
266
+ for i, frame in enumerate(frame_list):
267
+ frame_data = keyframe_data[frame]
268
+ bipcom_pos = frame_data['bipComPos']
269
+ bipcom_rot = frame_data['bipComRot']
270
+
271
+ movement_direction_vec = rt.Point3(0, 0, 0)
272
+ movement_magnitude = 0.0
273
+
274
+ if i > 0:
275
+ prev_frame_data = keyframe_data[frame_list[i-1]]
276
+ movement_vector = bipcom_pos - prev_frame_data['bipComPos']
277
+ movement_magnitude = rt.length(movement_vector)
278
+ if movement_magnitude > 0.001:
279
+ movement_direction_vec = rt.normalize(movement_vector)
280
+ elif i < len(frame_list) - 1: # First frame, use next frame for initial direction
281
+ next_frame_data = keyframe_data[frame_list[i+1]]
282
+ movement_vector = next_frame_data['bipComPos'] - bipcom_pos
283
+ movement_magnitude = rt.length(movement_vector)
284
+ if movement_magnitude > 0.001:
285
+ movement_direction_vec = rt.normalize(movement_vector)
286
+
287
+ dot_forward = 0.0
288
+ dot_backward = 0.0
289
+ dot_right = 0.0
290
+ dot_left = 0.0
291
+
292
+ if movement_magnitude > 0.001:
293
+ dot_forward = rt.dot(movement_direction_vec, world_forward)
294
+ dot_backward = rt.dot(movement_direction_vec, world_backward)
295
+ dot_right = rt.dot(movement_direction_vec, world_right)
296
+ dot_left = rt.dot(movement_direction_vec, world_left)
297
+
298
+ # Determine strictly active directions
299
+ strictly_active_directions = []
300
+ if dot_forward >= strict_activation_thresh:
301
+ strictly_active_directions.append(("forward", dot_forward))
302
+ if dot_backward >= strict_activation_thresh:
303
+ strictly_active_directions.append(("backward", dot_backward))
304
+ if dot_right >= strict_activation_thresh:
305
+ strictly_active_directions.append(("right", dot_right))
306
+ if dot_left >= strict_activation_thresh:
307
+ strictly_active_directions.append(("left", dot_left))
308
+
309
+ current_primary_direction = ""
310
+ current_active_directions_list = []
311
+ is_transition_frame = not strictly_active_directions
312
+
313
+ if is_transition_frame:
314
+ current_primary_direction = "Transition"
315
+ else:
316
+ strictly_active_directions.sort(key=lambda x: x[1], reverse=True) # Sort by dot product value
317
+ current_primary_direction = strictly_active_directions[0][0]
318
+ current_active_directions_list = [d[0] for d in strictly_active_directions]
319
+
320
+ # Calculate locomotion_pos
321
+ locomotion_pos_z = frame_data['position'].z # Z is always from original calculation or fixed
322
+ if i == 0:
323
+ locomotion_pos = rt.Point3(first_bipcom_pos.x, first_bipcom_pos.y, locomotion_pos_z)
324
+ else:
325
+ prev_locomotion_pos = converted_data[frame_list[i-1]]['position']
326
+ locomotion_pos = rt.Point3(prev_locomotion_pos.x, prev_locomotion_pos.y, locomotion_pos_z)
327
+
328
+ if is_transition_frame:
329
+ locomotion_pos.x = bipcom_pos.x
330
+ locomotion_pos.y = bipcom_pos.y
331
+ else:
332
+ # Update Y if forward or backward is strictly active
333
+ if "forward" in current_active_directions_list or "backward" in current_active_directions_list:
334
+ locomotion_pos.y = bipcom_pos.y
335
+
336
+ # Update X if right or left is strictly active
337
+ if "right" in current_active_directions_list or "left" in current_active_directions_list:
338
+ locomotion_pos.x = bipcom_pos.x
339
+
340
+ # Direction changed flag considers "Transition" as a distinct direction state
341
+ direction_changed_flag = (current_primary_direction != prev_primary_direction and prev_primary_direction != "")
342
+
343
+ # Determine final rotation for the frame
344
+ final_frame_rotation = frame_data['rotation'] # Default to original rotation
345
+
346
+ if self.followZRotation: # Use instance attribute
347
+ if current_primary_direction == "Transition":
348
+ transition_z_rotation = 0.0
349
+ if dot_forward > 0:
350
+ transition_z_rotation += (-90.0 * dot_forward)
351
+ if dot_backward > 0:
352
+ transition_z_rotation += (90.0 * dot_backward)
353
+ if dot_left > 0: # world_left (1,0,0) corresponds to 180 deg Z rotation
354
+ transition_z_rotation += (0.0 * dot_left)
355
+ if dot_right > 0: # world_right (-1,0,0) corresponds to -180 deg Z rotation
356
+ transition_z_rotation += (-180.0 * dot_right)
357
+
358
+ # Clamp the transition_z_rotation
359
+ if transition_z_rotation < -180.0:
360
+ transition_z_rotation = -180.0
361
+ elif transition_z_rotation > 180.0:
362
+ transition_z_rotation = 180.0
363
+
364
+ final_frame_rotation = rt.EulerAngles(0, 0, transition_z_rotation)
365
+ elif current_primary_direction == "forward":
366
+ final_frame_rotation = rt.EulerAngles(0, 0, -90)
367
+ elif current_primary_direction == "backward":
368
+ final_frame_rotation = rt.EulerAngles(0, 0, 90)
369
+ elif current_primary_direction == "left":
370
+ final_frame_rotation = rt.EulerAngles(0, 0, 0)
371
+ elif current_primary_direction == "right":
372
+ final_frame_rotation = rt.EulerAngles(0, 0, -180)
373
+ # If followZRotation is true but direction is not one of the above (e.g. empty, though unlikely),
374
+ # it will keep the original frame_data['rotation']. This is a safe fallback.
375
+
376
+ converted_data[frame] = {
377
+ 'position': locomotion_pos,
378
+ 'rotation': final_frame_rotation, # Apply the calculated rotation
379
+ 'bipComPos': bipcom_pos,
380
+ 'bipComRot': bipcom_rot,
381
+ 'direction': current_primary_direction,
382
+ 'direction_changed': direction_changed_flag,
383
+ 'active_directions': current_active_directions_list, # Will be empty for "Transition"
384
+ 'dot_values': {
385
+ 'forward': dot_forward,
386
+ 'backward': dot_backward,
387
+ 'right': dot_right,
388
+ 'left': dot_left
389
+ },
390
+ 'direction_threshold': direction_threshold, # Store original for reference
391
+ 'strict_activation_threshold': strict_activation_thresh,
392
+ 'velocity': rt.Point3(0, 0, 0),
393
+ 'acceleration': rt.Point3(0, 0, 0),
394
+ 'acceleration_magnitude': 0.0,
395
+ 'needs_keyframe': False
396
+ }
397
+
398
+ prev_primary_direction = current_primary_direction
399
+
400
+ # 속도 계산 (지정된 프레임 범위를 사용)
401
+ for i in range(len(frame_list)):
402
+ current_frame = frame_list[i]
403
+ current_data = converted_data[current_frame]
404
+
405
+ prev_index = max(0, i - self.accelerationFrameRange) # Use instance attribute
406
+ next_index = min(len(frame_list) - 1, i + self.accelerationFrameRange) # Use instance attribute
407
+
408
+ if prev_index != i and next_index != i:
409
+ prev_frame_for_vel = frame_list[prev_index]
410
+ next_frame_for_vel = frame_list[next_index]
411
+ prev_data_for_vel = converted_data[prev_frame_for_vel]
412
+ next_data_for_vel = converted_data[next_frame_for_vel]
413
+
414
+ frame_diff = float(next_frame_for_vel - prev_frame_for_vel)
415
+ pos_diff = next_data_for_vel['position'] - prev_data_for_vel['position']
416
+
417
+ if frame_diff > 0:
418
+ current_data['velocity'] = pos_diff / frame_diff
419
+ elif next_index != i: # Start of range
420
+ next_frame_for_vel = frame_list[next_index]
421
+ next_data_for_vel = converted_data[next_frame_for_vel]
422
+ frame_diff = float(next_frame_for_vel - current_frame)
423
+ pos_diff = next_data_for_vel['position'] - current_data['position']
424
+ if frame_diff > 0:
425
+ current_data['velocity'] = pos_diff / frame_diff
426
+ elif prev_index != i: # End of range
427
+ prev_frame_for_vel = frame_list[prev_index]
428
+ prev_data_for_vel = converted_data[prev_frame_for_vel]
429
+ frame_diff = float(current_frame - prev_frame_for_vel)
430
+ pos_diff = current_data['position'] - prev_data_for_vel['position']
431
+ if frame_diff > 0:
432
+ current_data['velocity'] = pos_diff / frame_diff
433
+
434
+ # 가속도 계산 및 키프레임 필요성 판단
435
+ for i in range(len(frame_list)):
436
+ current_frame = frame_list[i]
437
+ current_data = converted_data[current_frame]
438
+
439
+ prev_index = max(0, i - self.accelerationFrameRange) # Use instance attribute
440
+ # next_index = min(len(frame_list) - 1, i + acceleration_frame_range) # Not used in this specific accel calc
441
+
442
+ if prev_index < i : # Check if there is a distinct previous frame for accel calc
443
+ prev_frame_for_accel = frame_list[prev_index]
444
+ prev_data_for_accel = converted_data[prev_frame_for_accel]
445
+
446
+ frame_span_from_prev_sample = float(current_frame - prev_frame_for_accel)
447
+ if frame_span_from_prev_sample > 0:
448
+ velocity_diff = current_data['velocity'] - prev_data_for_accel['velocity']
449
+ acceleration = velocity_diff / frame_span_from_prev_sample
450
+ current_data['acceleration'] = acceleration
451
+ current_data['acceleration_magnitude'] = rt.length(acceleration)
452
+
453
+ # 키프레임 필요성 판단 로직 수정
454
+ if current_data['direction'] == "Transition":
455
+ if current_data['acceleration_magnitude'] > (self.accelerationThreshold / 2.0):
456
+ current_data['needs_keyframe'] = True
457
+ else: # "forward", "backward", "left", "right"
458
+ if current_data['acceleration_magnitude'] > self.accelerationThreshold or current_data.get('direction_changed', False):
459
+ current_data['needs_keyframe'] = True
460
+
461
+ if frame_list:
462
+ if converted_data: # Ensure converted_data is not empty
463
+ if frame_list[0] in converted_data:
464
+ converted_data[frame_list[0]]['needs_keyframe'] = True
465
+ if len(frame_list) > 1 and frame_list[-1] in converted_data: # Ensure there's more than one frame
466
+ converted_data[frame_list[-1]]['needs_keyframe'] = True
467
+
468
+ return converted_data
469
+
470
+ def apply_keyframes_locomotion_mode(self, keyframeData, keySmoothness=10.0):
471
+ """
472
+ 로코모션 모드로 키프레임을 적용하는 함수 (needs_keyframe이 True인 프레임에만 키 생성)
473
+
474
+ Args:
475
+ keyframeData (dict): 키프레임 데이터 딕셔너리 (convert_keyframe_data_for_locomotion에서 변환된 데이터)
476
+
477
+ Returns:
478
+ bool: 성공 여부
479
+ """
480
+ if not keyframeData or not self.rootNode:
481
+ return False
482
+
483
+ node_name = self.rootNode.name
484
+ frame_list = sorted(keyframeData.keys())
485
+
486
+ if len(frame_list) < 1:
487
+ return False
488
+
489
+ # 모든 프레임 데이터를 MAXScript 배열로 준비
490
+ pos_list = [f'[{data["position"].x}, {data["position"].y}, {data["position"].z}]' for data in keyframeData.values()]
491
+ rot_list = [f'(eulerAngles {data["rotation"].x} {data["rotation"].y} {data["rotation"].z})' for data in keyframeData.values()]
492
+ needs_keyframe_list = [str(data.get("needs_keyframe", False)).lower() for data in keyframeData.values()]
493
+
494
+ maxScriptFrameArray = f"#({', '.join(map(str, frame_list))})"
495
+ maxScriptPosArray = f"#({', '.join(pos_list)})"
496
+ maxScriptRotArray = f"#({', '.join(rot_list)})"
497
+ maxScriptNeedsKeyframeArray = f"#({', '.join(needs_keyframe_list)})"
498
+
499
+ maxscriptCode = f"""
500
+ (
501
+ local frameArray = {maxScriptFrameArray}
502
+ local posArray = {maxScriptPosArray}
503
+ local rotArray = {maxScriptRotArray}
504
+ local needsKeyframeArray = {maxScriptNeedsKeyframeArray}
505
+
506
+ disableSceneRedraw()
507
+
508
+ animate on(
509
+ for i = 1 to frameArray.count do
510
+ (
511
+ -- needs_keyframe이 true인 경우에만 키프레임 생성
512
+ if needsKeyframeArray[i] == true then
513
+ (
514
+ local frame_time = frameArray[i]
515
+ local position = posArray[i]
516
+ local rotation = rotArray[i]
517
+
518
+ at time frame_time (
519
+ $'{node_name}'.position = position
520
+ $'{node_name}'.transform = (matrix3 1) * (rotateXMatrix rotation.x) * (rotateYMatrix rotation.y) * (rotateZMatrix rotation.z) * (transMatrix $'{node_name}'.pos)
521
+ )
522
+ )
523
+ )
524
+ )
525
+
526
+ reduceKeys $'{node_name}'.position.controller {keySmoothness} 1f
527
+ reduceKeys $'{node_name}'.rotation.controller {keySmoothness} 1f
528
+ reduceKeys $'{node_name}'.scale.controller {keySmoothness} 1f
529
+
530
+ enableSceneRedraw()
531
+ )
532
+ """
533
+
534
+ print(maxscriptCode)
535
+
536
+ try:
537
+ # 키프레임이 생성될 범위 계산
538
+ start_frame = min(frame_list)
539
+ end_frame = max(frame_list)
540
+
541
+ # 첫 번째 실행 (3DS Max 버그 우회용)
542
+ rt.execute(maxscriptCode)
543
+
544
+ # 생성된 키들을 프레임 범위에서만 삭제
545
+ self.anim.delete_keys_in_range(self.rootNode, start_frame, end_frame)
546
+
547
+ # 두 번째 실행 (실제 키 생성)
548
+ rt.execute(maxscriptCode)
549
+
550
+ return True
551
+
552
+ except Exception as e:
553
+ print(f"Error applying keyframes in locomotion mode: {e}")
554
+ return False
555
+
556
+ def apply_keyframes_normal_mode(self, keyframeData, keySmoothness=10.0):
557
+ """
558
+ 일반 모드로 키프레임을 적용하는 함수 (모든 키프레임에 키 생성)
559
+
560
+ Args:
561
+ keyframeData (dict): 키프레임 데이터 딕셔너리
562
+
563
+ Returns:
564
+ bool: 성공 여부
565
+ """
566
+ if not keyframeData or not self.rootNode:
567
+ return False
568
+
569
+ node_name = self.rootNode.name
570
+ frame_list = list(keyframeData.keys())
571
+ pos_list = [f'[{data["position"].x}, {data["position"].y}, {data["position"].z}]' for data in keyframeData.values()]
572
+ rot_list = [f'(eulerAngles {data["rotation"].x} {data["rotation"].y} {data["rotation"].z})' for data in keyframeData.values()]
573
+
574
+ maxScriptFrameArray = f"#({', '.join(map(str, frame_list))})"
575
+ maxScriptPosArray = f"#({', '.join(pos_list)})"
576
+ maxScriptRotArray = f"#({', '.join(rot_list)})"
577
+
578
+ maxscriptCode = f"""
579
+ (
580
+ local frameArray = {maxScriptFrameArray}
581
+ local posArray = {maxScriptPosArray}
582
+ local rotArray = {maxScriptRotArray}
583
+
584
+ disableSceneRedraw()
585
+
586
+ animate on(
587
+ for i = 1 to frameArray.count do
588
+ (
589
+ local frame_time = frameArray[i]
590
+ local position = posArray[i]
591
+ local rotation = rotArray[i]
592
+
593
+ at time frame_time (
594
+ $'{node_name}'.position = position
595
+ $'{node_name}'.transform = (matrix3 1) * (rotateXMatrix rotation.x) * (rotateYMatrix rotation.y) * (rotateZMatrix rotation.z) * (transMatrix $'{node_name}'.pos)
596
+ )
597
+ )
598
+ )
599
+
600
+ reduceKeys $'{node_name}'.position.controller {keySmoothness} 1f
601
+ reduceKeys $'{node_name}'.rotation.controller {keySmoothness} 1f
602
+ reduceKeys $'{node_name}'.scale.controller {keySmoothness} 1f
603
+
604
+ enableSceneRedraw()
605
+ )
606
+ """
607
+ print(maxscriptCode)
608
+
609
+ try:
610
+ # 첫 번째 실행 (3DS Max 버그 우회용)
611
+ rt.execute(maxscriptCode)
612
+
613
+ # 생성된 키들을 프레임 범위에서만 삭제
614
+ if frame_list:
615
+ start_frame = min(frame_list)
616
+ end_frame = max(frame_list)
617
+ self.anim.delete_keys_in_range(self.rootNode, start_frame, end_frame)
618
+
619
+ # 두 번째 실행 (실제 키 생성)
620
+ rt.execute(maxscriptCode)
621
+ return True
622
+ except Exception as e:
623
+ print(f"Error applying keyframes in normal mode: {e}")
624
+ return False
625
+
626
+ def get_bipcom_position(self, frame_time):
627
+ """
628
+ 특정 프레임에서 bipCom의 위치를 가져오는 헬퍼 함수
629
+
630
+ Args:
631
+ frame_time (int): 프레임 시간
632
+
633
+ Returns:
634
+ Point3: bipCom의 위치
635
+ """
636
+ if hasattr(self, 'pelvis') and self.pelvis and rt.isValidNode(self.pelvis):
637
+ with attime(frame_time):
638
+ return self.pelvis.transform.position
639
+ return rt.Point3(0, 0, 0)