-
Notifications
You must be signed in to change notification settings - Fork 4
/
__init__.py
318 lines (280 loc) · 10.7 KB
/
__init__.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
# Thanks to X-Ray/Stalker engine developers
# Thanks to Vakhurin Sergey (igel), Pavel_Blend: https://github.com/PavelBlend/blender-xray
bl_info = {
'name' : 'X-Ray .skls File Browser',
'description' : 'X-Ray/Stalker engine animation browser for .skls files.',
'author' : 'Viktoria Danchenko',
'version' : (0, 4),
'blender' : (2, 80, 0),
'location' : '3D View > N Panel > Skls file browser',
'category' : '3D View',
'wiki_url': 'https://github.com/vika-sonne/xray-skls-file-browser',
'tracker_url': 'https://github.com/vika-sonne/xray-skls-file-browser/issues',
}
from typing import List, Dict, Optional
import bpy
from bpy.utils import register_class, unregister_class
from mathutils import Matrix, Euler
from . import xray_skls
class View3DPanel:
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
class SKLSBROWSER_OT_open_skls_file(bpy.types.Operator):
'Shows file open dialog, reads .skls file, cleares & populates animations list'
bl_idname = 'skls_browser.open_skls_file'
bl_label = 'Open .skls file'
bl_description = 'Opens .skls file with collection of animations. Used to import X-Ray engine animations.'+\
' To import select object with X-Ray struct of bones'
# bl_options = {'REGISTER', 'UNDO'}
class SklsAnimations(bpy.types.PropertyGroup):
'Contains animation properties in animations list'
name: bpy.props.StringProperty(name='Name')
frames: bpy.props.IntProperty(name='frames')
fps: bpy.props.FloatProperty(name='fps')
filepath: bpy.props.StringProperty(subtype='FILE_PATH')
filter_glob: bpy.props.StringProperty(default='*.skls', options={'HIDDEN'})
@classmethod
def poll(cls, context):
return context.active_object is not None and hasattr(context.active_object.data, 'bones')
def execute(self, context):
self.report({'INFO'}, 'Loading animations from .skls file: "{}"'.format(self.filepath))
bpy.context.window.cursor_set('WAIT')
ob = bpy.context.object
ob.SklsAnimations.clear()
SKLSBROWSER_PT_skls_browser.skls_file = xray_skls.SklsFile(file_path=self.filepath)
self.report({'INFO'}, 'Done: {} animation(s)'.format(len(SKLSBROWSER_PT_skls_browser.skls_file.animations)))
# fill list with animations names
for animation_name, offset_frames_fps in SKLSBROWSER_PT_skls_browser.skls_file.animations.items():
newitem = ob.SklsAnimations.add()
newitem.name = animation_name
newitem.frames = offset_frames_fps[1]
newitem.fps = offset_frames_fps[2]
bpy.context.window.cursor_set('DEFAULT')
return {'FINISHED'}
def invoke(self, context, event):
wm = context.window_manager
wm.fileselect_add(operator=self)
return {'RUNNING_MODAL'}
class SKLSBROWSER_OT_close_skls_file(bpy.types.Operator):
'Closes .skls file and free resources'
bl_idname = 'skls_browser.close_skls_file'
bl_label = 'Close .skls file'
bl_description = 'Closes .skls file'
# bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
ob = bpy.context.object
try:
return bool(ob.SklsAnimations) or bool(SKLSBROWSER_PT_skls_browser.skls_file)
except:
return False
def execute(self, context):
ob = bpy.context.object
try:
ob.SklsAnimations.clear()
except:
pass
SKLSBROWSER_PT_skls_browser.skls_file = None
return {'FINISHED'}
class SKLSBROWSER_PT_skls_browser(View3DPanel, bpy.types.Panel):
'Contains open .skls file operator, animations list'
bl_label = 'Skls file browser'
bl_category = 'Skls'
skls_file: Optional[xray_skls.SklsFile] = None # pure python hold variable of .skls file buffer instance
def draw(self, context):
layout = self.layout
row = layout.row(align=True)
try:
# integration with another add-on
row.operator(operator='xray_import.object', text='Import object file...')
except:
pass
row.operator(operator='skls_browser.open_skls_file', text='Open skls file...')
row.operator(operator='skls_browser.close_skls_file', text='', icon='X')
# if SKLSBROWSER_PT_skls_browser.skls_file:
# col.label(text=SKLSBROWSER_PT_skls_browser.skls_file.file_path)
# else:
# col.label(text='')
if hasattr(context.object, 'SklsAnimations'):
layout.template_list(listtype_name='SKLSBROWSER_UL_animation_list_item', list_id='compact',
dataptr=context.object, propname='SklsAnimations',
active_dataptr=context.object, active_propname='SklsAnimations_index', rows=15)#, type='COMPACT')
class SKLSBROWSER_UL_animation_list_item(bpy.types.UIList):
def draw_item(self, _context, layout, _data, item : SKLSBROWSER_OT_open_skls_file.SklsAnimations,
_icon, _active_data, _active_propname):
row = layout.row(align=True)
row = row.split(factor=0.37)
row.alignment = 'RIGHT'
row.label(text='%s@%s' % (item.frames, item.fps))
row.alignment = 'LEFT'
row.label(text=item.name)
def animations_index_changed(self, context):
'''Selected animation changed in .skls list'''
# try to cancel & unlink old animation
try:
bpy.ops.screen.animation_cancel()
except:
pass
try:
# it can happened that unlink action is inaccessible
bpy.ops.action.unlink()
except:
pass
# get new animation name
if not SKLSBROWSER_PT_skls_browser.skls_file:
return
animation_name = self.SklsAnimations[self.SklsAnimations_index].name
animation_fps = self.SklsAnimations[self.SklsAnimations_index].fps
# remove previous animation if need
ob = context.active_object
if ob.animation_data:
try:
# integration with another add-on
need_to_remove = ob.animation_data.action.name not in context.object.actions_to_egg.animations
except:
need_to_remove = True
if need_to_remove:
# need to remove previous animation to free the memory since .skls can contains thousand animations
try:
act = ob.animation_data.action
ob.animation_data_clear()
act.user_clear()
bpy.data.actions.remove(action=act)
except:
pass
# import animation from .skls file
if animation_name not in bpy.data.actions:
# animation not imported yet # import & create animation to bpy.data.actions
context.window.cursor_set('WAIT')
fail_bones_names = set()
import_animation(
animation_name=animation_name,
animation=SKLSBROWSER_PT_skls_browser.skls_file.get_animation(animation_name),
bpy_armature=ob,
bonesmap={ b.name.lower(): b for b in ob.data.bones },
fail_bones_names=fail_bones_names)
context.window.cursor_set('DEFAULT')
# try to find DopeSheet editor & set action to play
try:
ds = [ i for i in context.screen.areas if i.type=='DOPESHEET_EDITOR']
if ds and not ds[0].spaces[0].action:
ds.spaces[0].action = bpy.data.actions[animation_name]
except:
pass
# assign & play a new animation
# bpy.data.armatures[0].pose_position='POSE'
try:
act = bpy.data.actions[animation_name]
if not ob.animation_data:
ob.animation_data_create()
ob.animation_data.action = act
except:
pass
else:
# play an action from first to last frames in cycle
try:
active_scene = bpy.context.window.scene
active_scene.frame_start = act.frame_range[0]
active_scene.frame_current = act.frame_range[0]
active_scene.frame_end = act.frame_range[1]
active_scene.render.fps = animation_fps
bpy.ops.screen.animation_play()
except:
pass
def import_animation(
animation_name: str,
animation: xray_skls.Animation,
bpy_armature,
bonesmap: Dict[str, bpy.types.Bone],
fail_bones_names: List[str]) -> bpy.types.Action:
def find_bone_exportable_parent(bpy_bone):
def is_exportable_bone(bpy_bone) -> bool:
return bpy_bone.xray.exportable and not bpy_bone.name.endswith('.fake')
result = bpy_bone.parent
while (result is not None) and not is_exportable_bone(result):
result = result.parent
return result
MATRIX_BONE = Matrix((
(1.0, 0.0, 0.0, 0.0),
(0.0, 0.0, -1.0, 0.0),
(0.0, 1.0, 0.0, 0.0),
(0.0, 0.0, 0.0, 1.0)
)).freeze()
ret = bpy.data.actions.new(name=animation_name)
ret.use_fake_user = True # force the animation to be kept
for bone in animation.bones:
tmpfc: List[bpy.types.FCurve] = [ret.fcurves.new('temp', index=i) for i in range(6)]
try:
times: Dict[float, bool] = {}
for envelope, fcurve in zip(bone.envelopes, tmpfc):
for key in envelope.keys:
time = key.time * animation.fps
times[time] = True
fcurve.keyframe_points.insert(frame=time, value=key.value, options={'FAST'})
armature_bone = bpy_armature.data.bones.get(bone.name, None)
if armature_bone is None:
armature_bone = bonesmap.get(bone.name.lower(), None)
if armature_bone is None:
if bone.name not in fail_bones_names:
# warn('bone is not found', bone=bone.name)
fail_bones_names.add(bone.name)
continue
if bone.name not in fail_bones_names:
fail_bones_names.add(bone.name)
bone.name = armature_bone.name
data_path = 'pose.bones["' + bone.name + '"]'
fcs: List[bpy.types.FCurve] = [
ret.fcurves.new(data_path=data_path + '.location', index=0, action_group=bone.name),
ret.fcurves.new(data_path=data_path + '.location', index=1, action_group=bone.name),
ret.fcurves.new(data_path=data_path + '.location', index=2, action_group=bone.name),
ret.fcurves.new(data_path=data_path + '.rotation_euler', index=0, action_group=bone.name),
ret.fcurves.new(data_path=data_path + '.rotation_euler', index=1, action_group=bone.name),
ret.fcurves.new(data_path=data_path + '.rotation_euler', index=2, action_group=bone.name)
]
xmat = armature_bone.matrix_local.inverted()
real_parent = find_bone_exportable_parent(armature_bone)
if real_parent:
xmat = xmat @ real_parent.matrix_local
else:
xmat = xmat @ MATRIX_BONE
for time in times:
mat = xmat @ Matrix.Translation((
+tmpfc[0].evaluate(frame=time),
+tmpfc[1].evaluate(frame=time),
-tmpfc[2].evaluate(frame=time),
)) @ Euler((
-tmpfc[4].evaluate(frame=time),
-tmpfc[3].evaluate(frame=time),
+tmpfc[5].evaluate(frame=time),
), 'ZXY').to_matrix().to_4x4()
translation = mat.to_translation()
rotation = mat.to_euler('ZXY')
for i in range(3):
fcs[i + 0].keyframe_points.insert(frame=time, value=translation[i], options={'FAST'})
for i in range(3):
fcs[i + 3].keyframe_points.insert(frame=time, value=rotation[i], options={'FAST'})
finally:
for fcurve in tmpfc:
ret.fcurves.remove(fcurve=fcurve)
return ret
classes = (
SKLSBROWSER_PT_skls_browser,
SKLSBROWSER_OT_open_skls_file.SklsAnimations,
SKLSBROWSER_OT_open_skls_file,
SKLSBROWSER_OT_close_skls_file,
SKLSBROWSER_UL_animation_list_item,
)
def register():
for _ in classes:
register_class(_)
bpy.types.Object.SklsAnimations = bpy.props.CollectionProperty(type=SKLSBROWSER_OT_open_skls_file.SklsAnimations)
bpy.types.Object.SklsAnimations_index = bpy.props.IntProperty(update=animations_index_changed)
def unregister():
for _ in classes:
unregister_class(_)
if SKLSBROWSER_PT_skls_browser.skls_file:
SKLSBROWSER_PT_skls_browser.skls_file = None
del bpy.types.Object.SklsAnimations
del bpy.types.Object.SklsAnimations_index
if __name__ == '__main__':
register()