-
Notifications
You must be signed in to change notification settings - Fork 17
/
morphing.py
297 lines (255 loc) · 10.9 KB
/
morphing.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
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
#
# Copyright (C) 2020-2022 Michael Vigovsky
import logging
import bpy # pylint: disable=import-error
from .lib import morpher, fit_calc, utils
from .common import manager
logger = logging.getLogger(__name__)
class OpResetChar(bpy.types.Operator):
bl_idname = "charmorph.reset_char"
bl_label = "Reset character"
bl_description = "Reset all unavailable character morphs"
bl_options = {"UNDO"}
@classmethod
def poll(cls, context):
return context.mode == "OBJECT" and manager.morpher.core.char
def execute(self, _):
mcore = manager.morpher.core
mcore.cleanup_asset_morphs()
mcore.obj.data["cm_morpher"] = "ext"
new_morpher = morpher.get(mcore.obj)
if (new_morpher.error and not new_morpher.alt_topo_buildable) or not new_morpher.core.has_morphs():
if new_morpher.error:
self.report({'ERROR'}, new_morpher.error)
else:
self.report({'ERROR'}, "Error - no morphs found")
del mcore.obj.data["cm_morpher"]
return {"CANCELLED"}
manager.update_morpher(new_morpher)
return {"FINISHED"}
class OpProceedSlowMorphing(bpy.types.Operator):
bl_idname = "charmorph.proceed_slow"
bl_label = "Proceed"
bl_description = "Proceed to slow morphing"
bl_options = {"UNDO"}
@classmethod
def poll(cls, context):
return context.mode == "OBJECT" and manager.morpher and manager.morpher.is_slow
def execute(self, _):
manager.morpher.is_slow = False
return {"FINISHED"}
class OpBuildAltTopo(bpy.types.Operator):
bl_idname = "charmorph.build_alt_topo"
bl_label = "Build alt topo"
bl_description = "Build alt topo from modified character mesh"
bl_options = {"UNDO"}
@classmethod
def poll(cls, _):
return manager.morpher and manager.morpher.core.alt_topo_buildable and manager.morpher.core.has_morphs()
def execute(self, context): # pylint: disable=no-self-use
ui = context.window_manager.charmorph_ui
mcore = manager.morpher.core
obj = mcore.obj
btype = ui.alt_topo_build_type
sk = obj.data.shape_keys
has_sk = bool(sk and sk.key_blocks)
if btype == "K" and has_sk:
obj.data["cm_alt_topo"] = "sk"
manager.update_morpher(morpher.get(obj))
return {"FINISHED"}
result = fit_calc.FitCalculator(fit_calc.geom_morpher_final(mcore))\
.get_binding(obj).fit(mcore.full_basis - mcore.get_final())
result += utils.get_morphed_numpy(obj)
result = result.reshape(-1)
if btype == "K":
basis = obj.shape_key_add(name="Basis", from_mix=False)
final = obj.shape_key_add(name="charmorph_final", from_mix=False)
basis.data.foreach_set("co", result)
obj.data["cm_alt_topo"] = "sk"
final.value = 1
else:
mesh = obj.data.copy()
obj.data["cm_alt_topo"] = mesh
if has_sk:
old_mesh = obj.data
obj.data = mesh
while mesh.shape_keys and mesh.shape_keys.key_blocks:
obj.shape_key_remove(mesh.shape_keys.key_blocks[0])
obj.data = old_mesh
mesh.vertices.foreach_set("co", result)
manager.update_morpher(morpher.get(obj))
return {"FINISHED"}
class UIProps:
relative_meta: bpy.props.BoolProperty(
name="Relative meta props",
description="Adjust meta props relatively",
default=True)
meta_materials: bpy.props.EnumProperty(
name="Materials",
description="How changing meta properties will affect materials",
default="A",
items=[
("N", "None", "Don't change materials"),
("A", "Absolute", "Change materials according to absolute value of meta property"),
("R", "Relative", "Change materials according to relative value of meta property")])
morph_filter: bpy.props.StringProperty(
name="Filter",
description="Show only morphs matching this name",
options={"TEXTEDIT_UPDATE"},
)
morph_clamp: bpy.props.BoolProperty(
name="Clamp props",
description="Clamp properties to (-1..1) so they remain in realistic range",
get=lambda _: manager.morpher.core.clamp,
set=lambda _, value: manager.morpher.set_clamp(value),
update=lambda _ui, _: manager.morpher.update())
morph_l1: bpy.props.EnumProperty(
name="Type",
description="Choose character type",
items=lambda _ui, _: manager.morpher.L1_list,
get=lambda _: manager.morpher.L1_idx,
set=lambda _, value: manager.morpher.set_L1_by_idx(value),
options={"SKIP_SAVE"})
morph_category: bpy.props.EnumProperty(
name="Category",
items=lambda _ui, _:
[("<None>", "<None>", "Hide all morphs"), ("<All>", "<All>", "Show all morphs")]
+ [(name, name, "") for name in manager.morpher.categories],
description="Select morphing categories to show")
morph_preset: bpy.props.EnumProperty(
name="Presets",
items=lambda _ui, _: manager.morpher.presets_list,
description="Choose morphing preset",
update=lambda ui, _: manager.morpher.apply_morph_data(
manager.morpher.presets.get(ui.morph_preset), ui.morph_preset_mix))
morph_preset_mix: bpy.props.BoolProperty(
name="Mix with current",
description="Mix selected preset with current morphs",
default=False)
alt_topo_build_type: bpy.props.EnumProperty(
name="Alt topo type",
description="Type of alt topo to build",
default="P",
items=[
("K", "Shapekey", "Store alt topo basis in shapekey"),
("P", "Separate mesh", "Store alt topo basis in separate mesh")])
class CHARMORPH_PT_Morphing(bpy.types.Panel):
bl_label = "Morphing"
bl_parent_id = "VIEW3D_PT_CharMorph"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_order = 2
@classmethod
def poll(cls, context):
if context.mode != "OBJECT":
if manager.morpher and not manager.morpher.error:
manager.last_object = None
manager.morpher.error = "Please re-select character"
return False
return manager.morpher
def draw(self, context):
mm = manager.morpher
m = mm.core
ui = context.window_manager.charmorph_ui
if mm.is_slow:
col = self.layout.column()
col.label(text="Warning:")
col.label(text="Morphing a rigged character")
col.label(text="with this rig type")
col.label(text="can be very slow")
col.label(text="Proceed with caution")
self.layout.operator("charmorph.proceed_slow")
return
if mm.error:
self.layout.label(text="Morphing error:")
col = self.layout.column()
for line in mm.error.split("\n"):
col.label(text=line)
if m.alt_topo_buildable:
col = self.layout.column()
col.label(text="It seems there have been changes to object's topology")
col.label(text="You can try to build alt topo")
col.label(text="to continue morphing")
self.layout.operator("charmorph.build_alt_topo")
self.layout.prop(ui, "alt_topo_build_type")
return
if not hasattr(context.window_manager, "charmorphs") or not m.has_morphs():
if m.char:
col = self.layout.column(align=True)
col.label(text="Object is detected as")
col.label(text="valid CharMorph character,")
col.label(text="but the morphing data was removed")
if m.obj.data.get("cm_morpher") == "ext":
return
col.separator()
col.label(text="You can reset the character")
col.label(text="to resume morphing")
col.separator()
col.operator('charmorph.reset_char')
else:
self.layout.label(text="No morphing data found")
return
self.layout.label(text="Character type")
col = self.layout.column(align=True)
if m.morphs_l1:
col.prop(ui, "morph_l1")
col = self.layout.column(align=True)
col.prop(ui, "morph_preset")
col.prop(ui, "morph_preset_mix")
col.separator()
morphs = context.window_manager.charmorphs
meta_morphs = m.char.morphs_meta.keys()
if meta_morphs:
self.layout.label(text="Meta morphs")
col = self.layout.column(align=True)
col.prop(ui, "meta_materials")
col.prop(ui, "relative_meta")
for prop in meta_morphs:
col.prop(morphs, "meta_" + prop, slider=True)
self.layout.prop(ui, "morph_clamp")
self.layout.separator()
if mm.categories:
self.layout.label(text="Sub Morphs:")
self.layout.prop(ui, "morph_category")
if ui.morph_category == "<None>":
return
self.layout.prop(ui, "morph_filter")
col = self.layout.column(align=True)
for morph in m.morphs_l2:
prop = morph.name
if not prop:
col.separator()
elif ui.morph_category == "<All>" or prop.startswith(ui.morph_category):
if ui.morph_filter.lower() in prop.lower():
col.prop(morphs, "prop_" + prop, slider=True)
class CHARMORPH_PT_Materials(bpy.types.Panel):
bl_label = "Materials"
bl_parent_id = "VIEW3D_PT_CharMorph"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_options = {"DEFAULT_CLOSED"}
bl_order = 6
@classmethod
def poll(cls, _):
return manager.morpher and manager.morpher.materials and manager.morpher.materials.props
def draw(self, _):
for _, prop in manager.morpher.materials.get_node_outputs():
self.layout.prop(prop, "default_value", text=prop.node.label)
classes = [OpResetChar, OpBuildAltTopo, OpProceedSlowMorphing, CHARMORPH_PT_Morphing, CHARMORPH_PT_Materials]