1 # SPDX-FileCopyrightText: 2022-2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 #-------------------------- COLORS / GROUPS EXCHANGER -------------------------#
7 # Vertex Color to Vertex Group allow you to convert colors channles to weight #
9 # The main purpose is to use vertex colors to store information when importing #
10 # files from other softwares. The script works with the active vertex color #
12 # For use the command "Vertex Clors to Vertex Groups" use the search bar #
15 # (c) Alessandro Zomparelli #
18 # http://www.co-de-it.com/ #
20 ################################################################################
24 import math
, timeit
, time
26 from statistics
import mean
, stdev
27 from mathutils
import Vector
28 from mathutils
.kdtree
import KDTree
30 try: import numexpr
as ne
33 from bpy
.types
import (
39 from bpy
.props
import (
52 class formula_prop(PropertyGroup
):
53 name
: StringProperty()
54 formula
: StringProperty()
55 float_var
: FloatVectorProperty(name
="", description
="", default
=(0, 0, 0, 0, 0), size
=5)
56 int_var
: IntVectorProperty(name
="", description
="", default
=(0, 0, 0, 0, 0), size
=5)
59 def compute_formula(ob
=None, formula
="rx", float_var
=(0,0,0,0,0), int_var
=(0,0,0,0,0)):
60 verts
= ob
.data
.vertices
63 f1
,f2
,f3
,f4
,f5
= float_var
64 i1
,i2
,i3
,i4
,i5
= int_var
66 do_groups
= "w[" in formula
67 do_local
= "lx" in formula
or "ly" in formula
or "lz" in formula
68 do_global
= "gx" in formula
or "gy" in formula
or "gz" in formula
69 do_relative
= "rx" in formula
or "ry" in formula
or "rz" in formula
70 do_normal
= "nx" in formula
or "ny" in formula
or "nz" in formula
74 if "w["+str(i
)+"]" in formula
and i
> len(ob
.vertex_groups
)-1:
75 return "w["+str(i
)+"] not found"
78 for i
in range(len(ob
.vertex_groups
)):
80 if "w["+str(i
)+"]" in formula
:
81 vg
= ob
.vertex_groups
[i
]
84 w
[i
].append(vg
.weight(v
.index
))
89 start_time
= timeit
.default_timer()
90 # compute vertex coordinates
91 if do_local
or do_relative
or do_global
:
93 verts
.foreach_get('co', co
)
94 np_co
= array(co
).reshape((n_verts
, 3))
95 lx
, ly
, lz
= array(np_co
).transpose()
97 rx
= np
.interp(lx
, (lx
.min(), lx
.max()), (0, +1))
98 ry
= np
.interp(ly
, (ly
.min(), ly
.max()), (0, +1))
99 rz
= np
.interp(lz
, (lz
.min(), lz
.max()), (0, +1))
101 co
= [v
.co
for v
in verts
]
104 global_co
.append(mat
@ v
)
105 global_co
= array(global_co
).reshape((n_verts
, 3))
106 gx
, gy
, gz
= array(global_co
).transpose()
107 # compute vertex normals
109 normal
= [0]*n_verts
*3
110 verts
.foreach_get('normal', normal
)
111 normal
= array(normal
).reshape((n_verts
, 3))
112 nx
, ny
, nz
= array(normal
).transpose()
115 weight
= eval(formula
)
118 return "There is something wrong"
119 print("Weight Formula: " + str(timeit
.default_timer() - start_time
))
121 class weight_formula_wiki(Operator
):
122 bl_idname
= "scene.weight_formula_wiki"
123 bl_label
= "Online Documentation"
124 bl_options
= {'REGISTER', 'UNDO'}
126 def execute(self
, context
):
127 bpy
.ops
.wm
.url_open(url
="https://github.com/alessandro-zomparelli/tissue/wiki/Weight-Tools#weight-formula")
130 class weight_formula(Operator
):
131 bl_idname
= "object.weight_formula"
132 bl_label
= "Weight Formula"
133 bl_description
= "Generate a Vertex Group according to a mathematical formula"
134 bl_options
= {'REGISTER', 'UNDO'}
137 ('cos(arctan(nx/ny)*i1*2 + sin(rz*i3))/i2 + cos(arctan(nx/ny)*i1*2 - sin(rz*i3))/i2 + 0.5','Vertical Spots'),
138 ('cos(arctan(nx/ny)*i1*2 + sin(rz*i2))/2 + cos(arctan(nx/ny)*i1*2 - sin(rz*i2))/2','Vertical Spots'),
139 ('(sin(arctan(nx/ny)*i1*2)*sin(nz*i1*2)+1)/2','Grid Spots'),
140 ('cos(arctan(nx/ny)*f1)','Vertical Stripes'),
141 ('cos(arctan(lx/ly)*f1 + sin(rz*f2)*f3)','Curly Stripes'),
142 ('sin(rz*pi*i1+arctan2(nx,ny))/2+0.5', 'Vertical Spiral'),
143 ('sin(nx*15)<sin(ny*15)','Chess'),
144 ('cos(ny*rz**2*i1)','Hyperbolic'),
145 ('sin(rx*30) > 0','Step Stripes'),
146 ('sin(nz*i1)','Normal Stripes'),
147 ('w[0]**2','Vertex Group square'),
148 ('abs(0.5-rz)*2','Double vertical gradient'),
149 ('rz', 'Vertical Gradient')
151 _ex_items
= list((str(i
),'{} ( {} )'.format(s
[0],s
[1]),s
[1]) for i
,s
in enumerate(ex_items
))
152 _ex_items
.append(('CUSTOM', "User Formula", ""))
154 examples
: EnumProperty(
155 items
= _ex_items
, default
='CUSTOM', name
="Examples")
159 formula
: StringProperty(
160 name
="Formula", default
="", description
="Formula to Evaluate")
162 slider_f01
: FloatProperty(
163 name
="f1", default
=1, description
="Slider Float 1")
164 slider_f02
: FloatProperty(
165 name
="f2", default
=1, description
="Slider Float 2")
166 slider_f03
: FloatProperty(
167 name
="f3", default
=1, description
="Slider Float 3")
168 slider_f04
: FloatProperty(
169 name
="f4", default
=1, description
="Slider Float 4")
170 slider_f05
: FloatProperty(
171 name
="f5", default
=1, description
="Slider Float 5")
172 slider_i01
: IntProperty(
173 name
="i1", default
=1, description
="Slider Integer 1")
174 slider_i02
: IntProperty(
175 name
="i2", default
=1, description
="Slider Integer 2")
176 slider_i03
: IntProperty(
177 name
="i3", default
=1, description
="Slider Integer 3")
178 slider_i04
: IntProperty(
179 name
="i4", default
=1, description
="Slider Integer 4")
180 slider_i05
: IntProperty(
181 name
="i5", default
=1, description
="Slider Integer 5")
183 def invoke(self
, context
, event
):
184 return context
.window_manager
.invoke_props_dialog(self
, width
=350)
186 def draw(self
, context
):
188 #layout.label(text="Examples")
189 layout
.prop(self
, "examples", text
="Examples")
190 #if self.examples == 'CUSTOM':
191 layout
.label(text
="Formula")
192 layout
.prop(self
, "formula", text
="")
193 #try: self.examples = self.formula
196 if self
.examples
!= 'CUSTOM':
197 example
= self
.ex_items
[int(self
.examples
)][0]
198 if example
!= self
.old_ex
:
199 self
.formula
= example
200 self
.old_ex
= example
201 elif self
.formula
!= example
:
202 self
.examples
= 'CUSTOM'
203 formula
= self
.formula
206 if "f1" in formula
: layout
.prop(self
, "slider_f01")
207 if "f2" in formula
: layout
.prop(self
, "slider_f02")
208 if "f3" in formula
: layout
.prop(self
, "slider_f03")
209 if "f4" in formula
: layout
.prop(self
, "slider_f04")
210 if "f5" in formula
: layout
.prop(self
, "slider_f05")
211 if "i1" in formula
: layout
.prop(self
, "slider_i01")
212 if "i2" in formula
: layout
.prop(self
, "slider_i02")
213 if "i3" in formula
: layout
.prop(self
, "slider_i03")
214 if "i4" in formula
: layout
.prop(self
, "slider_i04")
215 if "i5" in formula
: layout
.prop(self
, "slider_i05")
217 layout
.label(text
="Variables (for each vertex):")
218 layout
.label(text
="lx, ly, lz: Local Coordinates", icon
='ORIENTATION_LOCAL')
219 layout
.label(text
="gx, gy, gz: Global Coordinates", icon
='WORLD')
220 layout
.label(text
="rx, ry, rz: Local Coordinates (0 to 1)", icon
='NORMALIZE_FCURVES')
221 layout
.label(text
="nx, ny, nz: Normal Coordinates", icon
='SNAP_NORMAL')
222 layout
.label(text
="w[0], w[1], w[2], ... : Vertex Groups", icon
="GROUP_VERTEX")
224 layout
.label(text
="f1, f2, f3, f4, f5: Float Sliders", icon
='MOD_HUE_SATURATION')#PROPERTIES
225 layout
.label(text
="i1, i2, i3, i4, i5: Integer Sliders", icon
='MOD_HUE_SATURATION')
227 #layout.label(text="All mathematical functions are based on Numpy", icon='INFO')
228 #layout.label(text="https://docs.scipy.org/doc/numpy-1.13.0/reference/routines.math.html", icon='INFO')
229 layout
.operator("scene.weight_formula_wiki", icon
="HELP")
230 #layout.label(text="(where 'i' is the index of the Vertex Group)")
232 def execute(self
, context
):
233 ob
= context
.active_object
234 n_verts
= len(ob
.data
.vertices
)
235 #if self.examples == 'CUSTOM':
236 # formula = self.formula
238 #self.formula = self.examples
239 # formula = self.examples
241 #f1, f2, f3, f4, f5 = self.slider_f01, self.slider_f02, self.slider_f03, self.slider_f04, self.slider_f05
242 #i1, i2, i3, i4, i5 = self.slider_i01, self.slider_i02, self.slider_i03, self.slider_i04, self.slider_i05
243 f_sliders
= self
.slider_f01
, self
.slider_f02
, self
.slider_f03
, self
.slider_f04
, self
.slider_f05
244 i_sliders
= self
.slider_i01
, self
.slider_i02
, self
.slider_i03
, self
.slider_i04
, self
.slider_i05
246 if self
.examples
!= 'CUSTOM':
247 example
= self
.ex_items
[int(self
.examples
)][0]
248 if example
!= self
.old_ex
:
249 self
.formula
= example
250 self
.old_ex
= example
251 elif self
.formula
!= example
:
252 self
.examples
= 'CUSTOM'
253 formula
= self
.formula
255 if formula
== "": return {'FINISHED'}
256 # replace numeric sliders value
257 for i
, slider
in enumerate(f_sliders
):
258 formula
= formula
.replace('f'+str(i
+1),"{0:.2f}".format(slider
))
259 for i
, slider
in enumerate(i_sliders
):
260 formula
=formula
.replace('i'+str(i
+1),str(slider
))
261 vertex_group_name
= "" + formula
262 ob
.vertex_groups
.new(name
=vertex_group_name
)
264 weight
= compute_formula(ob
, formula
=formula
, float_var
=f_sliders
, int_var
=i_sliders
)
265 if type(weight
) == str:
266 self
.report({'ERROR'}, weight
)
269 #start_time = timeit.default_timer()
270 weight
= nan_to_num(weight
)
271 vg
= ob
.vertex_groups
[-1]
272 if type(weight
) == int or type(weight
) == float:
273 for i
in range(n_verts
):
274 vg
.add([i
], weight
, 'REPLACE')
275 elif type(weight
) == ndarray
:
276 for i
in range(n_verts
):
277 vg
.add([i
], weight
[i
], 'REPLACE')
279 bpy
.ops
.object.mode_set(mode
='WEIGHT_PAINT')
281 # Store formula settings
282 new_formula
= ob
.formula_settings
.add()
283 new_formula
.name
= ob
.vertex_groups
[-1].name
284 new_formula
.formula
= formula
285 new_formula
.int_var
= i_sliders
286 new_formula
.float_var
= f_sliders
288 #for f in ob.formula_settings:
289 # print(f.name, f.formula, f.int_var, f.float_var)
293 class update_weight_formula(Operator
):
294 bl_idname
= "object.update_weight_formula"
295 bl_label
= "Update Weight Formula"
296 bl_description
= "Update an existing Vertex Group. Make sure that the name\nof the active Vertex Group is a valid formula"
297 bl_options
= {'REGISTER', 'UNDO'}
300 def poll(cls
, context
):
301 return len(context
.object.vertex_groups
) > 0
303 def execute(self
, context
):
304 ob
= context
.active_object
305 n_verts
= len(ob
.data
.vertices
)
307 vg
= ob
.vertex_groups
.active
309 weight
= compute_formula(ob
, formula
=formula
)
310 if type(weight
) == str:
311 self
.report({'ERROR'}, "The name of the active Vertex Group\nis not a valid Formula")
314 #start_time = timeit.default_timer()
315 weight
= nan_to_num(weight
)
316 if type(weight
) == int or type(weight
) == float:
317 for i
in range(n_verts
):
318 vg
.add([i
], weight
, 'REPLACE')
319 elif type(weight
) == ndarray
:
320 for i
in range(n_verts
):
321 vg
.add([i
], weight
[i
], 'REPLACE')
323 bpy
.ops
.object.mode_set(mode
='WEIGHT_PAINT')
327 class _weight_laplacian(Operator
):
328 bl_idname
= "object._weight_laplacian"
329 bl_label
= "Weight Laplacian"
330 bl_description
= ("Compute the Vertex Group Laplacian")
331 bl_options
= {'REGISTER', 'UNDO'}
333 bounds
: EnumProperty(
334 items
=(('MANUAL', "Manual Bounds", ""),
335 ('POSITIVE', "Positive Only", ""),
336 ('NEGATIVE', "Negative Only", ""),
337 ('AUTOMATIC', "Automatic Bounds", "")),
338 default
='AUTOMATIC', name
="Bounds")
341 items
=(('LENGTH', "Length Weight", ""),
342 ('SIMPLE', "Simple", "")),
343 default
='SIMPLE', name
="Evaluation Mode")
345 min_def
: FloatProperty(
346 name
="Min", default
=0, soft_min
=-1, soft_max
=0,
347 description
="Laplacian value with 0 weight")
349 max_def
: FloatProperty(
350 name
="Max", default
=0.5, soft_min
=0, soft_max
=5,
351 description
="Laplacian value with 1 weight")
358 def poll(cls
, context
):
359 return len(context
.object.vertex_groups
) > 0
361 def draw(self
, context
):
363 col
= layout
.column(align
=True)
364 col
.label(text
="Evaluation Mode")
365 col
.prop(self
, "mode", text
="")
366 col
.label(text
="Bounds")
367 col
.prop(self
, "bounds", text
="")
368 if self
.bounds
== 'MANUAL':
369 col
.label(text
="Strain Rate \u03B5:")
370 col
.prop(self
, "min_def")
371 col
.prop(self
, "max_def")
372 col
.label(text
="\u03B5" + ": from " + self
.bounds_string
)
375 def execute(self
, context
):
376 try: ob
= context
.object
378 self
.report({'ERROR'}, "Please select an Object")
381 group_id
= ob
.vertex_groups
.active_index
382 input_group
= ob
.vertex_groups
[group_id
].name
384 group_name
= "Laplacian"
385 ob
.vertex_groups
.new(name
=group_name
)
389 bm
.edges
.ensure_lookup_table()
391 # store weight values
393 for v
in me
.vertices
:
395 weight
.append(ob
.vertex_groups
[input_group
].weight(v
.index
))
399 n_verts
= len(bm
.verts
)
402 if self
.mode
== 'LENGTH':
403 length
= e
.calc_length()
404 if length
== 0: continue
405 id0
= e
.verts
[0].index
406 id1
= e
.verts
[1].index
407 lap
[id0
] += weight
[id1
]/length
- weight
[id0
]/length
408 lap
[id1
] += weight
[id0
]/length
- weight
[id1
]/length
410 id0
= e
.verts
[0].index
411 id1
= e
.verts
[1].index
412 lap
[id0
] += weight
[id1
] - weight
[id0
]
413 lap
[id1
] += weight
[id0
] - weight
[id1
]
416 stdev_lap
= stdev(lap
)
417 filter_lap
= [i
for i
in lap
if mean_lap
-2*stdev_lap
< i
< mean_lap
+2*stdev_lap
]
418 if self
.bounds
== 'MANUAL':
419 min_def
= self
.min_def
420 max_def
= self
.max_def
421 elif self
.bounds
== 'AUTOMATIC':
422 min_def
= min(filter_lap
)
423 max_def
= max(filter_lap
)
424 self
.min_def
= min_def
425 self
.max_def
= max_def
426 elif self
.bounds
== 'NEGATIVE':
428 max_def
= min(filter_lap
)
429 self
.min_def
= min_def
430 self
.max_def
= max_def
431 elif self
.bounds
== 'POSITIVE':
433 max_def
= max(filter_lap
)
434 self
.min_def
= min_def
435 self
.max_def
= max_def
436 delta_def
= max_def
- min_def
438 # check undeformed errors
439 if delta_def
== 0: delta_def
= 0.0001
441 for i
in range(len(lap
)):
442 val
= (lap
[i
]-min_def
)/delta_def
443 #if val > 0.7: print(str(val) + " " + str(lap[i]))
444 #val = weight[i] + 0.2*lap[i]
445 ob
.vertex_groups
[-1].add([i
], val
, 'REPLACE')
446 self
.bounds_string
= str(round(min_def
,2)) + " to " + str(round(max_def
,2))
447 ob
.vertex_groups
[-1].name
= group_name
+ " " + self
.bounds_string
448 ob
.vertex_groups
.update()
450 bpy
.ops
.object.mode_set(mode
='WEIGHT_PAINT')
454 class ok_weight_laplacian(Operator
):
455 bl_idname
= "object.weight_laplacian"
456 bl_label
= "Weight Laplacian"
457 bl_description
= ("Compute the Vertex Group Laplacian")
458 bl_options
= {'REGISTER', 'UNDO'}
465 def poll(cls
, context
):
466 return len(context
.object.vertex_groups
) > 0
469 def execute(self
, context
):
470 try: ob
= context
.object
472 self
.report({'ERROR'}, "Please select an Object")
478 bm
.edges
.ensure_lookup_table()
480 group_id
= ob
.vertex_groups
.active_index
481 input_group
= ob
.vertex_groups
[group_id
].name
483 group_name
= "Laplacian"
484 ob
.vertex_groups
.new(name
=group_name
)
486 # store weight values
488 for v
in me
.vertices
:
490 a
.append(ob
.vertex_groups
[input_group
].weight(v
.index
))
498 n_verts
= len(bm
.verts
)
499 # find max number of edges for vertex
504 n_edges
= len(v
.link_edges
)
505 max_edges
= max(max_edges
, n_edges
)
506 n_neighbors
.append(n_edges
)
508 for e
in v
.link_edges
:
510 if v
!= v1
: neighbors
.append(v1
.index
)
511 id_neighbors
.append(neighbors
)
512 n_neighbors
= array(n_neighbors
)
515 lap_map
= [[] for i
in range(n_verts
)]
519 id0 = e.verts[0].index
520 id1 = e.verts[1].index
521 lap_map[id0].append(id1)
522 lap_map[id1].append(id0)
524 lap
= zeros((n_verts
))#[0]*n_verts
525 n_records
= zeros((n_verts
))
527 id0
= e
.verts
[0].index
528 id1
= e
.verts
[1].index
529 length
= e
.calc_length()
530 if length
== 0: continue
531 #lap[id0] += abs(a[id1] - a[id0])/length
532 #lap[id1] += abs(a[id0] - a[id1])/length
533 lap
[id0
] += (a
[id1
] - a
[id0
])/length
534 lap
[id1
] += (a
[id0
] - a
[id1
])/length
540 for i
in range(n_verts
):
541 ob
.vertex_groups
['Laplacian'].add([i
], lap
[i
], 'REPLACE')
542 ob
.vertex_groups
.update()
544 bpy
.ops
.object.mode_set(mode
='WEIGHT_PAINT')
548 class weight_laplacian(Operator
):
549 bl_idname
= "object.weight_laplacian"
550 bl_label
= "Weight Laplacian"
551 bl_description
= ("Compute the Vertex Group Laplacian")
552 bl_options
= {'REGISTER', 'UNDO'}
559 def poll(cls
, context
):
560 return len(context
.object.vertex_groups
) > 0
563 def execute(self
, context
):
564 try: ob
= context
.object
566 self
.report({'ERROR'}, "Please select an Object")
572 bm
.edges
.ensure_lookup_table()
573 n_verts
= len(me
.vertices
)
575 group_id
= ob
.vertex_groups
.active_index
576 input_group
= ob
.vertex_groups
[group_id
].name
578 group_name
= "Laplacian"
579 vg
= ob
.vertex_groups
.new(name
=group_name
)
581 # store weight values
582 dvert_lay
= bm
.verts
.layers
.deform
.active
583 weight
= bmesh_get_weight_numpy(group_id
, dvert_lay
, bm
.verts
)
585 #verts, normals = get_vertices_and_normals_numpy(me)
587 #lap = zeros((n_verts))#[0]*n_verts
588 lap
= [Vector((0,0,0)) for i
in range(n_verts
)]
589 n_records
= zeros((n_verts
))
599 v01
-= v01
.project(vert0
.normal
)
600 v10
-= v10
.project(vert1
.normal
)
601 length
= e
.calc_length()
602 if length
== 0: continue
603 dw
= (weight
[id1
] - weight
[id0
])/length
604 lap
[id0
] += v01
.normalized() * dw
605 lap
[id1
] -= v10
.normalized() * dw
608 #lap /= n_records[:,np.newaxis]
609 lap
= [l
.length
/r
for r
,l
in zip(n_records
,lap
)]
615 for i
in range(n_verts
):
616 vg
.add([i
], lap
[i
], 'REPLACE')
617 ob
.vertex_groups
.update()
619 bpy
.ops
.object.mode_set(mode
='WEIGHT_PAINT')
623 class edges_deformation(Operator
):
624 bl_idname
= "object.edges_deformation"
625 bl_label
= "Edges Deformation"
626 bl_description
= ("Compute Weight based on the deformation of edges"+
627 "according to visible modifiers.")
628 bl_options
= {'REGISTER', 'UNDO'}
630 bounds
: EnumProperty(
631 items
=(('MANUAL', "Manual Bounds", ""),
632 ('COMPRESSION', "Compressed Only", ""),
633 ('TENSION', "Extended Only", ""),
634 ('AUTOMATIC', "Automatic Bounds", "")),
635 default
='AUTOMATIC', name
="Bounds")
638 items
=(('MAX', "Max Deformation", ""),
639 ('MEAN', "Average Deformation", "")),
640 default
='MEAN', name
="Evaluation Mode")
642 min_def
: FloatProperty(
643 name
="Min", default
=0, soft_min
=-1, soft_max
=0,
644 description
="Deformations with 0 weight")
646 max_def
: FloatProperty(
647 name
="Max", default
=0.5, soft_min
=0, soft_max
=5,
648 description
="Deformations with 1 weight")
655 def poll(cls
, context
):
656 return len(context
.object.modifiers
) > 0
658 def draw(self
, context
):
660 col
= layout
.column(align
=True)
661 col
.label(text
="Evaluation Mode")
662 col
.prop(self
, "mode", text
="")
663 col
.label(text
="Bounds")
664 col
.prop(self
, "bounds", text
="")
665 if self
.bounds
== 'MANUAL':
666 col
.label(text
="Strain Rate \u03B5:")
667 col
.prop(self
, "min_def")
668 col
.prop(self
, "max_def")
669 col
.label(text
="\u03B5" + ": from " + self
.bounds_string
)
671 def execute(self
, context
):
672 try: ob
= context
.object
674 self
.report({'ERROR'}, "Please select an Object")
677 # check if the object is Cloth or Softbody
679 for m
in ob
.modifiers
:
680 if m
.type == 'CLOTH' or m
.type == 'SOFT_BODY':
682 if context
.scene
.frame_current
== 1 and self
.frame
!= None:
683 context
.scene
.frame_current
= self
.frame
685 if not physics
: self
.frame
= None
687 if self
.mode
== 'MEAN': group_name
= "Average Deformation"
688 elif self
.mode
== 'MAX': group_name
= "Max Deformation"
689 ob
.vertex_groups
.new(name
=group_name
)
692 me
= simple_to_mesh(ob
) #ob.to_mesh(preserve_all_data_layers=True, depsgraph=bpy.context.evaluated_depsgraph_get()).copy()
693 if len(me
.vertices
) != len(me0
.vertices
) or len(me
.edges
) != len(me0
.edges
):
694 self
.report({'ERROR'}, "The topology of the object should be" +
703 for e0
, e
in zip(bm0
.edges
, bm
.edges
):
705 l0
= e0
.calc_length()
707 epsilon
= (l1
- l0
)/l0
708 deformations
.append(epsilon
)
709 except: deformations
.append(1)
713 for e
in v
.link_edges
:
714 vdef
.append(deformations
[e
.index
])
715 if self
.mode
== 'MEAN': v_deformations
.append(mean(vdef
))
716 elif self
.mode
== 'MAX': v_deformations
.append(max(vdef
, key
=abs))
717 #elif self.mode == 'MIN': v_deformations.append(min(vdef, key=abs))
719 if self
.bounds
== 'MANUAL':
720 min_def
= self
.min_def
721 max_def
= self
.max_def
722 elif self
.bounds
== 'AUTOMATIC':
723 min_def
= min(v_deformations
)
724 max_def
= max(v_deformations
)
725 self
.min_def
= min_def
726 self
.max_def
= max_def
727 elif self
.bounds
== 'COMPRESSION':
729 max_def
= min(v_deformations
)
730 self
.min_def
= min_def
731 self
.max_def
= max_def
732 elif self
.bounds
== 'TENSION':
734 max_def
= max(v_deformations
)
735 self
.min_def
= min_def
736 self
.max_def
= max_def
737 delta_def
= max_def
- min_def
739 # check undeformed errors
741 if self
.bounds
== 'MANUAL':
744 message
= "The object doesn't have deformations."
746 message
= message
+ ("\nIf you are using Physics try to " +
747 "save it in the cache before.")
748 self
.report({'ERROR'}, message
)
752 self
.frame
= context
.scene
.frame_current
754 for i
in range(len(v_deformations
)):
755 weight
= (v_deformations
[i
] - min_def
)/delta_def
756 ob
.vertex_groups
[-1].add([i
], weight
, 'REPLACE')
757 self
.bounds_string
= str(round(min_def
,2)) + " to " + str(round(max_def
,2))
758 ob
.vertex_groups
[-1].name
= group_name
+ " " + self
.bounds_string
759 ob
.vertex_groups
.update()
761 bpy
.ops
.object.mode_set(mode
='WEIGHT_PAINT')
762 bpy
.data
.meshes
.remove(me
)
767 class edges_bending(Operator
):
768 bl_idname
= "object.edges_bending"
769 bl_label
= "Edges Bending"
770 bl_description
= ("Compute Weight based on the bending of edges"+
771 "according to visible modifiers.")
772 bl_options
= {'REGISTER', 'UNDO'}
774 bounds
: EnumProperty(
775 items
=(('MANUAL', "Manual Bounds", ""),
776 ('POSITIVE', "Positive Only", ""),
777 ('NEGATIVE', "Negative Only", ""),
778 ('UNSIGNED', "Absolute Bending", ""),
779 ('AUTOMATIC', "Signed Bending", "")),
780 default
='AUTOMATIC', name
="Bounds")
782 min_def
: FloatProperty(
783 name
="Min", default
=-10, soft_min
=-45, soft_max
=45,
784 description
="Deformations with 0 weight")
786 max_def
: FloatProperty(
787 name
="Max", default
=10, soft_min
=-45, soft_max
=45,
788 description
="Deformations with 1 weight")
794 def poll(cls
, context
):
795 return len(context
.object.modifiers
) > 0
797 def draw(self
, context
):
799 layout
.label(text
="Bounds")
800 layout
.prop(self
, "bounds", text
="")
801 if self
.bounds
== 'MANUAL':
802 layout
.prop(self
, "min_def")
803 layout
.prop(self
, "max_def")
805 def execute(self
, context
):
806 try: ob
= context
.object
808 self
.report({'ERROR'}, "Please select an Object")
811 group_name
= "Edges Bending"
812 ob
.vertex_groups
.new(name
=group_name
)
814 # check if the object is Cloth or Softbody
816 for m
in ob
.modifiers
:
817 if m
.type == 'CLOTH' or m
.type == 'SOFT_BODY':
819 if context
.scene
.frame_current
== 1 and self
.frame
!= None:
820 context
.scene
.frame_current
= self
.frame
822 if not physics
: self
.frame
= None
825 #context.scene.update()
827 me
= simple_to_mesh(ob
) #ob.to_mesh(preserve_all_data_layers=True, depsgraph=bpy.context.evaluated_depsgraph_get()).copy()
828 if len(me
.vertices
) != len(me0
.vertices
) or len(me
.edges
) != len(me0
.edges
):
829 self
.report({'ERROR'}, "The topology of the object should be" +
836 for e0
, e
in zip(bm0
.edges
, bm
.edges
):
838 ang
= e
.calc_face_angle_signed()
839 ang0
= e0
.calc_face_angle_signed()
840 if self
.bounds
== 'UNSIGNED':
841 deformations
.append(abs(ang
-ang0
))
843 deformations
.append(ang
-ang0
)
844 except: deformations
.append(0)
848 for e
in v
.link_edges
:
849 vdef
.append(deformations
[e
.index
])
850 v_deformations
.append(mean(vdef
))
851 if self
.bounds
== 'MANUAL':
852 min_def
= radians(self
.min_def
)
853 max_def
= radians(self
.max_def
)
854 elif self
.bounds
== 'AUTOMATIC':
855 min_def
= min(v_deformations
)
856 max_def
= max(v_deformations
)
857 elif self
.bounds
== 'POSITIVE':
859 max_def
= min(v_deformations
)
860 elif self
.bounds
== 'NEGATIVE':
862 max_def
= max(v_deformations
)
863 elif self
.bounds
== 'UNSIGNED':
865 max_def
= max(v_deformations
)
866 delta_def
= max_def
- min_def
868 # check undeformed errors
870 if self
.bounds
== 'MANUAL':
873 message
= "The object doesn't have deformations."
875 message
= message
+ ("\nIf you are using Physics try to " +
876 "save it in the cache before.")
877 self
.report({'ERROR'}, message
)
881 self
.frame
= context
.scene
.frame_current
883 for i
in range(len(v_deformations
)):
884 weight
= (v_deformations
[i
] - min_def
)/delta_def
885 ob
.vertex_groups
[-1].add([i
], weight
, 'REPLACE')
886 self
.bounds_string
= str(round(min_def
,2)) + " to " + str(round(max_def
,2))
887 ob
.vertex_groups
[-1].name
= group_name
+ " " + self
.bounds_string
888 ob
.vertex_groups
.update()
890 bpy
.ops
.object.mode_set(mode
='WEIGHT_PAINT')
891 bpy
.data
.meshes
.remove(me
)
896 class weight_contour_displace(Operator
):
897 bl_idname
= "object.weight_contour_displace"
898 bl_label
= "Contour Displace"
899 bl_description
= ("")
900 bl_options
= {'REGISTER', 'UNDO'}
902 use_modifiers
: BoolProperty(
903 name
="Use Modifiers", default
=True,
904 description
="Apply all the modifiers")
905 min_iso
: FloatProperty(
906 name
="Min Iso Value", default
=0.49, min=0, max=1,
907 description
="Threshold value")
908 max_iso
: FloatProperty(
909 name
="Max Iso Value", default
=0.51, min=0, max=1,
910 description
="Threshold value")
911 n_cuts
: IntProperty(
912 name
="Cuts", default
=2, min=1, soft_max
=10,
913 description
="Number of cuts in the selected range of values")
914 bool_displace
: BoolProperty(
915 name
="Add Displace", default
=True, description
="Add Displace Modifier")
916 bool_flip
: BoolProperty(
917 name
="Flip", default
=False, description
="Flip Output Weight")
919 weight_mode
: EnumProperty(
920 items
=[('Remapped', 'Remapped', 'Remap values'),
921 ('Alternate', 'Alternate', 'Alternate 0 and 1'),
922 ('Original', 'Original', 'Keep original Vertex Group')],
923 name
="Weight", description
="Choose how to convert vertex group",
924 default
="Remapped", options
={'LIBRARY_EDITABLE'})
927 def poll(cls
, context
):
928 return len(context
.object.vertex_groups
) > 0
930 def invoke(self
, context
, event
):
931 return context
.window_manager
.invoke_props_dialog(self
, width
=350)
933 def execute(self
, context
):
934 start_time
= timeit
.default_timer()
936 check
= context
.object.vertex_groups
[0]
938 self
.report({'ERROR'}, "The object doesn't have Vertex Groups")
943 group_id
= ob0
.vertex_groups
.active_index
944 vertex_group_name
= ob0
.vertex_groups
[group_id
].name
946 bpy
.ops
.object.mode_set(mode
='EDIT')
947 bpy
.ops
.mesh
.select_all(action
='DESELECT')
948 bpy
.ops
.object.mode_set(mode
='OBJECT')
949 if self
.use_modifiers
:
950 #me0 = ob0.to_mesh(preserve_all_data_layers=True, depsgraph=bpy.context.evaluated_depsgraph_get()).copy()
951 me0
= simple_to_mesh(ob0
)
953 me0
= ob0
.data
.copy()
958 bm
.verts
.ensure_lookup_table()
959 bm
.edges
.ensure_lookup_table()
960 bm
.faces
.ensure_lookup_table()
962 # store weight values
964 ob
= bpy
.data
.objects
.new("temp", me0
)
965 for g
in ob0
.vertex_groups
:
966 ob
.vertex_groups
.new(name
=g
.name
)
967 for v
in me0
.vertices
:
969 weight
.append(ob
.vertex_groups
[vertex_group_name
].weight(v
.index
))
975 for i_cut
in range(self
.n_cuts
):
976 delta_iso
= abs(self
.max_iso
- self
.min_iso
)
977 min_iso
= min(self
.min_iso
, self
.max_iso
)
978 max_iso
= max(self
.min_iso
, self
.max_iso
)
979 if delta_iso
== 0: iso_val
= min_iso
980 elif self
.n_cuts
> 1: iso_val
= i_cut
/(self
.n_cuts
-1)*delta_iso
+ min_iso
981 else: iso_val
= (self
.max_iso
+ self
.min_iso
)/2
982 iso_values
.append(iso_val
)
984 # Start Cuts Iterations
985 filtered_edges
= bm
.edges
986 for iso_val
in iso_values
:
997 if w
> w_max
: w_max
= w
998 if w
< w_min
: w_min
= w
999 if w_min
< iso_val
and w_max
> iso_val
:
1000 faces_mask
.append(f
)
1003 #link_faces = [[f for f in e.link_faces] for e in bm.edges]
1005 #faces_todo = [f.select for f in bm.faces]
1006 #faces_todo = [True for f in bm.faces]
1010 _filtered_edges
= []
1011 n_verts
= len(bm
.verts
)
1013 for e
in filtered_edges
:
1014 #id0 = e.vertices[0]
1015 #id1 = e.vertices[1]
1016 id0
= e
.verts
[0].index
1017 id1
= e
.verts
[1].index
1021 if w0
== w1
: continue
1022 elif w0
> iso_val
and w1
> iso_val
:
1023 _filtered_edges
.append(e
)
1025 elif w0
< iso_val
and w1
< iso_val
: continue
1026 elif w0
== iso_val
or w1
== iso_val
:
1027 _filtered_edges
.append(e
)
1030 v0
= bm
.verts
[id0
].co
1031 v1
= bm
.verts
[id1
].co
1032 v
= v0
.lerp(v1
, (iso_val
-w0
)/(w1
-w0
))
1033 if e
not in delete_edges
:
1034 delete_edges
.append(e
)
1036 edges_id
[str(id0
)+"_"+str(id1
)] = count
1037 edges_id
[str(id1
)+"_"+str(id0
)] = count
1039 _filtered_edges
.append(e
)
1040 filtered_edges
= _filtered_edges
1045 for f
in faces_mask
:
1046 # create sub-faces slots. Once a new vertex is reached it will
1047 # change slot, storing the next vertices for a new face.
1048 build_faces
= [[],[]]
1050 verts0
= [v
.index
for v
in f
.verts
]
1051 verts1
= list(verts0
)
1052 verts1
.append(verts1
.pop(0)) # shift list
1053 for id0
, id1
in zip(verts0
, verts1
):
1055 # add first vertex to active slot
1056 build_faces
[switch
].append(id0
)
1060 # check if the edge must be splitted
1061 new_vert
= edges_id
[str(id0
)+"_"+str(id1
)]
1063 build_faces
[switch
].append(new_vert
)
1064 # if there is an open face on the other slot
1065 if len(build_faces
[not switch
]) > 0:
1067 splitted_faces
.append(build_faces
[switch
])
1068 # reset actual faces and switch
1069 build_faces
[switch
] = []
1072 # continue previous face
1073 build_faces
[switch
].append(new_vert
)
1075 if len(build_faces
[not switch
]) == 2:
1076 build_faces
[not switch
].append(id0
)
1077 if len(build_faces
[not switch
]) > 2:
1078 splitted_faces
.append(build_faces
[not switch
])
1080 splitted_faces
.append(build_faces
[switch
])
1081 #del_faces.append(f.index)
1083 # adding new vertices
1084 _new_vert
= bm
.verts
.new
1085 for v
in verts
: new_vert
= _new_vert(v
)
1086 bm
.verts
.index_update()
1087 bm
.verts
.ensure_lookup_table()
1089 _new_face
= bm
.faces
.new
1092 for f
in splitted_faces
:
1094 face_verts
= [bm
.verts
[i
] for i
in f
]
1095 new_face
= _new_face(face_verts
)
1096 for e
in new_face
.edges
:
1097 filtered_edges
.append(e
)
1099 missed_faces
.append(f
)
1101 bm
.faces
.ensure_lookup_table()
1102 # updating weight values
1103 weight
= weight
+ [iso_val
]*len(verts
)
1105 # deleting old edges/faces
1106 _remove_edge
= bm
.edges
.remove
1107 bm
.edges
.ensure_lookup_table()
1108 for e
in delete_edges
:
1110 _filtered_edges
= []
1111 for e
in filtered_edges
:
1112 if e
not in delete_edges
: _filtered_edges
.append(e
)
1113 filtered_edges
= _filtered_edges
1115 name
= ob0
.name
+ '_ContourDisp'
1116 me
= bpy
.data
.meshes
.new(name
)
1119 ob
= bpy
.data
.objects
.new(name
, me
)
1121 # Link object to scene and make active
1123 context
.collection
.objects
.link(ob
)
1124 context
.view_layer
.objects
.active
= ob
1126 ob0
.select_set(False)
1128 # generate new vertex group
1129 for g
in ob0
.vertex_groups
:
1130 ob
.vertex_groups
.new(name
=g
.name
)
1131 #ob.vertex_groups.new(name=vertex_group_name)
1133 all_weight
= weight
+ [iso_val
]*len(verts
)
1134 #mult = 1/(1-iso_val)
1135 for id in range(len(all_weight
)):
1136 #if False: w = (all_weight[id]-iso_val)*mult
1138 if self
.weight_mode
== 'Alternate':
1139 direction
= self
.bool_flip
1140 for i
in range(len(iso_values
)-1):
1141 val0
, val1
= iso_values
[i
], iso_values
[i
+1]
1142 if val0
< w
<= val1
:
1143 if direction
: w1
= (w
-val0
)/(val1
-val0
)
1144 else: w1
= (val1
-w
)/(val1
-val0
)
1145 direction
= not direction
1146 if w
< iso_values
[0]: w1
= not self
.bool_flip
1147 if w
> iso_values
[-1]: w1
= not direction
1148 elif self
.weight_mode
== 'Remapped':
1149 if w
< min_iso
: w1
= 0
1150 elif w
> max_iso
: w1
= 1
1151 else: w1
= (w
- min_iso
)/delta_iso
1153 if self
.bool_flip
: w1
= 1-w
1155 ob
.vertex_groups
[vertex_group_name
].add([id], w1
, 'REPLACE')
1157 ob
.vertex_groups
.active_index
= group_id
1160 ob
.matrix_world
= ob0
.matrix_world
1163 if self
.bool_displace
:
1164 ob
.modifiers
.new(type='DISPLACE', name
='Displace')
1165 ob
.modifiers
["Displace"].mid_level
= 0
1166 ob
.modifiers
["Displace"].strength
= 0.1
1167 ob
.modifiers
['Displace'].vertex_group
= vertex_group_name
1169 bpy
.ops
.object.mode_set(mode
='EDIT')
1170 bpy
.ops
.object.mode_set(mode
='WEIGHT_PAINT')
1171 print("Contour Displace time: " + str(timeit
.default_timer() - start_time
) + " sec")
1173 bpy
.data
.meshes
.remove(me0
)
1177 class weight_contour_mask(Operator
):
1178 bl_idname
= "object.weight_contour_mask"
1179 bl_label
= "Contour Mask"
1180 bl_description
= ("")
1181 bl_options
= {'REGISTER', 'UNDO'}
1183 use_modifiers
: BoolProperty(
1184 name
="Use Modifiers", default
=True,
1185 description
="Apply all the modifiers")
1186 iso
: FloatProperty(
1187 name
="Iso Value", default
=0.5, soft_min
=0, soft_max
=1,
1188 description
="Threshold value")
1189 bool_solidify
: BoolProperty(
1190 name
="Solidify", default
=True, description
="Add Solidify Modifier")
1191 offset
: FloatProperty(
1192 name
="Offset", default
=1, min=0, max=1,
1193 description
="Offset")
1194 thickness
: FloatProperty(
1195 name
="Thickness", default
=0.5, soft_min
=0, soft_max
=1,
1196 description
="Thickness")
1197 normalize_weight
: BoolProperty(
1198 name
="Normalize Weight", default
=True,
1199 description
="Normalize weight of remaining vertices")
1202 def poll(cls
, context
):
1203 return len(context
.object.vertex_groups
) > 0
1205 def invoke(self
, context
, event
):
1206 return context
.window_manager
.invoke_props_dialog(self
, width
=350)
1208 def execute(self
, context
):
1209 start_time
= timeit
.default_timer()
1211 check
= context
.object.vertex_groups
[0]
1213 self
.report({'ERROR'}, "The object doesn't have Vertex Groups")
1214 return {'CANCELLED'}
1216 ob0
= bpy
.context
.object
1219 group_id
= ob0
.vertex_groups
.active_index
1220 vertex_group_name
= ob0
.vertex_groups
[group_id
].name
1222 bpy
.ops
.object.mode_set(mode
='EDIT')
1223 bpy
.ops
.mesh
.select_all(action
='SELECT')
1224 bpy
.ops
.object.mode_set(mode
='OBJECT')
1225 if self
.use_modifiers
:
1226 me0
= simple_to_mesh(ob0
)#ob0.to_mesh(preserve_all_data_layers=True, depsgraph=bpy.context.evaluated_depsgraph_get()).copy()
1228 me0
= ob0
.data
.copy()
1230 # generate new bmesh
1233 bm
.verts
.ensure_lookup_table()
1234 bm
.edges
.ensure_lookup_table()
1235 bm
.faces
.ensure_lookup_table()
1237 # store weight values
1239 ob
= bpy
.data
.objects
.new("temp", me0
)
1240 for g
in ob0
.vertex_groups
:
1241 ob
.vertex_groups
.new(name
=g
.name
)
1242 for v
in me0
.vertices
:
1244 #weight.append(v.groups[vertex_group_name].weight)
1245 weight
.append(ob
.vertex_groups
[vertex_group_name
].weight(v
.index
))
1257 if w
> w_max
: w_max
= w
1258 if w
< w_min
: w_min
= w
1259 if w_min
< iso_val
and w_max
> iso_val
:
1260 faces_mask
.append(f
)
1263 filtered_edges
= bm
.edges
# me0.edges
1264 faces_todo
= [f
.select
for f
in bm
.faces
]
1269 _filtered_edges
= []
1270 n_verts
= len(bm
.verts
)
1272 for e
in filtered_edges
:
1273 id0
= e
.verts
[0].index
1274 id1
= e
.verts
[1].index
1278 if w0
== w1
: continue
1279 elif w0
> iso_val
and w1
> iso_val
:
1281 elif w0
< iso_val
and w1
< iso_val
: continue
1282 elif w0
== iso_val
or w1
== iso_val
: continue
1284 v0
= me0
.vertices
[id0
].co
1285 v1
= me0
.vertices
[id1
].co
1286 v
= v0
.lerp(v1
, (iso_val
-w0
)/(w1
-w0
))
1287 delete_edges
.append(e
)
1289 edges_id
[str(id0
)+"_"+str(id1
)] = count
1290 edges_id
[str(id1
)+"_"+str(id0
)] = count
1297 for f
in faces_mask
:
1298 # create sub-faces slots. Once a new vertex is reached it will
1299 # change slot, storing the next vertices for a new face.
1300 build_faces
= [[],[]]
1302 verts0
= list(me0
.polygons
[f
.index
].vertices
)
1303 verts1
= list(verts0
)
1304 verts1
.append(verts1
.pop(0)) # shift list
1305 for id0
, id1
in zip(verts0
, verts1
):
1307 # add first vertex to active slot
1308 build_faces
[switch
].append(id0
)
1312 # check if the edge must be splitted
1313 new_vert
= edges_id
[str(id0
)+"_"+str(id1
)]
1315 build_faces
[switch
].append(new_vert
)
1316 # if there is an open face on the other slot
1317 if len(build_faces
[not switch
]) > 0:
1319 splitted_faces
.append(build_faces
[switch
])
1320 # reset actual faces and switch
1321 build_faces
[switch
] = []
1324 # continue previous face
1325 build_faces
[switch
].append(new_vert
)
1327 if len(build_faces
[not switch
]) == 2:
1328 build_faces
[not switch
].append(id0
)
1329 if len(build_faces
[not switch
]) > 2:
1330 splitted_faces
.append(build_faces
[not switch
])
1332 splitted_faces
.append(build_faces
[switch
])
1334 # adding new vertices
1335 _new_vert
= bm
.verts
.new
1336 for v
in verts
: _new_vert(v
)
1337 bm
.verts
.ensure_lookup_table()
1339 # deleting old edges/faces
1340 _remove_edge
= bm
.edges
.remove
1341 bm
.edges
.ensure_lookup_table()
1343 for e
in delete_edges
: _remove_edge(e
)
1345 bm
.verts
.ensure_lookup_table()
1347 _new_face
= bm
.faces
.new
1349 for f
in splitted_faces
:
1351 face_verts
= [bm
.verts
[i
] for i
in f
]
1352 _new_face(face_verts
)
1354 missed_faces
.append(f
)
1358 _remove_vert
= bm
.verts
.remove
1359 all_weight
= weight
+ [iso_val
+0.0001]*len(verts
)
1361 for w
, v
in zip(all_weight
, bm
.verts
):
1362 if w
< iso_val
: _remove_vert(v
)
1363 else: weight
.append(w
)
1365 # Create mesh and object
1366 name
= ob0
.name
+ '_ContourMask_{:.3f}'.format(iso_val
)
1367 me
= bpy
.data
.meshes
.new(name
)
1370 ob
= bpy
.data
.objects
.new(name
, me
)
1372 # Link object to scene and make active
1374 context
.collection
.objects
.link(ob
)
1375 context
.view_layer
.objects
.active
= ob
1377 ob0
.select_set(False)
1379 # generate new vertex group
1380 for g
in ob0
.vertex_groups
:
1381 ob
.vertex_groups
.new(name
=g
.name
)
1383 if iso_val
!= 1: mult
= 1/(1-iso_val
)
1385 for id in range(len(weight
)):
1386 if self
.normalize_weight
: w
= (weight
[id]-iso_val
)*mult
1387 else: w
= weight
[id]
1388 ob
.vertex_groups
[vertex_group_name
].add([id], w
, 'REPLACE')
1389 ob
.vertex_groups
.active_index
= group_id
1392 ob
.matrix_world
= ob0
.matrix_world
1395 if self
.bool_solidify
and True:
1396 ob
.modifiers
.new(type='SOLIDIFY', name
='Solidify')
1397 ob
.modifiers
['Solidify'].thickness
= self
.thickness
1398 ob
.modifiers
['Solidify'].offset
= self
.offset
1399 ob
.modifiers
['Solidify'].vertex_group
= vertex_group_name
1401 bpy
.ops
.object.mode_set(mode
='EDIT')
1402 bpy
.ops
.object.mode_set(mode
='WEIGHT_PAINT')
1403 print("Contour Mask time: " + str(timeit
.default_timer() - start_time
) + " sec")
1405 bpy
.data
.meshes
.remove(me0
)
1410 class weight_contour_mask_wip(Operator
):
1411 bl_idname
= "object.weight_contour_mask"
1412 bl_label
= "Contour Mask"
1413 bl_description
= ("")
1414 bl_options
= {'REGISTER', 'UNDO'}
1416 use_modifiers
: BoolProperty(
1417 name
="Use Modifiers", default
=True,
1418 description
="Apply all the modifiers")
1419 iso
: FloatProperty(
1420 name
="Iso Value", default
=0.5, soft_min
=0, soft_max
=1,
1421 description
="Threshold value")
1422 bool_solidify
: BoolProperty(
1423 name
="Solidify", default
=True, description
="Add Solidify Modifier")
1424 normalize_weight
: BoolProperty(
1425 name
="Normalize Weight", default
=True,
1426 description
="Normalize weight of remaining vertices")
1429 def poll(cls
, context
):
1430 return len(context
.object.vertex_groups
) > 0
1432 def execute(self
, context
):
1433 start_time
= timeit
.default_timer()
1435 check
= context
.object.vertex_groups
[0]
1437 self
.report({'ERROR'}, "The object doesn't have Vertex Groups")
1438 return {'CANCELLED'}
1440 ob0
= bpy
.context
.object
1443 group_id
= ob0
.vertex_groups
.active_index
1444 vertex_group_name
= ob0
.vertex_groups
[group_id
].name
1446 if self
.use_modifiers
:
1447 me0
= simple_to_mesh(ob0
)
1449 me0
= ob0
.data
.copy()
1451 # generate new bmesh
1455 # store weight values
1457 ob
= bpy
.data
.objects
.new("temp", me0
)
1458 for g
in ob0
.vertex_groups
:
1459 ob
.vertex_groups
.new(name
=g
.name
)
1460 weight
= get_weight_numpy(ob
.vertex_groups
[vertex_group_name
], len(me0
.vertices
))
1462 me0
, bm
, weight
= contour_bmesh(me0
, bm
, weight
, iso_val
)
1465 mask
= weight
>= iso_val
1466 weight
= weight
[mask
]
1467 mask
= np
.logical_not(mask
)
1468 delete_verts
= np
.array(bm
.verts
)[mask
]
1470 # Create mesh and object
1471 name
= ob0
.name
+ '_ContourMask_{:.3f}'.format(iso_val
)
1472 me
= bpy
.data
.meshes
.new(name
)
1475 ob
= bpy
.data
.objects
.new(name
, me
)
1477 # Link object to scene and make active
1479 context
.collection
.objects
.link(ob
)
1480 context
.view_layer
.objects
.active
= ob
1482 ob0
.select_set(False)
1484 # generate new vertex group
1485 for g
in ob0
.vertex_groups
:
1486 ob
.vertex_groups
.new(name
=g
.name
)
1488 if iso_val
!= 1: mult
= 1/(1-iso_val
)
1490 for id in range(len(weight
)):
1491 if self
.normalize_weight
: w
= (weight
[id]-iso_val
)*mult
1492 else: w
= weight
[id]
1493 ob
.vertex_groups
[vertex_group_name
].add([id], w
, 'REPLACE')
1494 ob
.vertex_groups
.active_index
= group_id
1497 ob
.matrix_world
= ob0
.matrix_world
1500 if self
.bool_solidify
and True:
1501 ob
.modifiers
.new(type='SOLIDIFY', name
='Solidify')
1502 ob
.modifiers
['Solidify'].thickness
= 0.05
1503 ob
.modifiers
['Solidify'].offset
= 0
1504 ob
.modifiers
['Solidify'].vertex_group
= vertex_group_name
1506 bpy
.ops
.object.mode_set(mode
='EDIT')
1507 bpy
.ops
.object.mode_set(mode
='WEIGHT_PAINT')
1508 print("Contour Mask time: " + str(timeit
.default_timer() - start_time
) + " sec")
1510 bpy
.data
.meshes
.remove(me0
)
1514 class vertex_colors_to_vertex_groups(Operator
):
1515 bl_idname
= "object.vertex_colors_to_vertex_groups"
1516 bl_label
= "Vertex Color"
1517 bl_options
= {'REGISTER', 'UNDO'}
1518 bl_description
= ("Convert the active Vertex Color into a Vertex Group.")
1521 name
="red channel", default
=False, description
="convert red channel")
1522 green
: BoolProperty(
1523 name
="green channel", default
=False,
1524 description
="convert green channel")
1525 blue
: BoolProperty(
1526 name
="blue channel", default
=False, description
="convert blue channel")
1527 value
: BoolProperty(
1528 name
="value channel", default
=True, description
="convert value channel")
1529 invert
: BoolProperty(
1530 name
="invert", default
=False, description
="invert all color channels")
1533 def poll(cls
, context
):
1535 return len(context
.object.data
.color_attributes
) > 0
1536 except: return False
1538 def execute(self
, context
):
1539 ob
= context
.active_object
1540 id = len(ob
.vertex_groups
)
1546 boolCol
= len(ob
.data
.color_attributes
)
1548 col
= ob
.data
.color_attributes
.active_color
1549 bpy
.ops
.object.mode_set(mode
='EDIT')
1550 bpy
.ops
.mesh
.select_all(action
='SELECT')
1552 if(self
.red
and boolCol
):
1553 bpy
.ops
.object.vertex_group_add()
1554 bpy
.ops
.object.vertex_group_assign()
1556 ob
.vertex_groups
[id_red
].name
= col
.name
+ '_red'
1558 if(self
.green
and boolCol
):
1559 bpy
.ops
.object.vertex_group_add()
1560 bpy
.ops
.object.vertex_group_assign()
1562 ob
.vertex_groups
[id_green
].name
= col
.name
+ '_green'
1564 if(self
.blue
and boolCol
):
1565 bpy
.ops
.object.vertex_group_add()
1566 bpy
.ops
.object.vertex_group_assign()
1568 ob
.vertex_groups
[id_blue
].name
= col
.name
+ '_blue'
1570 if(self
.value
and boolCol
):
1571 bpy
.ops
.object.vertex_group_add()
1572 bpy
.ops
.object.vertex_group_assign()
1574 ob
.vertex_groups
[id_value
].name
= col
.name
+ '_value'
1578 if(self
.invert
): mult
= -1
1579 bpy
.ops
.object.mode_set(mode
='OBJECT')
1580 sub_red
= 1 + self
.value
+ self
.blue
+ self
.green
1581 sub_green
= 1 + self
.value
+ self
.blue
1582 sub_blue
= 1 + self
.value
1585 id = len(ob
.vertex_groups
)
1586 if(id_red
<= id and id_green
<= id and id_blue
<= id and id_value
<= \
1588 v_colors
= ob
.data
.color_attributes
.active_color
.data
1591 if ob
.data
.color_attributes
.active_color
.domain
== 'POINT':
1592 for v
in ob
.data
.vertices
:
1594 if(self
.red
): gr
[min(len(gr
)-sub_red
, id_red
)].weight
= \
1595 self
.invert
+ mult
* v_colors
[i
].color
[0]
1596 if(self
.green
): gr
[min(len(gr
)-sub_green
, id_green
)].weight\
1597 = self
.invert
+ mult
* v_colors
[i
].color
[1]
1598 if(self
.blue
): gr
[min(len(gr
)-sub_blue
, id_blue
)].weight
= \
1599 self
.invert
+ mult
* v_colors
[i
].color
[2]
1601 r
= v_colors
[i
].color
[0]
1602 g
= v_colors
[i
].color
[1]
1603 b
= v_colors
[i
].color
[2]
1604 gr
[min(len(gr
)-sub_value
, id_value
)].weight\
1605 = self
.invert
+ mult
* (0.2126*r
+ 0.7152*g
+ 0.0722*b
)
1607 elif ob
.data
.color_attributes
.active_color
.domain
== 'CORNER':
1608 for f
in ob
.data
.polygons
:
1609 for v
in f
.vertices
:
1610 gr
= ob
.data
.vertices
[v
].groups
1611 if(self
.red
): gr
[min(len(gr
)-sub_red
, id_red
)].weight
= \
1612 self
.invert
+ mult
* v_colors
[i
].color
[0]
1613 if(self
.green
): gr
[min(len(gr
)-sub_green
, id_green
)].weight\
1614 = self
.invert
+ mult
* v_colors
[i
].color
[1]
1615 if(self
.blue
): gr
[min(len(gr
)-sub_blue
, id_blue
)].weight
= \
1616 self
.invert
+ mult
* v_colors
[i
].color
[2]
1618 r
= v_colors
[i
].color
[0]
1619 g
= v_colors
[i
].color
[1]
1620 b
= v_colors
[i
].color
[2]
1621 gr
[min(len(gr
)-sub_value
, id_value
)].weight\
1622 = self
.invert
+ mult
* (0.2126*r
+ 0.7152*g
+ 0.0722*b
)
1625 bpy
.ops
.paint
.weight_paint_toggle()
1628 class vertex_group_to_vertex_colors(Operator
):
1629 bl_idname
= "object.vertex_group_to_vertex_colors"
1630 bl_label
= "Vertex Group"
1631 bl_options
= {'REGISTER', 'UNDO'}
1632 bl_description
= ("Convert the active Vertex Group into a Vertex Color.")
1634 channel
: EnumProperty(
1635 items
=[('BLUE', 'Blue Channel', 'Convert to Blue Channel'),
1636 ('GREEN', 'Green Channel', 'Convert to Green Channel'),
1637 ('RED', 'Red Channel', 'Convert to Red Channel'),
1638 ('VALUE', 'Value Channel', 'Convert to Grayscale'),
1639 ('FALSE_COLORS', 'False Colors', 'Convert to False Colors')],
1640 name
="Convert to", description
="Choose how to convert vertex group",
1641 default
="VALUE", options
={'LIBRARY_EDITABLE'})
1643 invert
: BoolProperty(
1644 name
="invert", default
=False, description
="invert color channel")
1647 def poll(cls
, context
):
1648 return len(context
.object.vertex_groups
) > 0
1650 def execute(self
, context
):
1651 obj
= context
.active_object
1653 group_id
= obj
.vertex_groups
.active_index
1654 if (group_id
== -1):
1657 bpy
.ops
.object.mode_set(mode
='OBJECT')
1658 group_name
= obj
.vertex_groups
[group_id
].name
1659 bpy
.ops
.geometry
.color_attribute_add()
1660 active_color
= obj
.data
.color_attributes
.active_color
1662 colors_name
= group_name
1663 if(self
.channel
== 'FALSE_COLORS'): colors_name
+= "_false_colors"
1664 elif(self
.channel
== 'VALUE'): colors_name
+= "_value"
1665 elif(self
.channel
== 'RED'): colors_name
+= "_red"
1666 elif(self
.channel
== 'GREEN'): colors_name
+= "_green"
1667 elif(self
.channel
== 'BLUE'): colors_name
+= "_blue"
1668 active_color
.name
= colors_name
1670 v_colors
= obj
.data
.color_attributes
.active_color
.data
1674 dvert_lay
= bm
.verts
.layers
.deform
.active
1675 weight
= bmesh_get_weight_numpy(group_id
,dvert_lay
,bm
.verts
)
1676 if self
.invert
: weight
= 1-weight
1677 loops_size
= get_attribute_numpy(me
.polygons
, attribute
='loop_total', mult
=1)
1678 n_colors
= np
.sum(loops_size
)
1679 splitted_weight
= weight
[:,None]
1680 r
= np
.zeros(splitted_weight
.shape
)
1681 g
= np
.zeros(splitted_weight
.shape
)
1682 b
= np
.zeros(splitted_weight
.shape
)
1683 a
= np
.ones(splitted_weight
.shape
)
1684 if(self
.channel
== 'FALSE_COLORS'):
1685 mult
= 0.6+0.4*splitted_weight
1686 mask
= splitted_weight
< 0.25
1687 g
[mask
] = splitted_weight
[mask
]*4
1688 b
[mask
] = np
.ones(splitted_weight
.shape
)[mask
]
1690 mask
= np
.where(np
.logical_and(splitted_weight
>=0.25, splitted_weight
<0.5))
1691 g
[mask
] = np
.ones(splitted_weight
.shape
)[mask
]
1692 b
[mask
] = (1-(splitted_weight
[mask
]-0.25)*4)
1694 mask
= np
.where(np
.logical_and(splitted_weight
>=0.5, splitted_weight
<0.75))
1695 r
[mask
] = (splitted_weight
[mask
]-0.5)*4
1696 g
[mask
] = np
.ones(splitted_weight
.shape
)[mask
]
1698 mask
= 0.75 <= splitted_weight
1699 r
[mask
] = np
.ones(splitted_weight
.shape
)[mask
]
1700 g
[mask
] = (1-(splitted_weight
[mask
]-0.75)*4)
1701 elif(self
.channel
== 'VALUE'):
1705 elif(self
.channel
== 'RED'):
1707 elif(self
.channel
== 'GREEN'):
1709 elif(self
.channel
== 'BLUE'):
1712 colors
= np
.concatenate((r
,g
,b
,a
),axis
=1).flatten()
1713 v_colors
.foreach_set('color',colors
)
1715 bpy
.ops
.paint
.vertex_paint_toggle()
1716 bpy
.ops
.geometry
.color_attribute_render_set(name
=active_color
.name
)
1719 class vertex_group_to_uv(Operator
):
1720 bl_idname
= "object.vertex_group_to_uv"
1721 bl_label
= "Vertex Group"
1722 bl_options
= {'REGISTER', 'UNDO'}
1723 bl_description
= ("Combine two Vertex Groups as UV Map Layer.")
1725 vertex_group_u
: StringProperty(
1726 name
="U", default
='',
1727 description
="Vertex Group used for the U coordinate")
1728 vertex_group_v
: StringProperty(
1729 name
="V", default
='',
1730 description
="Vertex Group used for the V coordinate")
1731 normalize_weight
: BoolProperty(
1732 name
="Normalize Weight", default
=True,
1733 description
="Normalize weight values")
1734 invert_u
: BoolProperty(
1735 name
="Invert U", default
=False, description
="Invert U")
1736 invert_v
: BoolProperty(
1737 name
="Invert V", default
=False, description
="Invert V")
1740 def poll(cls
, context
):
1741 return len(context
.object.vertex_groups
) > 0
1743 def invoke(self
, context
, event
):
1744 return context
.window_manager
.invoke_props_dialog(self
, width
=250)
1746 def draw(self
, context
):
1748 layout
= self
.layout
1749 col
= layout
.column(align
=True)
1750 row
= col
.row(align
=True)
1751 row
.prop_search(self
, 'vertex_group_u', ob
, "vertex_groups", text
='')
1753 row
.prop_search(self
, 'vertex_group_v', ob
, "vertex_groups", text
='')
1754 row
= col
.row(align
=True)
1755 row
.prop(self
, "invert_u")
1757 row
.prop(self
, "invert_v")
1758 row
= col
.row(align
=True)
1759 row
.prop(self
, "normalize_weight")
1761 def execute(self
, context
):
1762 ob
= context
.active_object
1764 n_verts
= len(me
.vertices
)
1765 vg_keys
= ob
.vertex_groups
.keys()
1766 bool_u
= self
.vertex_group_u
in vg_keys
1767 bool_v
= self
.vertex_group_v
in vg_keys
1768 if bool_u
or bool_v
:
1771 dvert_lay
= bm
.verts
.layers
.deform
.active
1773 u_index
= ob
.vertex_groups
[self
.vertex_group_u
].index
1774 u
= bmesh_get_weight_numpy(u_index
, dvert_lay
, bm
.verts
)
1777 if self
.normalize_weight
:
1778 u
= np
.interp(u
, (u
.min(), u
.max()), (0, 1))
1780 u
= np
.zeros(n_verts
)
1782 v_index
= ob
.vertex_groups
[self
.vertex_group_v
].index
1783 v
= bmesh_get_weight_numpy(v_index
, dvert_lay
, bm
.verts
)
1786 if self
.normalize_weight
:
1787 v
= np
.interp(v
, (v
.min(), v
.max()), (0, 1))
1789 v
= np
.zeros(n_verts
)
1791 u
= v
= np
.zeros(n_verts
)
1793 uv_layer
= me
.uv_layers
.new(name
='Weight_to_UV')
1794 loops_size
= get_attribute_numpy(me
.polygons
, attribute
='loop_total', mult
=1)
1795 n_data
= np
.sum(loops_size
)
1796 v_id
= np
.ones(n_data
)
1797 me
.polygons
.foreach_get('vertices',v_id
)
1798 v_id
= v_id
.astype(int)
1799 split_u
= u
[v_id
,None]
1800 split_v
= v
[v_id
,None]
1801 uv
= np
.concatenate((split_u
,split_v
),axis
=1).flatten()
1802 uv_layer
.data
.foreach_set('uv',uv
)
1803 me
.uv_layers
.update()
1806 class curvature_to_vertex_groups(Operator
):
1807 bl_idname
= "object.curvature_to_vertex_groups"
1808 bl_label
= "Curvature"
1809 bl_options
= {'REGISTER', 'UNDO'}
1810 bl_description
= ("Generate a Vertex Group based on the curvature of the"
1811 "mesh. Is based on Dirty Vertex Color.")
1813 blur_strength
: FloatProperty(
1814 name
="Blur Strength", default
=1, min=0.001,
1815 max=1, description
="Blur strength per iteration")
1817 blur_iterations
: IntProperty(
1818 name
="Blur Iterations", default
=1, min=0,
1819 max=40, description
="Number of times to blur the values")
1821 angle
: FloatProperty(
1822 name
="Angle", default
=5*pi
/90, min=0,
1823 max=pi
/2, subtype
='ANGLE', description
="Angle")
1825 invert
: BoolProperty(
1826 name
="Invert", default
=False,
1827 description
="Invert the curvature map")
1829 absolute
: BoolProperty(
1830 name
="Absolute", default
=False, description
="Absolute values")
1832 def execute(self
, context
):
1833 bpy
.ops
.object.mode_set(mode
='OBJECT')
1834 bpy
.ops
.geometry
.color_attribute_add(domain
='CORNER', color
= (1,1,1,1))
1835 color_attributes
= context
.active_object
.data
.color_attributes
1836 color_attributes
.active
= color_attributes
[-1]
1837 color_attributes
.active_color
= color_attributes
[-1]
1838 color_attributes
[-1].name
= "Curvature"
1839 bpy
.ops
.geometry
.color_attribute_render_set(name
=color_attributes
[-1].name
)
1840 bpy
.ops
.object.mode_set(mode
='VERTEX_PAINT')
1841 bpy
.ops
.paint
.vertex_color_dirt(
1842 blur_strength
=self
.blur_strength
,
1843 blur_iterations
=self
.blur_iterations
,
1844 clean_angle
=pi
/2 + self
.angle
,
1845 dirt_angle
=pi
/2 - self
.angle
,
1847 bpy
.ops
.object.vertex_colors_to_vertex_groups(invert
=self
.invert
)
1850 weight
= get_weight_numpy(ob
.vertex_groups
[-1], len(ob
.data
.vertices
))
1851 weight
= np
.abs(0.5-weight
)*2
1853 bm
.from_mesh(ob
.data
)
1854 bmesh_set_weight_numpy(bm
,len(ob
.vertex_groups
)-1,weight
)
1856 ob
.vertex_groups
.update()
1858 #bpy.ops.geometry.color_attribute_remove()
1861 class face_area_to_vertex_groups(Operator
):
1862 bl_idname
= "object.face_area_to_vertex_groups"
1864 bl_options
= {'REGISTER', 'UNDO'}
1865 bl_description
= ("Generate a Vertex Group based on the area of individual"
1868 invert
: BoolProperty(
1869 name
="invert", default
=False, description
="invert values")
1870 bounds
: EnumProperty(
1871 items
=(('MANUAL', "Manual Bounds", ""),
1872 ('AUTOMATIC', "Automatic Bounds", "")),
1873 default
='AUTOMATIC', name
="Bounds")
1875 min_area
: FloatProperty(
1876 name
="Min", default
=0.01, soft_min
=0, soft_max
=1,
1877 description
="Faces with 0 weight")
1879 max_area
: FloatProperty(
1880 name
="Max", default
=0.1, soft_min
=0, soft_max
=1,
1881 description
="Faces with 1 weight")
1883 def draw(self
, context
):
1884 layout
= self
.layout
1885 layout
.label(text
="Bounds")
1886 layout
.prop(self
, "bounds", text
="")
1887 if self
.bounds
== 'MANUAL':
1888 layout
.prop(self
, "min_area")
1889 layout
.prop(self
, "max_area")
1891 def execute(self
, context
):
1892 try: ob
= context
.object
1894 self
.report({'ERROR'}, "Please select an Object")
1895 return {'CANCELLED'}
1896 ob
.vertex_groups
.new(name
="Faces Area")
1898 areas
= [[] for v
in ob
.data
.vertices
]
1900 for p
in ob
.data
.polygons
:
1901 for v
in p
.vertices
:
1902 areas
[v
].append(p
.area
)
1904 for i
in range(len(areas
)):
1905 areas
[i
] = mean(areas
[i
])
1906 if self
.bounds
== 'MANUAL':
1907 min_area
= self
.min_area
1908 max_area
= self
.max_area
1909 elif self
.bounds
== 'AUTOMATIC':
1910 min_area
= min(areas
)
1911 max_area
= max(areas
)
1912 elif self
.bounds
== 'COMPRESSION':
1914 max_area
= min(areas
)
1915 elif self
.bounds
== 'TENSION':
1917 max_area
= max(areas
)
1918 delta_area
= max_area
- min_area
1921 if self
.bounds
== 'MANUAL':
1924 self
.report({'ERROR'}, "The faces have the same areas")
1925 #return {'CANCELLED'}
1926 for i
in range(len(areas
)):
1927 weight
= (areas
[i
] - min_area
)/delta_area
1928 ob
.vertex_groups
[-1].add([i
], weight
, 'REPLACE')
1929 ob
.vertex_groups
.update()
1931 bpy
.ops
.object.mode_set(mode
='WEIGHT_PAINT')
1934 class random_weight(Operator
):
1935 bl_idname
= "object.random_weight"
1937 bl_options
= {'REGISTER', 'UNDO'}
1938 bl_description
= ("Generate a random Vertex Group")
1940 min_val
: FloatProperty(
1941 name
="Min", default
=0, soft_min
=0, soft_max
=1,
1942 description
="Minimum Value")
1944 max_val
: FloatProperty(
1945 name
="Max", default
=1, soft_min
=0, soft_max
=1,
1946 description
="Maximum Value")
1948 #def draw(self, context):
1949 # layout = self.layout
1950 # layout.prop(self, "min_area")
1951 # layout.prop(self, "max_area")
1953 def poll(cls
, context
):
1954 return len(context
.object.vertex_groups
) > 0
1956 def execute(self
, context
):
1957 try: ob
= context
.object
1959 self
.report({'ERROR'}, "Please select an Object")
1960 return {'CANCELLED'}
1961 #ob.vertex_groups.new(name="Random")
1962 n_verts
= len(ob
.data
.vertices
)
1963 weight
= np
.random
.uniform(low
=self
.min_val
, high
=self
.max_val
, size
=(n_verts
,))
1964 np
.clip(weight
, 0, 1, out
=weight
)
1966 group_id
= ob
.vertex_groups
.active_index
1967 for i
in range(n_verts
):
1968 ob
.vertex_groups
[group_id
].add([i
], weight
[i
], 'REPLACE')
1969 ob
.vertex_groups
.update()
1971 bpy
.ops
.object.mode_set(mode
='WEIGHT_PAINT')
1975 class harmonic_weight(Operator
):
1976 bl_idname
= "object.harmonic_weight"
1977 bl_label
= "Harmonic"
1978 bl_options
= {'REGISTER', 'UNDO'}
1979 bl_description
= ("Create an harmonic variation of the active Vertex Group")
1981 freq
: FloatProperty(
1982 name
="Frequency", default
=20, soft_min
=0,
1983 soft_max
=100, description
="Wave frequency")
1985 amp
: FloatProperty(
1986 name
="Amplitude", default
=1, soft_min
=0,
1987 soft_max
=10, description
="Wave amplitude")
1989 midlevel
: FloatProperty(
1990 name
="Midlevel", default
=0, min=-1,
1991 max=1, description
="Midlevel")
1993 add
: FloatProperty(
1994 name
="Add", default
=0, min=-1,
1995 max=1, description
="Add to the Weight")
1997 mult
: FloatProperty(
1998 name
="Multiply", default
=0, min=0,
1999 max=1, description
="Multiply for he Weight")
2002 def poll(cls
, context
):
2003 return len(context
.object.vertex_groups
) > 0
2005 def execute(self
, context
):
2006 ob
= context
.active_object
2007 if len(ob
.vertex_groups
) > 0:
2008 group_id
= ob
.vertex_groups
.active_index
2009 ob
.vertex_groups
.new(name
="Harmonic")
2010 for i
in range(len(ob
.data
.vertices
)):
2011 try: val
= ob
.vertex_groups
[group_id
].weight(i
)
2013 weight
= self
.amp
*(math
.sin(val
*self
.freq
) - self
.midlevel
)/2 + 0.5 + self
.add
*val
*(1-(1-val
)*self
.mult
)
2014 ob
.vertex_groups
[-1].add([i
], weight
, 'REPLACE')
2017 self
.report({'ERROR'}, "Active object doesn't have vertex groups")
2018 return {'CANCELLED'}
2019 bpy
.ops
.object.mode_set(mode
='WEIGHT_PAINT')
2023 class tissue_weight_distance(Operator
):
2024 bl_idname
= "object.tissue_weight_distance"
2025 bl_label
= "Weight Distance"
2026 bl_options
= {'REGISTER', 'UNDO'}
2027 bl_description
= ("Create a weight map according to the distance from the "
2028 "selected vertices along the mesh surface")
2030 mode
: EnumProperty(
2031 items
=(('GEOD', "Geodesic Distance", ""),
2032 ('EUCL', "Euclidean Distance", ""),
2033 ('TOPO', "Topology Distance", "")),
2034 default
='GEOD', name
="Distance Method")
2036 normalize
: BoolProperty(
2037 name
="Normalize", default
=True,
2038 description
="Automatically remap the distance values from 0 to 1")
2040 min_value
: FloatProperty(
2041 name
="Min", default
=0, min=0,
2042 soft_max
=100, description
="Minimum Distance")
2044 max_value
: FloatProperty(
2045 name
="Max", default
=10, min=0,
2046 soft_max
=100, description
="Max Distance")
2048 def invoke(self
, context
, event
):
2049 return context
.window_manager
.invoke_props_dialog(self
, width
=250)
2051 def fill_neighbors(self
,verts
,weight
):
2054 for f
in v0
.link_faces
:
2056 if self
.mode
== 'GEOD':
2057 dist
= weight
[v0
.index
] + (v0
.co
-v1
.co
).length
2058 elif self
.mode
== 'TOPO':
2059 dist
= weight
[v0
.index
] + 1.0
2060 w1
= weight
[v1
.index
]
2061 if w1
== None or w1
> dist
:
2062 weight
[v1
.index
] = dist
2064 if len(neigh
) == 0: return weight
2065 else: return self
.fill_neighbors(neigh
.keys(), weight
)
2067 def execute(self
, context
):
2070 if old_mode
!= 'OBJECT':
2071 bpy
.ops
.object.mode_set(mode
='OBJECT')
2075 # store weight values
2076 weight
= [None]*len(me
.vertices
)
2078 if self
.mode
!= 'EUCL':
2081 bm
.verts
.ensure_lookup_table()
2082 bm
.edges
.ensure_lookup_table()
2083 bm
.faces
.ensure_lookup_table()
2084 selected
= [v
for v
in bm
.verts
if v
.select
]
2085 if len(selected
) == 0:
2086 bpy
.ops
.object.mode_set(mode
=old_mode
)
2087 message
= "Please, select one or more vertices"
2088 self
.report({'ERROR'}, message
)
2089 return {'CANCELLED'}
2090 for v
in selected
: weight
[v
.index
] = 0
2091 weight
= self
.fill_neighbors(selected
, weight
)
2094 selected
= [v
for v
in me
.vertices
if v
.select
]
2095 kd
= KDTree(len(selected
))
2096 for i
, v
in enumerate(selected
):
2099 for i
,v
in enumerate(me
.vertices
):
2100 co
, index
, dist
= kd
.find(v
.co
)
2104 for i
in range(len(weight
)):
2105 if weight
[i
] == None: weight
[i
] = 0
2106 weight
= np
.array(weight
)
2107 max_dist
= np
.max(weight
)
2112 delta_value
= self
.max_value
- self
.min_value
2113 if delta_value
== 0: delta_value
= 0.0000001
2114 weight
= (weight
-self
.min_value
)/delta_value
2116 if self
.mode
== 'TOPO':
2117 vg
= ob
.vertex_groups
.new(name
='Distance: {:d}'.format(int(max_dist
)))
2119 vg
= ob
.vertex_groups
.new(name
='Distance: {:.4f}'.format(max_dist
))
2120 for i
, w
in enumerate(weight
):
2121 vg
.add([i
], w
, 'REPLACE')
2122 bpy
.ops
.object.mode_set(mode
='WEIGHT_PAINT')
2125 class TISSUE_PT_color(Panel
):
2126 bl_label
= "Tissue Tools"
2127 bl_category
= "Tissue"
2128 bl_space_type
= "VIEW_3D"
2129 bl_region_type
= "UI"
2130 #bl_options = {'DEFAULT_CLOSED'}
2131 bl_context
= "vertexpaint"
2133 def draw(self
, context
):
2134 layout
= self
.layout
2135 col
= layout
.column(align
=True)
2136 col
.operator("object.vertex_colors_to_vertex_groups",
2137 icon
="GROUP_VERTEX", text
="Convert to Weight")
2139 class TISSUE_PT_weight(Panel
):
2140 bl_label
= "Tissue Tools"
2141 bl_category
= "Tissue"
2142 bl_space_type
= "VIEW_3D"
2143 bl_region_type
= "UI"
2144 #bl_options = {'DEFAULT_CLOSED'}
2145 bl_context
= "weightpaint"
2147 def draw(self
, context
):
2148 layout
= self
.layout
2149 col
= layout
.column(align
=True)
2150 #if context.object.type == 'MESH' and context.mode == 'OBJECT':
2151 #col.label(text="Transform:")
2153 #elif bpy.context.mode == 'PAINT_WEIGHT':
2154 col
.label(text
="Weight Generate:")
2156 # "object.vertex_colors_to_vertex_groups", icon="GROUP_VCOL")
2157 col
.operator("object.face_area_to_vertex_groups", icon
="FACESEL")
2158 col
.operator("object.curvature_to_vertex_groups", icon
="SMOOTHCURVE")
2159 col
.operator("object.tissue_weight_distance", icon
="TRACKING")
2160 row
= col
.row(align
=True)
2161 try: row
.operator("object.weight_formula", icon
="CON_TRANSFORM")
2162 except: row
.operator("object.weight_formula")#, icon="CON_TRANSFORM")
2163 row
.operator("object.update_weight_formula", icon
="FILE_REFRESH", text
='')#, icon="CON_TRANSFORM")
2164 #col.label(text="Weight Processing:")
2168 col
.operator("object.weight_laplacian", icon
="SMOOTHCURVE")
2170 col
.label(text
="Weight Edit:")
2171 col
.operator("object.harmonic_weight", icon
="IPO_ELASTIC")
2172 col
.operator("object.random_weight", icon
="RNDCURVE")
2174 col
.label(text
="Deformation Analysis:")
2175 col
.operator("object.edges_deformation", icon
="DRIVER_DISTANCE")#FULLSCREEN_ENTER")
2176 col
.operator("object.edges_bending", icon
="DRIVER_ROTATIONAL_DIFFERENCE")#"MOD_SIMPLEDEFORM")
2178 col
.label(text
="Weight Curves:")
2179 #col.operator("object.weight_contour_curves", icon="MOD_CURVE")
2180 col
.operator("object.tissue_weight_streamlines", icon
="ANIM")
2181 op
= col
.operator("object.tissue_weight_contour_curves_pattern", icon
="FORCE_TURBULENCE")
2182 op
.contour_mode
= 'WEIGHT'
2184 col
.operator("object.weight_contour_displace", icon
="MOD_DISPLACE")
2185 col
.operator("object.weight_contour_mask", icon
="MOD_MASK")
2187 col
.label(text
="Simulations:")
2188 col
.operator("object.start_reaction_diffusion",
2189 icon
="EXPERIMENTAL",
2190 text
="Reaction-Diffusion")
2192 col
.label(text
="Materials:")
2193 col
.operator("object.random_materials", icon
='COLOR')
2194 col
.operator("object.weight_to_materials", icon
='GROUP_VERTEX')
2196 col
.label(text
="Weight Convert:")
2197 col
.operator("object.vertex_group_to_vertex_colors", icon
="GROUP_VCOL",
2198 text
="Convert to Colors")
2199 col
.operator("object.vertex_group_to_uv", icon
="UV",
2200 text
="Convert to UV")
2202 def contour_bmesh(me
, bm
, weight
, iso_val
):
2203 bm
.verts
.ensure_lookup_table()
2204 bm
.edges
.ensure_lookup_table()
2205 bm
.faces
.ensure_lookup_table()
2207 vertices
= get_vertices_numpy(me
)
2208 faces_mask
= np
.array(bm
.faces
)
2209 filtered_edges
= get_edges_id_numpy(me
)
2210 n_verts
= len(bm
.verts
)
2212 #############################
2215 id0
= filtered_edges
[:,0]
2216 id1
= filtered_edges
[:,1]
2221 bool_w0
= w0
< iso_val
2222 bool_w1
= w1
< iso_val
2224 # mask all edges that have one weight value below the iso value
2225 mask_new_verts
= np
.logical_xor(bool_w0
, bool_w1
)
2226 if not mask_new_verts
.any(): return np
.array([[None]]), {}, np
.array([[None]])
2228 id0
= id0
[mask_new_verts
]
2229 id1
= id1
[mask_new_verts
]
2233 w0
= w0
[mask_new_verts
]
2234 w1
= w1
[mask_new_verts
]
2235 param
= (iso_val
-w0
)/(w1
-w0
)
2236 param
= np
.expand_dims(param
,axis
=1)
2237 verts
= v0
+ (v1
-v0
)*param
2240 for i
, e
in enumerate(filtered_edges
):
2241 #edges_id[id] = i + n_verts
2242 edges_id
['{}_{}'.format(e
[0],e
[1])] = i
+ n_verts
2243 edges_id
['{}_{}'.format(e
[1],e
[0])] = i
+ n_verts
2249 for f
in faces_mask
:
2250 # create sub-faces slots. Once a new vertex is reached it will
2251 # change slot, storing the next vertices for a new face.
2252 build_faces
= [[],[]]
2254 verts0
= list(me
.polygons
[f
.index
].vertices
)
2255 verts1
= list(verts0
)
2256 verts1
.append(verts1
.pop(0)) # shift list
2257 for id0
, id1
in zip(verts0
, verts1
):
2259 # add first vertex to active slot
2260 build_faces
[switch
].append(id0
)
2264 # check if the edge must be splitted
2265 new_vert
= edges_id
['{}_{}'.format(id0
,id1
)]
2267 build_faces
[switch
].append(new_vert
)
2268 # if there is an open face on the other slot
2269 if len(build_faces
[not switch
]) > 0:
2271 splitted_faces
.append(build_faces
[switch
])
2272 # reset actual faces and switch
2273 build_faces
[switch
] = []
2276 # continue previous face
2277 build_faces
[switch
].append(new_vert
)
2279 if len(build_faces
[not switch
]) == 2:
2280 build_faces
[not switch
].append(id0
)
2281 if len(build_faces
[not switch
]) > 2:
2282 splitted_faces
.append(build_faces
[not switch
])
2284 splitted_faces
.append(build_faces
[switch
])
2286 # adding new vertices use fast local method access
2287 _new_vert
= bm
.verts
.new
2288 for v
in verts
: _new_vert(v
)
2289 bm
.verts
.ensure_lookup_table()
2291 # deleting old edges/faces
2292 bm
.edges
.ensure_lookup_table()
2293 remove_edges
= [bm
.edges
[i
] for i
in filtered_edges
[:,2]]
2294 #for e in remove_edges: bm.edges.remove(e)
2295 #for e in delete_edges: bm.edges.remove(e)
2297 bm
.verts
.ensure_lookup_table()
2298 # adding new faces use fast local method access
2299 _new_face
= bm
.faces
.new
2301 for f
in splitted_faces
:
2303 face_verts
= [bm
.verts
[i
] for i
in f
]
2304 _new_face(face_verts
)
2306 missed_faces
.append(f
)
2308 #me = bpy.data.meshes.new('_tissue_tmp_')
2310 weight
= np
.concatenate((weight
, np
.ones(len(verts
))*iso_val
))
2312 return me
, bm
, weight
2317 class tissue_weight_streamlines(Operator
):
2318 bl_idname
= "object.tissue_weight_streamlines"
2319 bl_label
= "Streamlines Curves"
2320 bl_description
= ("")
2321 bl_options
= {'REGISTER', 'UNDO'}
2323 mode
: EnumProperty(
2325 ('VERTS', "Verts", "Follow vertices"),
2326 ('EDGES', "Edges", "Follow Edges")
2329 name
="Streamlines path mode"
2332 interpolation
: EnumProperty(
2334 ('POLY', "Poly", "Generate Polylines"),
2335 ('NURBS', "NURBS", "Generate Nurbs curves")
2338 name
="Interpolation mode"
2341 use_modifiers
: BoolProperty(
2342 name
="Use Modifiers", default
=True,
2343 description
="Apply all the modifiers")
2345 use_selected
: BoolProperty(
2346 name
="Use Selected Vertices", default
=False,
2347 description
="Use selected vertices as Seed")
2349 same_weight
: BoolProperty(
2350 name
="Same Weight", default
=True,
2351 description
="Continue the streamlines when the weight is the same")
2353 min_iso
: FloatProperty(
2354 name
="Min Value", default
=0., soft_min
=0, soft_max
=1,
2355 description
="Minimum weight value")
2356 max_iso
: FloatProperty(
2357 name
="Max Value", default
=1, soft_min
=0, soft_max
=1,
2358 description
="Maximum weight value")
2360 rand_seed
: IntProperty(
2361 name
="Seed", default
=0, min=0, soft_max
=10,
2362 description
="Random Seed")
2363 n_curves
: IntProperty(
2364 name
="Curves", default
=50, soft_min
=1, soft_max
=100000,
2365 description
="Number of Curves")
2369 pos_steps
: IntProperty(
2370 name
="High Steps", default
=50, min=0, soft_max
=100,
2371 description
="Number of steps in the direction of high weight")
2372 neg_steps
: IntProperty(
2373 name
="Low Steps", default
=50, min=0, soft_max
=100,
2374 description
="Number of steps in the direction of low weight")
2376 bevel_depth
: FloatProperty(
2377 name
="Bevel Depth", default
=0, min=0, soft_max
=1,
2379 min_bevel_depth
: FloatProperty(
2380 name
="Min Bevel Depth", default
=0.1, min=0, soft_max
=1,
2382 max_bevel_depth
: FloatProperty(
2383 name
="Max Bevel Depth", default
=1, min=0, soft_max
=1,
2386 rand_dir
: FloatProperty(
2387 name
="Randomize", default
=0, min=0, max=1,
2388 description
="Randomize streamlines directions (Slower)")
2390 vertex_group_seeds
: StringProperty(
2391 name
="Displace", default
='',
2392 description
="Vertex Group used for pattern displace")
2394 vertex_group_bevel
: StringProperty(
2395 name
="Bevel", default
='',
2396 description
="Variable Bevel depth")
2398 object_name
: StringProperty(
2399 name
="Active Object", default
='',
2402 try: vg_name
= bpy
.context
.object.vertex_groups
.active
.name
2403 except: vg_name
= ''
2405 vertex_group_streamlines
: StringProperty(
2406 name
="Flow", default
=vg_name
,
2407 description
="Vertex Group used for streamlines")
2410 def poll(cls
, context
):
2412 return ob
and len(ob
.vertex_groups
) > 0 or ob
.type == 'CURVE'
2414 def invoke(self
, context
, event
):
2415 return context
.window_manager
.invoke_props_dialog(self
, width
=250)
2417 def draw(self
, context
):
2418 if not context
.object.type == 'CURVE':
2419 self
.object_name
= context
.object.name
2420 ob
= bpy
.data
.objects
[self
.object_name
]
2421 if self
.vertex_group_streamlines
not in [vg
.name
for vg
in ob
.vertex_groups
]:
2422 self
.vertex_group_streamlines
= ob
.vertex_groups
.active
.name
2423 layout
= self
.layout
2424 col
= layout
.column(align
=True)
2425 row
= col
.row(align
=True)
2426 row
.prop(self
, 'mode', expand
=True,
2427 slider
=True, toggle
=False, icon_only
=False, event
=False,
2428 full_event
=False, emboss
=True, index
=-1)
2429 col
.prop(self
, "use_modifiers")
2430 col
.label(text
="Streamlines Curves:")
2431 row
= col
.row(align
=True)
2432 row
.prop(self
, 'interpolation', expand
=True,
2433 slider
=True, toggle
=False, icon_only
=False, event
=False,
2434 full_event
=False, emboss
=True, index
=-1)
2436 col
.prop_search(self
, 'vertex_group_streamlines', ob
, "vertex_groups", text
='')
2437 if not (self
.use_selected
or context
.mode
== 'EDIT_MESH'):
2438 row
= col
.row(align
=True)
2439 row
.prop(self
,'n_curves')
2440 #row.enabled = context.mode != 'EDIT_MESH'
2441 row
= col
.row(align
=True)
2442 row
.prop(self
,'rand_seed')
2443 #row.enabled = context.mode != 'EDIT_MESH'
2444 row
= col
.row(align
=True)
2445 row
.prop(self
,'neg_steps')
2446 row
.prop(self
,'pos_steps')
2447 #row = col.row(align=True)
2448 #row.prop(self,'min_iso')
2449 #row.prop(self,'max_iso')
2450 col
.prop(self
, "same_weight")
2452 col
.label(text
='Curves Bevel:')
2453 col
.prop_search(self
, 'vertex_group_bevel', ob
, "vertex_groups", text
='')
2454 if self
.vertex_group_bevel
!= '':
2455 row
= col
.row(align
=True)
2456 row
.prop(self
,'min_bevel_depth')
2457 row
.prop(self
,'max_bevel_depth')
2459 col
.prop(self
,'bevel_depth')
2461 col
.prop(self
, "rand_dir")
2463 def execute(self
, context
):
2464 start_time
= timeit
.default_timer()
2466 check
= context
.object.vertex_groups
[0]
2468 self
.report({'ERROR'}, "The object doesn't have Vertex Groups")
2469 return {'CANCELLED'}
2470 ob
= bpy
.data
.objects
[self
.object_name
]
2471 ob
.select_set(False)
2476 if bpy
.context
.mode
== 'EDIT_MESH':
2477 self
.use_selected
= True
2478 bpy
.ops
.object.mode_set(mode
='OBJECT')
2479 #ob = bpy.context.object
2480 #me = simple_to_mesh(ob)
2481 ob
= convert_object_to_mesh(ob
, apply_modifiers
=self
.use_modifiers
)
2482 #dg = context.evaluated_depsgraph_get()
2483 #ob = ob.evaluated_get(dg)
2486 if self
.use_selected
:
2487 # generate new bmesh
2490 #for v in me.vertices:
2491 # if v.select: seeds.append(v.index)
2493 if v
.select
: seeds
.append(v
.index
)
2495 n_verts
= len(me
.vertices
)
2496 n_edges
= len(me
.edges
)
2497 n_faces
= len(me
.polygons
)
2499 # store weight values
2501 weight
= get_weight_numpy(ob
.vertex_groups
[self
.vertex_group_streamlines
], n_verts
)
2503 bpy
.data
.objects
.remove(ob
)
2504 self
.report({'ERROR'}, "Please select a Vertex Group for streamlines")
2505 return {'CANCELLED'}
2507 variable_bevel
= False
2509 bevel_depth
= self
.bevel_depth
2511 if self
.min_bevel_depth
== self
.max_bevel_depth
:
2512 #bevel_weight = np.ones((n_verts))
2513 bevel_depth
= self
.min_bevel_depth
2515 b0
= min(self
.min_bevel_depth
, self
.max_bevel_depth
)
2516 b1
= max(self
.min_bevel_depth
, self
.max_bevel_depth
)
2517 bevel_weight
= get_weight_numpy(ob
.vertex_groups
[self
.vertex_group_bevel
], n_verts
)
2518 if self
.min_bevel_depth
> self
.max_bevel_depth
:
2519 bevel_weight
= 1-bevel_weight
2520 bevel_weight
= b0
/b1
+ bevel_weight
*((b1
-b0
)/b1
)
2522 variable_bevel
= True
2524 pass#bevel_weight = np.ones((n_verts))
2528 np
.random
.seed(self
.rand_seed
)
2529 seeds
= np
.random
.randint(n_verts
, size
=self
.n_curves
)
2531 #weight = np.array(get_weight(ob.vertex_groups.active, n_verts))
2536 neigh
= [[] for i
in range(n_verts
)]
2537 if self
.mode
== 'EDGES':
2541 neigh
[ev
[0]].append(ev
[1])
2542 neigh
[ev
[1]].append(ev
[0])
2544 elif self
.mode
== 'VERTS':
2546 for p
in me
.polygons
:
2547 face_verts
= [v
for v
in p
.vertices
]
2548 n_face_verts
= len(face_verts
)
2549 for i
in range(n_face_verts
):
2550 fv
= face_verts
.copy()
2551 neigh
[fv
.pop(i
)] += fv
2553 neigh_weight
= [weight
[n
].tolist() for n
in neigh
]
2555 # evaluate direction
2556 next_vert
= [-1]*n_verts
2558 if self
.rand_dir
> 0:
2559 for i
in range(n_verts
):
2561 nw
= neigh_weight
[i
]
2562 sorted_nw
= neigh_weight
[i
].copy()
2565 neigh
[i
] = [n
[nw
.index(w
)] for w
in sorted_nw
]
2567 if self
.pos_steps
> 0:
2568 for i
in range(n_verts
):
2570 if len(n
) == 0: continue
2571 nw
= neigh_weight
[i
]
2573 if self
.same_weight
:
2574 if max_w
>= weight
[i
]:
2575 next_vert
[i
] = n
[nw
.index(max(nw
))]
2577 if max_w
> weight
[i
]:
2578 next_vert
[i
] = n
[nw
.index(max(nw
))]
2580 if self
.neg_steps
> 0:
2581 prev_vert
= [-1]*n_verts
2582 for i
in range(n_verts
):
2584 if len(n
) == 0: continue
2585 nw
= neigh_weight
[i
]
2587 if self
.same_weight
:
2588 if min_w
<= weight
[i
]:
2589 prev_vert
[i
] = n
[nw
.index(min(nw
))]
2591 if min_w
< weight
[i
]:
2592 prev_vert
[i
] = n
[nw
.index(min(nw
))]
2595 me
.vertices
.foreach_get('co', co
)
2596 co
= np
.array(co
).reshape((-1,3))
2598 # create streamlines
2602 for j
in range(self
.pos_steps
):
2603 if self
.rand_dir
> 0:
2604 n
= neigh
[next_pts
[-1]]
2605 if len(n
) == 0: break
2606 next
= n
[int((len(n
)-1) * (1-random
.random() * self
.rand_dir
))]
2608 next
= next_vert
[next_pts
[-1]]
2610 if next
not in next_pts
: next_pts
.append(next
)
2614 for j
in range(self
.neg_steps
):
2615 if self
.rand_dir
> 0:
2616 n
= neigh
[prev_pts
[-1]]
2617 if len(n
) == 0: break
2618 prev
= n
[int(len(n
) * random
.random() * self
.rand_dir
)]
2620 prev
= prev_vert
[prev_pts
[-1]]
2622 if prev
not in prev_pts
:
2623 prev_pts
.append(prev
)
2626 next_pts
= np
.array(next_pts
).astype('int')
2627 prev_pts
= np
.flip(prev_pts
[1:]).astype('int')
2628 all_pts
= np
.concatenate((prev_pts
, next_pts
))
2629 if len(all_pts
) > 1:
2630 curves
.append(all_pts
)
2631 crv
= nurbs_from_vertices(curves
, co
, bevel_weight
, ob
.name
+ '_Streamlines', True, self
.interpolation
)
2632 crv
.data
.bevel_depth
= bevel_depth
2633 crv
.matrix_world
= ob
.matrix_world
2634 bpy
.data
.objects
.remove(ob
)
2636 print("Streamlines Curves, total time: " + str(timeit
.default_timer() - start_time
) + " sec")