Repository navigation
Expand file tree
/
Copy pathops.py
More file actions
453 lines (366 loc) · 15.6 KB
/
Copy pathops.py
File metadata and controls
453 lines (366 loc) · 15.6 KB
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
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
# Copyright (c) 2026 Blender ML Deformer contributors.
# Licensed under the MIT License. See LICENSE in the project root.
"""Operators. Long work runs through modal operators driven by the generator
functions in ``train`` / ``ue``; ESC cancels and restores the scene state."""
import os
import bpy
from . import bridge
from . import train as train_ops
from . import ue as ue_ops
# ---------------------------------------------------------------------------
# List management
# ---------------------------------------------------------------------------
class BMD_OT_sync_bones(bpy.types.Operator):
bl_idname = "bmd.sync_bones"
bl_label = "Load Bones From Armature"
bl_description = ("Rebuild the bone input list from the armature "
"(existing per-bone settings are kept by name)")
def execute(self, context):
n = bridge.sync_bones(context.scene.bmd)
self.report({"INFO"}, "Loaded %d bones" % n)
return {"FINISHED"}
class BMD_OT_sync_curve_inputs(bpy.types.Operator):
bl_idname = "bmd.sync_curve_inputs"
bl_label = "Load Shape Keys"
bl_description = "List the mesh shape keys as curve inputs"
def execute(self, context):
n = bridge.sync_curve_inputs(context.scene.bmd)
self.report({"INFO"}, "Loaded %d shape keys" % n)
return {"FINISHED"}
class BMD_OT_sync_morph_targets(bpy.types.Operator):
bl_idname = "bmd.sync_morph_targets"
bl_label = "Load Shape Keys"
bl_description = "List the mesh shape keys as morph targets (Neural model)"
def execute(self, context):
n = bridge.sync_morph_targets(context.scene.bmd)
self.report({"INFO"}, "Loaded %d shape keys" % n)
return {"FINISHED"}
class BMD_OT_bones_remove(bpy.types.Operator):
bl_idname = "bmd.bones_remove"
bl_label = "Remove Bone"
index: bpy.props.IntProperty(default=-1)
def execute(self, context):
settings = context.scene.bmd
i = self.index if self.index >= 0 else settings.active_bone_index
if 0 <= i < len(settings.bones):
settings.bones.remove(i)
settings.active_bone_index = min(i, len(settings.bones) - 1)
return {"FINISHED"}
class BMD_OT_key_remove(bpy.types.Operator):
bl_idname = "bmd.key_remove"
bl_label = "Remove Key"
target: bpy.props.StringProperty(default="CURVE") # CURVE | MORPH
index: bpy.props.IntProperty(default=-1)
def execute(self, context):
settings = context.scene.bmd
if self.target == "MORPH":
coll, active = settings.morph_targets, "active_morph_index"
else:
coll, active = settings.curve_inputs, "active_curve_index"
i = self.index if self.index >= 0 else getattr(settings, active)
if 0 <= i < len(coll):
coll.remove(i)
setattr(settings, active, min(i, len(coll) - 1))
return {"FINISHED"}
class BMD_OT_clip_add(bpy.types.Operator):
bl_idname = "bmd.clip_add"
bl_label = "Add Action Slot"
action: bpy.props.StringProperty(name="Action Name")
def execute(self, context):
settings = context.scene.bmd
e = settings.training_clips.add()
e.name = self.action
settings.active_clip_index = len(settings.training_clips) - 1
return {"FINISHED"}
class BMD_OT_clip_add_all(bpy.types.Operator):
bl_idname = "bmd.clip_add_all"
bl_label = "Add All Actions"
def execute(self, context):
settings = context.scene.bmd
existing = {e.name for e in settings.training_clips}
for act in bpy.data.actions:
if act.name not in existing:
e = settings.training_clips.add()
e.name = act.name
self.report({"INFO"}, "%d actions listed" % len(settings.training_clips))
return {"FINISHED"}
class BMD_OT_clip_remove(bpy.types.Operator):
bl_idname = "bmd.clip_remove"
bl_label = "Remove Action Slot"
index: bpy.props.IntProperty(default=-1)
def execute(self, context):
settings = context.scene.bmd
i = self.index if self.index >= 0 else settings.active_clip_index
if 0 <= i < len(settings.training_clips):
settings.training_clips.remove(i)
settings.active_clip_index = min(i, len(settings.training_clips) - 1)
return {"FINISHED"}
# ---------------------------------------------------------------------------
# Modal base
# ---------------------------------------------------------------------------
class BMD_ProgressOperator(bpy.types.Operator):
"""Runs a generator in a modal loop with a progress bar; ESC cancels."""
bl_options = {"REGISTER"}
_timer = None
_gen = None
_cancelled = False
def _make_generator(self, context):
raise NotImplementedError
def _on_done(self, context):
pass
def invoke(self, context, event):
try:
self._gen = self._make_generator(context)
except Exception as exc:
self.report({"ERROR"}, str(exc))
return {"CANCELLED"}
wm = context.window_manager
self._timer = wm.event_timer_add(0.1, window=context.window)
wm.modal_handler_add(self)
wm.progress_begin(0, 100)
return {"RUNNING_MODAL"}
def _abort(self, context, message=None):
if self._gen is not None:
try:
self._gen.close() # runs the generator's finally: restores scene
except Exception:
pass
self._gen = None
self._finish(context, cancelled=True, message=message)
def _finish(self, context, cancelled, message=None):
wm = context.window_manager
wm.progress_end()
if self._timer is not None:
wm.event_timer_remove(self._timer)
self._timer = None
if cancelled:
self.report({"WARNING"}, message or "Cancelled")
else:
try:
self._on_done(context)
except Exception as exc:
self.report({"ERROR"}, str(exc))
self.report({"INFO"}, "Done")
def modal(self, context, event):
wm = context.window_manager
if event.type == "ESC":
self._cancelled = True
return {"RUNNING_MODAL"}
if event.type != "TIMER" or self._gen is None:
return {"PASS_THROUGH"}
if self._cancelled:
self._abort(context, "Cancelled")
return {"FINISHED"}
try:
frac = next(self._gen)
wm.progress_update(int(frac * 100))
if frac >= 1.0:
try:
next(self._gen)
except StopIteration:
pass
self._gen = None
self._finish(context, cancelled=False)
return {"FINISHED"}
except StopIteration:
self._gen = None
self._finish(context, cancelled=False)
return {"FINISHED"}
except Exception as exc:
self._abort(context, str(exc))
return {"FINISHED"}
return {"PASS_THROUGH"}
# ---------------------------------------------------------------------------
# Long operations
# ---------------------------------------------------------------------------
class BMD_OT_generate_random_poses(BMD_ProgressOperator):
bl_idname = "bmd.generate_random_poses"
bl_label = "Generate Random Poses"
bl_description = ("Sample random poses with the per-bone rotation ranges "
"and keyframe them straight onto the timeline (no mesh "
"evaluation, no training data)")
def _make_generator(self, context):
return train_ops.generate_random_poses_iter(context.scene.bmd)
def _on_done(self, context):
self.report({"INFO"}, "Keyframed %d random poses on the timeline"
% context.scene.bmd.num_random_poses)
class BMD_OT_generate_training_data(BMD_ProgressOperator):
bl_idname = "bmd.generate_training_data"
bl_label = "Generate Training Data"
def _make_generator(self, context):
return train_ops.generate_training_data_iter(context.scene.bmd)
def _on_done(self, context):
self.report({"INFO"}, "Generated %d training frames"
% context.scene.bmd.num_training_frames)
class BMD_OT_train(BMD_ProgressOperator):
bl_idname = "bmd.train"
bl_label = "Train Model"
def _make_generator(self, context):
return train_ops.train_model_iter(context.scene.bmd)
class BMD_OT_bake_shape_keys(BMD_ProgressOperator):
bl_idname = "bmd.bake_shape_keys"
bl_label = "Bake Shape Keys"
def _make_generator(self, context):
return train_ops.bake_shape_keys_iter(context.scene.bmd)
class BMD_OT_export_engine_nmn(BMD_ProgressOperator):
bl_idname = "bmd.export_engine_nmn"
bl_label = "Export Engine Network (.nmn)"
bl_description = ("Retrain the neural model on the engine input layout and "
"write a global-mode .nmn network file")
filepath: bpy.props.StringProperty(name="File Path", subtype="FILE_PATH")
def invoke(self, context, event):
base = context.scene.bmd.model_dir or (os.path.dirname(bpy.data.filepath)
if bpy.data.filepath else "")
self.filepath = os.path.join(base, (context.scene.bmd.model_name
or "blender_ml_deformer") + ".nmn")
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
def _make_generator(self, context):
if not self.filepath:
raise ValueError("no file selected")
return ue_ops.export_nmn_iter(context.scene.bmd, self.filepath)
def _on_done(self, context):
self.report({"INFO"}, "Wrote %s" % self.filepath)
# ---------------------------------------------------------------------------
# Preview / IO
# ---------------------------------------------------------------------------
class BMD_OT_create_preview_proxy(bpy.types.Operator):
bl_idname = "bmd.create_preview_proxy"
bl_label = "Create Preview Proxy"
def execute(self, context):
try:
obj = bridge.create_preview_proxy(context)
self.report({"INFO"}, "Created preview proxy %r" % obj.name)
except Exception as exc:
self.report({"ERROR"}, str(exc))
return {"CANCELLED"}
return {"FINISHED"}
class BMD_OT_refresh_preview(bpy.types.Operator):
bl_idname = "bmd.refresh_preview"
bl_label = "Refresh Preview"
def execute(self, context):
bridge.refresh_preview(context.scene, force=True)
return {"FINISHED"}
class BMD_OT_export_model(bpy.types.Operator):
bl_idname = "bmd.export_model"
bl_label = "Export Model"
filepath: bpy.props.StringProperty(name="File Path", subtype="FILE_PATH")
def invoke(self, context, event):
settings = context.scene.bmd
base = settings.model_dir or (os.path.dirname(bpy.data.filepath)
if bpy.data.filepath else "")
self.filepath = os.path.join(base, (settings.model_name
or "blender_ml_deformer") + ".json")
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
def execute(self, context):
settings = context.scene.bmd
if self.filepath:
settings.model_dir = os.path.dirname(self.filepath)
try:
path = train_ops.export_model(settings)
self.report({"INFO"}, "Exported to %s" % path)
except Exception as exc:
self.report({"ERROR"}, str(exc))
return {"CANCELLED"}
return {"FINISHED"}
class BMD_OT_import_model(bpy.types.Operator):
bl_idname = "bmd.import_model"
bl_label = "Import Model"
filepath: bpy.props.StringProperty(name="File Path", subtype="FILE_PATH")
def invoke(self, context, event):
settings = context.scene.bmd
base = settings.model_dir or (os.path.dirname(bpy.data.filepath)
if bpy.data.filepath else "")
self.filepath = os.path.join(base, "pose_model.json")
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
def execute(self, context):
settings = context.scene.bmd
if self.filepath:
settings.model_dir = os.path.dirname(self.filepath)
try:
name = train_ops.import_model(settings)
self.report({"INFO"}, "Imported model %r" % name)
except Exception as exc:
self.report({"ERROR"}, str(exc))
return {"CANCELLED"}
return {"FINISHED"}
class BMD_OT_import_engine_nmn(bpy.types.Operator):
bl_idname = "bmd.import_engine_nmn"
bl_label = "Import Engine Network (.nmn)"
filepath: bpy.props.StringProperty(name="File Path", subtype="FILE_PATH")
def invoke(self, context, event):
base = context.scene.bmd.model_dir or (os.path.dirname(bpy.data.filepath)
if bpy.data.filepath else "")
self.filepath = os.path.join(base, "model.nmn")
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
def execute(self, context):
try:
name = ue_ops.import_nmn(context.scene.bmd, self.filepath)
self.report({"INFO"}, "Imported engine network %r" % name)
except Exception as exc:
self.report({"ERROR"}, str(exc))
return {"CANCELLED"}
return {"FINISHED"}
class BMD_OT_import_engine_onnx(bpy.types.Operator):
bl_idname = "bmd.import_engine_onnx"
bl_label = "Import ONNX Network"
filepath: bpy.props.StringProperty(name="File Path", subtype="FILE_PATH")
def invoke(self, context, event):
base = context.scene.bmd.model_dir or (os.path.dirname(bpy.data.filepath)
if bpy.data.filepath else "")
self.filepath = os.path.join(base, "model.onnx")
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
def execute(self, context):
try:
name = ue_ops.import_onnx(context.scene.bmd, self.filepath)
self.report({"INFO"}, "Imported ONNX network %r" % name)
except Exception as exc:
self.report({"ERROR"}, str(exc))
return {"CANCELLED"}
return {"FINISHED"}
class BMD_OT_clear(bpy.types.Operator):
bl_idname = "bmd.clear"
bl_label = "Clear State"
def execute(self, context):
settings = context.scene.bmd
bridge.clear_runtime()
settings.is_trained = False
settings.num_features = 0
settings.num_vertices = 0
settings.num_training_frames = 0
settings.training_loss = 0.0
settings.max_vertex_error = 0.0
if settings.preview_object is not None:
bpy.data.objects.remove(settings.preview_object, do_unlink=True)
settings.preview_object = None
for obj in [o for o in context.scene.objects
if o.name.endswith(("_BMDPreview", "_BMDBaked"))]:
bpy.data.objects.remove(obj, do_unlink=True)
self.report({"INFO"}, "Cleared Blender ML Deformer state")
return {"FINISHED"}
classes = (
BMD_OT_sync_bones,
BMD_OT_sync_curve_inputs,
BMD_OT_sync_morph_targets,
BMD_OT_bones_remove,
BMD_OT_key_remove,
BMD_OT_clip_add,
BMD_OT_clip_add_all,
BMD_OT_clip_remove,
BMD_OT_generate_random_poses,
BMD_OT_generate_training_data,
BMD_OT_train,
BMD_OT_bake_shape_keys,
BMD_OT_export_engine_nmn,
BMD_OT_create_preview_proxy,
BMD_OT_refresh_preview,
BMD_OT_export_model,
BMD_OT_import_model,
BMD_OT_import_engine_nmn,
BMD_OT_import_engine_onnx,
BMD_OT_clear,
)