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.
- pyjallib/__init__.py +1 -1
- pyjallib/max/__init__.py +8 -4
- pyjallib/max/anim.py +84 -0
- pyjallib/max/autoClavicle.py +1 -1
- pyjallib/max/bip.py +89 -79
- pyjallib/max/bone.py +84 -22
- pyjallib/max/boneChain.py +19 -22
- pyjallib/max/fbxHandler.py +215 -0
- pyjallib/max/groinBone.py +3 -3
- pyjallib/max/header.py +10 -13
- pyjallib/max/hip.py +4 -4
- pyjallib/max/kneeBone.py +44 -20
- pyjallib/max/layer.py +12 -6
- pyjallib/max/mocap.py +376 -0
- pyjallib/max/rootMotion.py +639 -0
- pyjallib/max/toolManager.py +92 -0
- pyjallib/max/twistBone.py +5 -1
- pyjallib/max/volumeBone.py +2 -1
- pyjallib/perforce.py +116 -4
- {pyjallib-0.1.12.dist-info → pyjallib-0.1.14.dist-info}/METADATA +1 -1
- {pyjallib-0.1.12.dist-info → pyjallib-0.1.14.dist-info}/RECORD +22 -18
- {pyjallib-0.1.12.dist-info → pyjallib-0.1.14.dist-info}/WHEEL +0 -0
@@ -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)
|