expre-gen/blender/3.4/scripts/addons/mesh_tissue/weight_tools.py
2025-10-17 09:47:23 +08:00

4666 lines
170 KiB
Python

# SPDX-License-Identifier: GPL-2.0-or-later
#-------------------------- COLORS / GROUPS EXCHANGER -------------------------#
# #
# Vertex Color to Vertex Group allow you to convert colors channels to weight #
# maps. #
# The main purpose is to use vertex colors to store information when importing #
# files from other software. The script works with the active vertex color #
# slot. #
# For use the command "Vertex Clors to Vertex Groups" use the search bar #
# (space bar). #
# #
# (c) Alessandro Zomparelli #
# (2017) #
# #
# http://www.co-de-it.com/ #
# #
################################################################################
import bpy, bmesh, os
import numpy as np
import math, timeit, time
from math import pi
from statistics import mean, stdev
from mathutils import Vector
from mathutils.kdtree import KDTree
from numpy import *
try: from .numba_functions import numba_reaction_diffusion, numba_reaction_diffusion_anisotropic, integrate_field
except: pass
#from .numba_functions import integrate_field
#from .numba_functions import numba_reaction_diffusion
try: import numexpr as ne
except: pass
# Reaction-Diffusion cache
from pathlib import Path
import random as rnd
import string
from bpy.types import (
Operator,
Panel,
PropertyGroup,
)
from bpy.props import (
BoolProperty,
EnumProperty,
FloatProperty,
IntProperty,
StringProperty,
FloatVectorProperty,
IntVectorProperty
)
from .utils import *
def reaction_diffusion_add_handler(self, context):
# remove existing handlers
reaction_diffusion_remove_handler(self, context)
# add new handler
bpy.app.handlers.frame_change_post.append(reaction_diffusion_scene)
def reaction_diffusion_remove_handler(self, context):
# remove existing handlers
old_handlers = []
for h in bpy.app.handlers.frame_change_post:
if "reaction_diffusion" in str(h):
old_handlers.append(h)
for h in old_handlers: bpy.app.handlers.frame_change_post.remove(h)
class formula_prop(PropertyGroup):
name : StringProperty()
formula : StringProperty()
float_var : FloatVectorProperty(name="", description="", default=(0, 0, 0, 0, 0), size=5)
int_var : IntVectorProperty(name="", description="", default=(0, 0, 0, 0, 0), size=5)
class reaction_diffusion_prop(PropertyGroup):
run : BoolProperty(default=False, update = reaction_diffusion_add_handler,
description='Compute a new iteration on frame changes. Currently is not working during Render Animation')
time_steps : IntProperty(
name="Steps", default=10, min=0, soft_max=50,
description="Number of Steps")
dt : FloatProperty(
name="dt", default=1, min=0, soft_max=0.2,
description="Time Step")
diff_a : FloatProperty(
name="Diff A", default=0.1, min=0, soft_max=2, precision=3,
description="Diffusion A")
diff_b : FloatProperty(
name="Diff B", default=0.05, min=0, soft_max=2, precision=3,
description="Diffusion B")
f : FloatProperty(
name="f", default=0.055, soft_min=0.01, soft_max=0.06, precision=4, step=0.05,
description="Feed Rate")
k : FloatProperty(
name="k", default=0.062, soft_min=0.035, soft_max=0.065, precision=4, step=0.05,
description="Kill Rate")
diff_mult : FloatProperty(
name="Scale", default=1, min=0, soft_max=1, max=10, precision=2,
description="Multiplier for the diffusion of both substances")
vertex_group_diff_a : StringProperty(
name="Diff A", default='',
description="Vertex Group used for A diffusion")
vertex_group_diff_b : StringProperty(
name="Diff B", default='',
description="Vertex Group used for B diffusion")
vertex_group_scale : StringProperty(
name="Scale", default='',
description="Vertex Group used for Scale value")
vertex_group_f : StringProperty(
name="f", default='',
description="Vertex Group used for Feed value (f)")
vertex_group_k : StringProperty(
name="k", default='',
description="Vertex Group used for Kill value (k)")
vertex_group_brush : StringProperty(
name="Brush", default='',
description="Vertex Group used for adding/removing B")
invert_vertex_group_diff_a : BoolProperty(default=False,
description='Inverte the value of the Vertex Group Diff A')
invert_vertex_group_diff_b : BoolProperty(default=False,
description='Inverte the value of the Vertex Group Diff B')
invert_vertex_group_scale : BoolProperty(default=False,
description='Inverte the value of the Vertex Group Scale')
invert_vertex_group_f : BoolProperty(default=False,
description='Inverte the value of the Vertex Group f')
invert_vertex_group_k : BoolProperty(default=False,
description='Inverte the value of the Vertex Group k')
min_diff_a : FloatProperty(
name="Min Diff A", default=0.1, min=0, soft_max=2, precision=3,
description="Min Diff A")
max_diff_a : FloatProperty(
name="Max Diff A", default=0.1, min=0, soft_max=2, precision=3,
description="Max Diff A")
min_diff_b : FloatProperty(
name="Min Diff B", default=0.1, min=0, soft_max=2, precision=3,
description="Min Diff B")
max_diff_b : FloatProperty(
name="Max Diff B", default=0.1, min=0, soft_max=2, precision=3,
description="Max Diff B")
min_scale : FloatProperty(
name="Scale", default=0.35, min=0, soft_max=1, max=10, precision=2,
description="Min Scale Value")
max_scale : FloatProperty(
name="Scale", default=1, min=0, soft_max=1, max=10, precision=2,
description="Max Scale value")
min_f : FloatProperty(
name="Min f", default=0.02, min=0, soft_min=0.01, soft_max=0.06, max=0.1, precision=4, step=0.05,
description="Min Feed Rate")
max_f : FloatProperty(
name="Max f", default=0.055, min=0, soft_min=0.01, soft_max=0.06, max=0.1, precision=4, step=0.05,
description="Max Feed Rate")
min_k : FloatProperty(
name="Min k", default=0.035, min=0, soft_min=0.035, soft_max=0.065, max=0.1, precision=4, step=0.05,
description="Min Kill Rate")
max_k : FloatProperty(
name="Max k", default=0.062, min=0, soft_min=0.035, soft_max=0.065, max=0.1, precision=4, step=0.05,
description="Max Kill Rate")
brush_mult : FloatProperty(
name="Mult", default=0.5, min=-1, max=1, precision=3, step=0.05,
description="Multiplier for brush value")
bool_mod : BoolProperty(
name="Use Modifiers", default=False,
description="Read modifiers affect the vertex groups")
bool_cache : BoolProperty(
name="Use Cache", default=False,
description="Read modifiers affect the vertex groups")
cache_frame_start : IntProperty(
name="Start", default=1,
description="Frame on which the simulation starts")
cache_frame_end : IntProperty(
name="End", default=250,
description="Frame on which the simulation ends")
cache_dir : StringProperty(
name="Cache directory", default="", subtype='FILE_PATH',
description = 'Directory that contains Reaction-Diffusion cache files'
)
update_weight_a : BoolProperty(
name="Update Vertex Group A", default=True,
description="Transfer Cache to the Vertex Groups named A")
update_weight_b : BoolProperty(
name="Update Vertex Group B", default=True,
description="Transfer Cache to the Vertex Groups named B")
update_colors_a : BoolProperty(
name="Update Vertex Color A", default=False,
description="Transfer Cache to the Vertex Color named A")
update_colors_b : BoolProperty(
name="Update Vertex Color B", default=False,
description="Transfer Cache to the Vertex Color named B")
update_colors : BoolProperty(
name="Update Vertex Color AB", default=False,
description="Transfer Cache to the Vertex Color named AB")
update_uv : BoolProperty(
name="Update UV", default=False,
description="Transfer Cache to the UV Map Layer named AB")
normalize : BoolProperty(
name="Normalize values", default=False,
description="Normalize values from 0 to 1")
fast_bake : BoolProperty(
name="Fast Bake", default=True,
description="Do not update modifiers or vertex groups while baking. Much faster!")
from numpy import *
def compute_formula(ob=None, formula="rx", float_var=(0,0,0,0,0), int_var=(0,0,0,0,0)):
verts = ob.data.vertices
n_verts = len(verts)
f1,f2,f3,f4,f5 = float_var
i1,i2,i3,i4,i5 = int_var
do_groups = "w[" in formula
do_local = "lx" in formula or "ly" in formula or "lz" in formula
do_global = "gx" in formula or "gy" in formula or "gz" in formula
do_relative = "rx" in formula or "ry" in formula or "rz" in formula
do_normal = "nx" in formula or "ny" in formula or "nz" in formula
mat = ob.matrix_world
for i in range(1000):
if "w["+str(i)+"]" in formula and i > len(ob.vertex_groups)-1:
return "w["+str(i)+"] not found"
w = []
for i in range(len(ob.vertex_groups)):
w.append([])
if "w["+str(i)+"]" in formula:
vg = ob.vertex_groups[i]
for v in verts:
try:
w[i].append(vg.weight(v.index))
except:
w[i].append(0)
w[i] = array(w[i])
start_time = timeit.default_timer()
# compute vertex coordinates
if do_local or do_relative or do_global:
co = [0]*n_verts*3
verts.foreach_get('co', co)
np_co = array(co).reshape((n_verts, 3))
lx, ly, lz = array(np_co).transpose()
if do_relative:
rx = np.interp(lx, (lx.min(), lx.max()), (0, +1))
ry = np.interp(ly, (ly.min(), ly.max()), (0, +1))
rz = np.interp(lz, (lz.min(), lz.max()), (0, +1))
if do_global:
co = [v.co for v in verts]
global_co = []
for v in co:
global_co.append(mat @ v)
global_co = array(global_co).reshape((n_verts, 3))
gx, gy, gz = array(global_co).transpose()
# compute vertex normals
if do_normal:
normal = [0]*n_verts*3
verts.foreach_get('normal', normal)
normal = array(normal).reshape((n_verts, 3))
nx, ny, nz = array(normal).transpose()
try:
weight = eval(formula)
return weight
except:
return "There is something wrong"
print("Weight Formula: " + str(timeit.default_timer() - start_time))
class weight_formula_wiki(Operator):
bl_idname = "scene.weight_formula_wiki"
bl_label = "Online Documentation"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
bpy.ops.wm.url_open(url="https://github.com/alessandro-zomparelli/tissue/wiki/Weight-Tools#weight-formula")
return {'FINISHED'}
class weight_formula(Operator):
bl_idname = "object.weight_formula"
bl_label = "Weight Formula"
bl_description = "Generate a Vertex Group according to a mathematical formula"
bl_options = {'REGISTER', 'UNDO'}
ex_items = [
('cos(arctan(nx/ny)*i1*2 + sin(rz*i3))/i2 + cos(arctan(nx/ny)*i1*2 - sin(rz*i3))/i2 + 0.5','Vertical Spots'),
('cos(arctan(nx/ny)*i1*2 + sin(rz*i2))/2 + cos(arctan(nx/ny)*i1*2 - sin(rz*i2))/2','Vertical Spots'),
('(sin(arctan(nx/ny)*i1*2)*sin(nz*i1*2)+1)/2','Grid Spots'),
('cos(arctan(nx/ny)*f1)','Vertical Stripes'),
('cos(arctan(lx/ly)*f1 + sin(rz*f2)*f3)','Curly Stripes'),
('sin(rz*pi*i1+arctan2(nx,ny))/2+0.5', 'Vertical Spiral'),
('sin(nx*15)<sin(ny*15)','Chess'),
('cos(ny*rz**2*i1)','Hyperbolic'),
('sin(rx*30) > 0','Step Stripes'),
('sin(nz*i1)','Normal Stripes'),
('w[0]**2','Vertex Group square'),
('abs(0.5-rz)*2','Double vertical gradient'),
('rz', 'Vertical Gradient')
]
_ex_items = list((str(i),'{} ( {} )'.format(s[0],s[1]),s[1]) for i,s in enumerate(ex_items))
_ex_items.append(('CUSTOM', "User Formula", ""))
examples : EnumProperty(
items = _ex_items, default='CUSTOM', name="Examples")
old_ex = ""
formula : StringProperty(
name="Formula", default="", description="Formula to Evaluate")
slider_f01 : FloatProperty(
name="f1", default=1, description="Slider Float 1")
slider_f02 : FloatProperty(
name="f2", default=1, description="Slider Float 2")
slider_f03 : FloatProperty(
name="f3", default=1, description="Slider Float 3")
slider_f04 : FloatProperty(
name="f4", default=1, description="Slider Float 4")
slider_f05 : FloatProperty(
name="f5", default=1, description="Slider Float 5")
slider_i01 : IntProperty(
name="i1", default=1, description="Slider Integer 1")
slider_i02 : IntProperty(
name="i2", default=1, description="Slider Integer 2")
slider_i03 : IntProperty(
name="i3", default=1, description="Slider Integer 3")
slider_i04 : IntProperty(
name="i4", default=1, description="Slider Integer 4")
slider_i05 : IntProperty(
name="i5", default=1, description="Slider Integer 5")
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self, width=350)
def draw(self, context):
layout = self.layout
#layout.label(text="Examples")
layout.prop(self, "examples", text="Examples")
#if self.examples == 'CUSTOM':
layout.label(text="Formula")
layout.prop(self, "formula", text="")
#try: self.examples = self.formula
#except: pass
if self.examples != 'CUSTOM':
example = self.ex_items[int(self.examples)][0]
if example != self.old_ex:
self.formula = example
self.old_ex = example
elif self.formula != example:
self.examples = 'CUSTOM'
formula = self.formula
layout.separator()
if "f1" in formula: layout.prop(self, "slider_f01")
if "f2" in formula: layout.prop(self, "slider_f02")
if "f3" in formula: layout.prop(self, "slider_f03")
if "f4" in formula: layout.prop(self, "slider_f04")
if "f5" in formula: layout.prop(self, "slider_f05")
if "i1" in formula: layout.prop(self, "slider_i01")
if "i2" in formula: layout.prop(self, "slider_i02")
if "i3" in formula: layout.prop(self, "slider_i03")
if "i4" in formula: layout.prop(self, "slider_i04")
if "i5" in formula: layout.prop(self, "slider_i05")
layout.label(text="Variables (for each vertex):")
layout.label(text="lx, ly, lz: Local Coordinates", icon='ORIENTATION_LOCAL')
layout.label(text="gx, gy, gz: Global Coordinates", icon='WORLD')
layout.label(text="rx, ry, rz: Local Coordinates (0 to 1)", icon='NORMALIZE_FCURVES')
layout.label(text="nx, ny, nz: Normal Coordinates", icon='SNAP_NORMAL')
layout.label(text="w[0], w[1], w[2], ... : Vertex Groups", icon="GROUP_VERTEX")
layout.separator()
layout.label(text="f1, f2, f3, f4, f5: Float Sliders", icon='MOD_HUE_SATURATION')#PROPERTIES
layout.label(text="i1, i2, i3, i4, i5: Integer Sliders", icon='MOD_HUE_SATURATION')
layout.separator()
#layout.label(text="All mathematical functions are based on Numpy", icon='INFO')
#layout.label(text="https://docs.scipy.org/doc/numpy-1.13.0/reference/routines.math.html", icon='INFO')
layout.operator("scene.weight_formula_wiki", icon="HELP")
#layout.label(text="(where 'i' is the index of the Vertex Group)")
def execute(self, context):
ob = context.active_object
n_verts = len(ob.data.vertices)
#if self.examples == 'CUSTOM':
# formula = self.formula
#else:
#self.formula = self.examples
# formula = self.examples
#f1, f2, f3, f4, f5 = self.slider_f01, self.slider_f02, self.slider_f03, self.slider_f04, self.slider_f05
#i1, i2, i3, i4, i5 = self.slider_i01, self.slider_i02, self.slider_i03, self.slider_i04, self.slider_i05
f_sliders = self.slider_f01, self.slider_f02, self.slider_f03, self.slider_f04, self.slider_f05
i_sliders = self.slider_i01, self.slider_i02, self.slider_i03, self.slider_i04, self.slider_i05
if self.examples != 'CUSTOM':
example = self.ex_items[int(self.examples)][0]
if example != self.old_ex:
self.formula = example
self.old_ex = example
elif self.formula != example:
self.examples = 'CUSTOM'
formula = self.formula
if formula == "": return {'FINISHED'}
# replace numeric sliders value
for i, slider in enumerate(f_sliders):
formula = formula.replace('f'+str(i+1),"{0:.2f}".format(slider))
for i, slider in enumerate(i_sliders):
formula =formula.replace('i'+str(i+1),str(slider))
vertex_group_name = "" + formula
ob.vertex_groups.new(name=vertex_group_name)
weight = compute_formula(ob, formula=formula, float_var=f_sliders, int_var=i_sliders)
if type(weight) == str:
self.report({'ERROR'}, weight)
return {'CANCELLED'}
#start_time = timeit.default_timer()
weight = nan_to_num(weight)
vg = ob.vertex_groups[-1]
if type(weight) == int or type(weight) == float:
for i in range(n_verts):
vg.add([i], weight, 'REPLACE')
elif type(weight) == ndarray:
for i in range(n_verts):
vg.add([i], weight[i], 'REPLACE')
ob.data.update()
bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
# Store formula settings
new_formula = ob.formula_settings.add()
new_formula.name = ob.vertex_groups[-1].name
new_formula.formula = formula
new_formula.int_var = i_sliders
new_formula.float_var = f_sliders
#for f in ob.formula_settings:
# print(f.name, f.formula, f.int_var, f.float_var)
return {'FINISHED'}
class update_weight_formula(Operator):
bl_idname = "object.update_weight_formula"
bl_label = "Update Weight Formula"
bl_description = "Update an existing Vertex Group. Make sure that the name\nof the active Vertex Group is a valid formula"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return len(context.object.vertex_groups) > 0
def execute(self, context):
ob = context.active_object
n_verts = len(ob.data.vertices)
vg = ob.vertex_groups.active
formula = vg.name
weight = compute_formula(ob, formula=formula)
if type(weight) == str:
self.report({'ERROR'}, "The name of the active Vertex Group\nis not a valid Formula")
return {'CANCELLED'}
#start_time = timeit.default_timer()
weight = nan_to_num(weight)
if type(weight) == int or type(weight) == float:
for i in range(n_verts):
vg.add([i], weight, 'REPLACE')
elif type(weight) == ndarray:
for i in range(n_verts):
vg.add([i], weight[i], 'REPLACE')
ob.data.update()
bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
return {'FINISHED'}
class _weight_laplacian(Operator):
bl_idname = "object._weight_laplacian"
bl_label = "Weight Laplacian"
bl_description = ("Compute the Vertex Group Laplacian")
bl_options = {'REGISTER', 'UNDO'}
bounds : EnumProperty(
items=(('MANUAL', "Manual Bounds", ""),
('POSITIVE', "Positive Only", ""),
('NEGATIVE', "Negative Only", ""),
('AUTOMATIC', "Automatic Bounds", "")),
default='AUTOMATIC', name="Bounds")
mode : EnumProperty(
items=(('LENGTH', "Length Weight", ""),
('SIMPLE', "Simple", "")),
default='SIMPLE', name="Evaluation Mode")
min_def : FloatProperty(
name="Min", default=0, soft_min=-1, soft_max=0,
description="Laplacian value with 0 weight")
max_def : FloatProperty(
name="Max", default=0.5, soft_min=0, soft_max=5,
description="Laplacian value with 1 weight")
bounds_string = ""
frame = None
@classmethod
def poll(cls, context):
return len(context.object.vertex_groups) > 0
def draw(self, context):
layout = self.layout
col = layout.column(align=True)
col.label(text="Evaluation Mode")
col.prop(self, "mode", text="")
col.label(text="Bounds")
col.prop(self, "bounds", text="")
if self.bounds == 'MANUAL':
col.label(text="Strain Rate \u03B5:")
col.prop(self, "min_def")
col.prop(self, "max_def")
col.label(text="\u03B5" + ": from " + self.bounds_string)
def execute(self, context):
try: ob = context.object
except:
self.report({'ERROR'}, "Please select an Object")
return {'CANCELLED'}
group_id = ob.vertex_groups.active_index
input_group = ob.vertex_groups[group_id].name
group_name = "Laplacian"
ob.vertex_groups.new(name=group_name)
me = ob.data
bm = bmesh.new()
bm.from_mesh(me)
bm.edges.ensure_lookup_table()
# store weight values
weight = []
for v in me.vertices:
try:
weight.append(ob.vertex_groups[input_group].weight(v.index))
except:
weight.append(0)
n_verts = len(bm.verts)
lap = [0]*n_verts
for e in bm.edges:
if self.mode == 'LENGTH':
length = e.calc_length()
if length == 0: continue
id0 = e.verts[0].index
id1 = e.verts[1].index
lap[id0] += weight[id1]/length - weight[id0]/length
lap[id1] += weight[id0]/length - weight[id1]/length
else:
id0 = e.verts[0].index
id1 = e.verts[1].index
lap[id0] += weight[id1] - weight[id0]
lap[id1] += weight[id0] - weight[id1]
mean_lap = mean(lap)
stdev_lap = stdev(lap)
filter_lap = [i for i in lap if mean_lap-2*stdev_lap < i < mean_lap+2*stdev_lap]
if self.bounds == 'MANUAL':
min_def = self.min_def
max_def = self.max_def
elif self.bounds == 'AUTOMATIC':
min_def = min(filter_lap)
max_def = max(filter_lap)
self.min_def = min_def
self.max_def = max_def
elif self.bounds == 'NEGATIVE':
min_def = 0
max_def = min(filter_lap)
self.min_def = min_def
self.max_def = max_def
elif self.bounds == 'POSITIVE':
min_def = 0
max_def = max(filter_lap)
self.min_def = min_def
self.max_def = max_def
delta_def = max_def - min_def
# check undeformed errors
if delta_def == 0: delta_def = 0.0001
for i in range(len(lap)):
val = (lap[i]-min_def)/delta_def
if val > 0.7: print(str(val) + " " + str(lap[i]))
#val = weight[i] + 0.2*lap[i]
ob.vertex_groups[-1].add([i], val, 'REPLACE')
self.bounds_string = str(round(min_def,2)) + " to " + str(round(max_def,2))
ob.vertex_groups[-1].name = group_name + " " + self.bounds_string
ob.vertex_groups.update()
ob.data.update()
bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
bm.free()
return {'FINISHED'}
class ok_weight_laplacian(Operator):
bl_idname = "object.weight_laplacian"
bl_label = "Weight Laplacian"
bl_description = ("Compute the Vertex Group Laplacian")
bl_options = {'REGISTER', 'UNDO'}
bounds_string = ""
frame = None
@classmethod
def poll(cls, context):
return len(context.object.vertex_groups) > 0
def execute(self, context):
try: ob = context.object
except:
self.report({'ERROR'}, "Please select an Object")
return {'CANCELLED'}
me = ob.data
bm = bmesh.new()
bm.from_mesh(me)
bm.edges.ensure_lookup_table()
group_id = ob.vertex_groups.active_index
input_group = ob.vertex_groups[group_id].name
group_name = "Laplacian"
ob.vertex_groups.new(name=group_name)
# store weight values
a = []
for v in me.vertices:
try:
a.append(ob.vertex_groups[input_group].weight(v.index))
except:
a.append(0)
a = array(a)
# initialize
n_verts = len(bm.verts)
# find max number of edges for vertex
max_edges = 0
n_neighbors = []
id_neighbors = []
for v in bm.verts:
n_edges = len(v.link_edges)
max_edges = max(max_edges, n_edges)
n_neighbors.append(n_edges)
neighbors = []
for e in v.link_edges:
for v1 in e.verts:
if v != v1: neighbors.append(v1.index)
id_neighbors.append(neighbors)
n_neighbors = array(n_neighbors)
lap_map = [[] for i in range(n_verts)]
#lap_map = []
'''
for e in bm.edges:
id0 = e.verts[0].index
id1 = e.verts[1].index
lap_map[id0].append(id1)
lap_map[id1].append(id0)
'''
lap = zeros((n_verts))#[0]*n_verts
n_records = zeros((n_verts))
for e in bm.edges:
id0 = e.verts[0].index
id1 = e.verts[1].index
length = e.calc_length()
if length == 0: continue
#lap[id0] += abs(a[id1] - a[id0])/length
#lap[id1] += abs(a[id0] - a[id1])/length
lap[id0] += (a[id1] - a[id0])/length
lap[id1] += (a[id0] - a[id1])/length
n_records[id0]+=1
n_records[id1]+=1
lap /= n_records
lap /= max(lap)
for i in range(n_verts):
ob.vertex_groups['Laplacian'].add([i], lap[i], 'REPLACE')
ob.vertex_groups.update()
ob.data.update()
bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
bm.free()
return {'FINISHED'}
class weight_laplacian(Operator):
bl_idname = "object.weight_laplacian"
bl_label = "Weight Laplacian"
bl_description = ("Compute the Vertex Group Laplacian")
bl_options = {'REGISTER', 'UNDO'}
bounds_string = ""
frame = None
@classmethod
def poll(cls, context):
return len(context.object.vertex_groups) > 0
def execute(self, context):
try: ob = context.object
except:
self.report({'ERROR'}, "Please select an Object")
return {'CANCELLED'}
me = ob.data
bm = bmesh.new()
bm.from_mesh(me)
bm.edges.ensure_lookup_table()
n_verts = len(me.vertices)
group_id = ob.vertex_groups.active_index
input_group = ob.vertex_groups[group_id].name
group_name = "Laplacian"
vg = ob.vertex_groups.new(name=group_name)
# store weight values
dvert_lay = bm.verts.layers.deform.active
weight = bmesh_get_weight_numpy(group_id, dvert_lay, bm.verts)
#verts, normals = get_vertices_and_normals_numpy(me)
#lap = zeros((n_verts))#[0]*n_verts
lap = [Vector((0,0,0)) for i in range(n_verts)]
n_records = zeros((n_verts))
for e in bm.edges:
vert0 = e.verts[0]
vert1 = e.verts[1]
id0 = vert0.index
id1 = vert1.index
v0 = vert0.co
v1 = vert1.co
v01 = v1-v0
v10 = -v01
v01 -= v01.project(vert0.normal)
v10 -= v10.project(vert1.normal)
length = e.calc_length()
if length == 0: continue
dw = (weight[id1] - weight[id0])/length
lap[id0] += v01.normalized() * dw
lap[id1] -= v10.normalized() * dw
n_records[id0]+=1
n_records[id1]+=1
#lap /= n_records[:,np.newaxis]
lap = [l.length/r for r,l in zip(n_records,lap)]
lap = np.array(lap)
lap /= np.max(lap)
lap = list(lap)
print(lap)
for i in range(n_verts):
vg.add([i], lap[i], 'REPLACE')
ob.vertex_groups.update()
ob.data.update()
bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
bm.free()
return {'FINISHED'}
class reaction_diffusion(Operator):
bl_idname = "object.reaction_diffusion"
bl_label = "Reaction Diffusion"
bl_description = ("Run a Reaction-Diffusion based on existing Vertex Groups: A and B")
bl_options = {'REGISTER', 'UNDO'}
steps : IntProperty(
name="Steps", default=10, min=0, soft_max=50,
description="Number of Steps")
dt : FloatProperty(
name="dt", default=0.2, min=0, soft_max=0.2,
description="Time Step")
diff_a : FloatProperty(
name="Diff A", default=1, min=0, soft_max=2,
description="Diffusion A")
diff_b : FloatProperty(
name="Diff B", default=0.5, min=0, soft_max=2,
description="Diffusion B")
f : FloatProperty(
name="f", default=0.055, min=0, soft_min=0.01, soft_max=0.06, max=0.1, precision=4,
description="Feed Rate")
k : FloatProperty(
name="k", default=0.062, min=0, soft_min=0.035, soft_max=0.065, max=0.1, precision=4,
description="Kill Rate")
bounds_string = ""
frame = None
@classmethod
def poll(cls, context):
return len(context.object.vertex_groups) > 0
def execute(self, context):
#bpy.app.handlers.frame_change_post.remove(reaction_diffusion_def)
reaction_diffusion_add_handler(self, context)
set_animatable_fix_handler(self, context)
try: ob = context.object
except:
self.report({'ERROR'}, "Please select an Object")
return {'CANCELLED'}
me = ob.data
bm = bmesh.new()
bm.from_mesh(me)
bm.edges.ensure_lookup_table()
# store weight values
a = []
b = []
for v in me.vertices:
try:
a.append(ob.vertex_groups["A"].weight(v.index))
except:
a.append(0)
try:
b.append(ob.vertex_groups["B"].weight(v.index))
except:
b.append(0)
a = array(a)
b = array(b)
f = self.f
k = self.k
diff_a = self.diff_a
diff_b = self.diff_b
dt = self.dt
n_verts = len(bm.verts)
for i in range(self.steps):
lap_a = zeros((n_verts))#[0]*n_verts
lap_b = zeros((n_verts))#[0]*n_verts
for e in bm.edges:
id0 = e.verts[0].index
id1 = e.verts[1].index
lap_a[id0] += a[id1] - a[id0]
lap_a[id1] += a[id0] - a[id1]
lap_b[id0] += b[id1] - b[id0]
lap_b[id1] += b[id0] - b[id1]
ab2 = a*b**2
a += (diff_a*lap_a - ab2 + f*(1-a))*dt
b += (diff_b*lap_b + ab2 - (k+f)*b)*dt
for i in range(n_verts):
ob.vertex_groups['A'].add([i], a[i], 'REPLACE')
ob.vertex_groups['B'].add([i], b[i], 'REPLACE')
ob.vertex_groups.update()
ob.data.update()
bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
bm.free()
return {'FINISHED'}
class edges_deformation(Operator):
bl_idname = "object.edges_deformation"
bl_label = "Edges Deformation"
bl_description = ("Compute Weight based on the deformation of edges"+
"according to visible modifiers.")
bl_options = {'REGISTER', 'UNDO'}
bounds : EnumProperty(
items=(('MANUAL', "Manual Bounds", ""),
('COMPRESSION', "Compressed Only", ""),
('TENSION', "Extended Only", ""),
('AUTOMATIC', "Automatic Bounds", "")),
default='AUTOMATIC', name="Bounds")
mode : EnumProperty(
items=(('MAX', "Max Deformation", ""),
('MEAN', "Average Deformation", "")),
default='MEAN', name="Evaluation Mode")
min_def : FloatProperty(
name="Min", default=0, soft_min=-1, soft_max=0,
description="Deformations with 0 weight")
max_def : FloatProperty(
name="Max", default=0.5, soft_min=0, soft_max=5,
description="Deformations with 1 weight")
bounds_string = ""
frame = None
@classmethod
def poll(cls, context):
return len(context.object.modifiers) > 0
def draw(self, context):
layout = self.layout
col = layout.column(align=True)
col.label(text="Evaluation Mode")
col.prop(self, "mode", text="")
col.label(text="Bounds")
col.prop(self, "bounds", text="")
if self.bounds == 'MANUAL':
col.label(text="Strain Rate \u03B5:")
col.prop(self, "min_def")
col.prop(self, "max_def")
col.label(text="\u03B5" + ": from " + self.bounds_string)
def execute(self, context):
try: ob = context.object
except:
self.report({'ERROR'}, "Please select an Object")
return {'CANCELLED'}
# check if the object is Cloth or Softbody
physics = False
for m in ob.modifiers:
if m.type == 'CLOTH' or m.type == 'SOFT_BODY':
physics = True
if context.scene.frame_current == 1 and self.frame != None:
context.scene.frame_current = self.frame
break
if not physics: self.frame = None
if self.mode == 'MEAN': group_name = "Average Deformation"
elif self.mode == 'MAX': group_name = "Max Deformation"
ob.vertex_groups.new(name=group_name)
me0 = ob.data
me = simple_to_mesh(ob) #ob.to_mesh(preserve_all_data_layers=True, depsgraph=bpy.context.evaluated_depsgraph_get()).copy()
if len(me.vertices) != len(me0.vertices) or len(me.edges) != len(me0.edges):
self.report({'ERROR'}, "The topology of the object should be" +
"unaltered")
return {'CANCELLED'}
bm0 = bmesh.new()
bm0.from_mesh(me0)
bm = bmesh.new()
bm.from_mesh(me)
deformations = []
for e0, e in zip(bm0.edges, bm.edges):
try:
l0 = e0.calc_length()
l1 = e.calc_length()
epsilon = (l1 - l0)/l0
deformations.append(epsilon)
except: deformations.append(1)
v_deformations = []
for v in bm.verts:
vdef = []
for e in v.link_edges:
vdef.append(deformations[e.index])
if self.mode == 'MEAN': v_deformations.append(mean(vdef))
elif self.mode == 'MAX': v_deformations.append(max(vdef, key=abs))
#elif self.mode == 'MIN': v_deformations.append(min(vdef, key=abs))
if self.bounds == 'MANUAL':
min_def = self.min_def
max_def = self.max_def
elif self.bounds == 'AUTOMATIC':
min_def = min(v_deformations)
max_def = max(v_deformations)
self.min_def = min_def
self.max_def = max_def
elif self.bounds == 'COMPRESSION':
min_def = 0
max_def = min(v_deformations)
self.min_def = min_def
self.max_def = max_def
elif self.bounds == 'TENSION':
min_def = 0
max_def = max(v_deformations)
self.min_def = min_def
self.max_def = max_def
delta_def = max_def - min_def
# check undeformed errors
if delta_def == 0:
if self.bounds == 'MANUAL':
delta_def = 0.0001
else:
message = "The object doesn't have deformations."
if physics:
message = message + ("\nIf you are using Physics try to " +
"save it in the cache before.")
self.report({'ERROR'}, message)
return {'CANCELLED'}
else:
if physics:
self.frame = context.scene.frame_current
for i in range(len(v_deformations)):
weight = (v_deformations[i] - min_def)/delta_def
ob.vertex_groups[-1].add([i], weight, 'REPLACE')
self.bounds_string = str(round(min_def,2)) + " to " + str(round(max_def,2))
ob.vertex_groups[-1].name = group_name + " " + self.bounds_string
ob.vertex_groups.update()
ob.data.update()
bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
bpy.data.meshes.remove(me)
bm.free()
bm0.free()
return {'FINISHED'}
class edges_bending(Operator):
bl_idname = "object.edges_bending"
bl_label = "Edges Bending"
bl_description = ("Compute Weight based on the bending of edges"+
"according to visible modifiers.")
bl_options = {'REGISTER', 'UNDO'}
bounds : EnumProperty(
items=(('MANUAL', "Manual Bounds", ""),
('POSITIVE', "Positive Only", ""),
('NEGATIVE', "Negative Only", ""),
('UNSIGNED', "Absolute Bending", ""),
('AUTOMATIC', "Signed Bending", "")),
default='AUTOMATIC', name="Bounds")
min_def : FloatProperty(
name="Min", default=-10, soft_min=-45, soft_max=45,
description="Deformations with 0 weight")
max_def : FloatProperty(
name="Max", default=10, soft_min=-45, soft_max=45,
description="Deformations with 1 weight")
bounds_string = ""
frame = None
@classmethod
def poll(cls, context):
return len(context.object.modifiers) > 0
def draw(self, context):
layout = self.layout
layout.label(text="Bounds")
layout.prop(self, "bounds", text="")
if self.bounds == 'MANUAL':
layout.prop(self, "min_def")
layout.prop(self, "max_def")
def execute(self, context):
try: ob = context.object
except:
self.report({'ERROR'}, "Please select an Object")
return {'CANCELLED'}
group_name = "Edges Bending"
ob.vertex_groups.new(name=group_name)
# check if the object is Cloth or Softbody
physics = False
for m in ob.modifiers:
if m.type == 'CLOTH' or m.type == 'SOFT_BODY':
physics = True
if context.scene.frame_current == 1 and self.frame != None:
context.scene.frame_current = self.frame
break
if not physics: self.frame = None
#ob.data.update()
#context.scene.update()
me0 = ob.data
me = simple_to_mesh(ob) #ob.to_mesh(preserve_all_data_layers=True, depsgraph=bpy.context.evaluated_depsgraph_get()).copy()
if len(me.vertices) != len(me0.vertices) or len(me.edges) != len(me0.edges):
self.report({'ERROR'}, "The topology of the object should be" +
"unaltered")
bm0 = bmesh.new()
bm0.from_mesh(me0)
bm = bmesh.new()
bm.from_mesh(me)
deformations = []
for e0, e in zip(bm0.edges, bm.edges):
try:
ang = e.calc_face_angle_signed()
ang0 = e0.calc_face_angle_signed()
if self.bounds == 'UNSIGNED':
deformations.append(abs(ang-ang0))
else:
deformations.append(ang-ang0)
except: deformations.append(0)
v_deformations = []
for v in bm.verts:
vdef = []
for e in v.link_edges:
vdef.append(deformations[e.index])
v_deformations.append(mean(vdef))
if self.bounds == 'MANUAL':
min_def = radians(self.min_def)
max_def = radians(self.max_def)
elif self.bounds == 'AUTOMATIC':
min_def = min(v_deformations)
max_def = max(v_deformations)
elif self.bounds == 'POSITIVE':
min_def = 0
max_def = min(v_deformations)
elif self.bounds == 'NEGATIVE':
min_def = 0
max_def = max(v_deformations)
elif self.bounds == 'UNSIGNED':
min_def = 0
max_def = max(v_deformations)
delta_def = max_def - min_def
# check undeformed errors
if delta_def == 0:
if self.bounds == 'MANUAL':
delta_def = 0.0001
else:
message = "The object doesn't have deformations."
if physics:
message = message + ("\nIf you are using Physics try to " +
"save it in the cache before.")
self.report({'ERROR'}, message)
return {'CANCELLED'}
else:
if physics:
self.frame = context.scene.frame_current
for i in range(len(v_deformations)):
weight = (v_deformations[i] - min_def)/delta_def
ob.vertex_groups[-1].add([i], weight, 'REPLACE')
self.bounds_string = str(round(min_def,2)) + " to " + str(round(max_def,2))
ob.vertex_groups[-1].name = group_name + " " + self.bounds_string
ob.vertex_groups.update()
ob.data.update()
bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
bpy.data.meshes.remove(me)
bm0.free()
bm.free()
return {'FINISHED'}
class weight_contour_displace(Operator):
bl_idname = "object.weight_contour_displace"
bl_label = "Contour Displace"
bl_description = ("")
bl_options = {'REGISTER', 'UNDO'}
use_modifiers : BoolProperty(
name="Use Modifiers", default=True,
description="Apply all the modifiers")
min_iso : FloatProperty(
name="Min Iso Value", default=0.49, min=0, max=1,
description="Threshold value")
max_iso : FloatProperty(
name="Max Iso Value", default=0.51, min=0, max=1,
description="Threshold value")
n_cuts : IntProperty(
name="Cuts", default=2, min=1, soft_max=10,
description="Number of cuts in the selected range of values")
bool_displace : BoolProperty(
name="Add Displace", default=True, description="Add Displace Modifier")
bool_flip : BoolProperty(
name="Flip", default=False, description="Flip Output Weight")
weight_mode : EnumProperty(
items=[('Remapped', 'Remapped', 'Remap values'),
('Alternate', 'Alternate', 'Alternate 0 and 1'),
('Original', 'Original', 'Keep original Vertex Group')],
name="Weight", description="Choose how to convert vertex group",
default="Remapped", options={'LIBRARY_EDITABLE'})
@classmethod
def poll(cls, context):
return len(context.object.vertex_groups) > 0
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self, width=350)
def execute(self, context):
start_time = timeit.default_timer()
try:
check = context.object.vertex_groups[0]
except:
self.report({'ERROR'}, "The object doesn't have Vertex Groups")
return {'CANCELLED'}
ob0 = context.object
group_id = ob0.vertex_groups.active_index
vertex_group_name = ob0.vertex_groups[group_id].name
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.object.mode_set(mode='OBJECT')
if self.use_modifiers:
#me0 = ob0.to_mesh(preserve_all_data_layers=True, depsgraph=bpy.context.evaluated_depsgraph_get()).copy()
me0 = simple_to_mesh(ob0)
else:
me0 = ob0.data.copy()
# generate new bmesh
bm = bmesh.new()
bm.from_mesh(me0)
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
bm.faces.ensure_lookup_table()
# store weight values
weight = []
ob = bpy.data.objects.new("temp", me0)
for g in ob0.vertex_groups:
ob.vertex_groups.new(name=g.name)
for v in me0.vertices:
try:
weight.append(ob.vertex_groups[vertex_group_name].weight(v.index))
except:
weight.append(0)
# define iso values
iso_values = []
for i_cut in range(self.n_cuts):
delta_iso = abs(self.max_iso - self.min_iso)
min_iso = min(self.min_iso, self.max_iso)
max_iso = max(self.min_iso, self.max_iso)
if delta_iso == 0: iso_val = min_iso
elif self.n_cuts > 1: iso_val = i_cut/(self.n_cuts-1)*delta_iso + min_iso
else: iso_val = (self.max_iso + self.min_iso)/2
iso_values.append(iso_val)
# Start Cuts Iterations
filtered_edges = bm.edges
for iso_val in iso_values:
delete_edges = []
faces_mask = []
for f in bm.faces:
w_min = 2
w_max = 2
for v in f.verts:
w = weight[v.index]
if w_min == 2:
w_max = w_min = w
if w > w_max: w_max = w
if w < w_min: w_min = w
if w_min < iso_val and w_max > iso_val:
faces_mask.append(f)
break
#link_faces = [[f for f in e.link_faces] for e in bm.edges]
#faces_todo = [f.select for f in bm.faces]
#faces_todo = [True for f in bm.faces]
verts = []
edges = []
edges_id = {}
_filtered_edges = []
n_verts = len(bm.verts)
count = n_verts
for e in filtered_edges:
#id0 = e.vertices[0]
#id1 = e.vertices[1]
id0 = e.verts[0].index
id1 = e.verts[1].index
w0 = weight[id0]
w1 = weight[id1]
if w0 == w1: continue
elif w0 > iso_val and w1 > iso_val:
_filtered_edges.append(e)
continue
elif w0 < iso_val and w1 < iso_val: continue
elif w0 == iso_val or w1 == iso_val:
_filtered_edges.append(e)
continue
else:
v0 = bm.verts[id0].co
v1 = bm.verts[id1].co
v = v0.lerp(v1, (iso_val-w0)/(w1-w0))
if e not in delete_edges:
delete_edges.append(e)
verts.append(v)
edges_id[str(id0)+"_"+str(id1)] = count
edges_id[str(id1)+"_"+str(id0)] = count
count += 1
_filtered_edges.append(e)
filtered_edges = _filtered_edges
splitted_faces = []
switch = False
# splitting faces
for f in faces_mask:
# create sub-faces slots. Once a new vertex is reached it will
# change slot, storing the next vertices for a new face.
build_faces = [[],[]]
#switch = False
verts0 = [v.index for v in f.verts]
verts1 = list(verts0)
verts1.append(verts1.pop(0)) # shift list
for id0, id1 in zip(verts0, verts1):
# add first vertex to active slot
build_faces[switch].append(id0)
# try to split edge
try:
# check if the edge must be splitted
new_vert = edges_id[str(id0)+"_"+str(id1)]
# add new vertex
build_faces[switch].append(new_vert)
# if there is an open face on the other slot
if len(build_faces[not switch]) > 0:
# store actual face
splitted_faces.append(build_faces[switch])
# reset actual faces and switch
build_faces[switch] = []
# change face slot
switch = not switch
# continue previous face
build_faces[switch].append(new_vert)
except: pass
if len(build_faces[not switch]) == 2:
build_faces[not switch].append(id0)
if len(build_faces[not switch]) > 2:
splitted_faces.append(build_faces[not switch])
# add last face
splitted_faces.append(build_faces[switch])
#del_faces.append(f.index)
# adding new vertices
_new_vert = bm.verts.new
for v in verts: new_vert = _new_vert(v)
bm.verts.index_update()
bm.verts.ensure_lookup_table()
# adding new faces
_new_face = bm.faces.new
missed_faces = []
added_faces = []
for f in splitted_faces:
try:
face_verts = [bm.verts[i] for i in f]
new_face = _new_face(face_verts)
for e in new_face.edges:
filtered_edges.append(e)
except:
missed_faces.append(f)
bm.faces.ensure_lookup_table()
# updating weight values
weight = weight + [iso_val]*len(verts)
# deleting old edges/faces
_remove_edge = bm.edges.remove
bm.edges.ensure_lookup_table()
for e in delete_edges:
_remove_edge(e)
_filtered_edges = []
for e in filtered_edges:
if e not in delete_edges: _filtered_edges.append(e)
filtered_edges = _filtered_edges
name = ob0.name + '_ContourDisp'
me = bpy.data.meshes.new(name)
bm.to_mesh(me)
bm.free()
ob = bpy.data.objects.new(name, me)
# Link object to scene and make active
scn = context.scene
context.collection.objects.link(ob)
context.view_layer.objects.active = ob
ob.select_set(True)
ob0.select_set(False)
# generate new vertex group
for g in ob0.vertex_groups:
ob.vertex_groups.new(name=g.name)
#ob.vertex_groups.new(name=vertex_group_name)
all_weight = weight + [iso_val]*len(verts)
#mult = 1/(1-iso_val)
for id in range(len(all_weight)):
#if False: w = (all_weight[id]-iso_val)*mult
w = all_weight[id]
if self.weight_mode == 'Alternate':
direction = self.bool_flip
for i in range(len(iso_values)-1):
val0, val1 = iso_values[i], iso_values[i+1]
if val0 < w <= val1:
if direction: w1 = (w-val0)/(val1-val0)
else: w1 = (val1-w)/(val1-val0)
direction = not direction
if w < iso_values[0]: w1 = not self.bool_flip
if w > iso_values[-1]: w1 = not direction
elif self.weight_mode == 'Remapped':
if w < min_iso: w1 = 0
elif w > max_iso: w1 = 1
else: w1 = (w - min_iso)/delta_iso
else:
if self.bool_flip: w1 = 1-w
else: w1 = w
ob.vertex_groups[vertex_group_name].add([id], w1, 'REPLACE')
ob.vertex_groups.active_index = group_id
# align new object
ob.matrix_world = ob0.matrix_world
# Displace Modifier
if self.bool_displace:
ob.modifiers.new(type='DISPLACE', name='Displace')
ob.modifiers["Displace"].mid_level = 0
ob.modifiers["Displace"].strength = 0.1
ob.modifiers['Displace'].vertex_group = vertex_group_name
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
print("Contour Displace time: " + str(timeit.default_timer() - start_time) + " sec")
bpy.data.meshes.remove(me0)
return {'FINISHED'}
class weight_contour_mask(Operator):
bl_idname = "object.weight_contour_mask"
bl_label = "Contour Mask"
bl_description = ("")
bl_options = {'REGISTER', 'UNDO'}
use_modifiers : BoolProperty(
name="Use Modifiers", default=True,
description="Apply all the modifiers")
iso : FloatProperty(
name="Iso Value", default=0.5, soft_min=0, soft_max=1,
description="Threshold value")
bool_solidify : BoolProperty(
name="Solidify", default=True, description="Add Solidify Modifier")
offset : FloatProperty(
name="Offset", default=1, min=0, max=1,
description="Offset")
thickness : FloatProperty(
name="Thickness", default=0.5, soft_min=0, soft_max=1,
description="Thickness")
normalize_weight : BoolProperty(
name="Normalize Weight", default=True,
description="Normalize weight of remaining vertices")
@classmethod
def poll(cls, context):
return len(context.object.vertex_groups) > 0
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self, width=350)
def execute(self, context):
start_time = timeit.default_timer()
try:
check = context.object.vertex_groups[0]
except:
self.report({'ERROR'}, "The object doesn't have Vertex Groups")
return {'CANCELLED'}
ob0 = bpy.context.object
iso_val = self.iso
group_id = ob0.vertex_groups.active_index
vertex_group_name = ob0.vertex_groups[group_id].name
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.object.mode_set(mode='OBJECT')
if self.use_modifiers:
me0 = simple_to_mesh(ob0)#ob0.to_mesh(preserve_all_data_layers=True, depsgraph=bpy.context.evaluated_depsgraph_get()).copy()
else:
me0 = ob0.data.copy()
# generate new bmesh
bm = bmesh.new()
bm.from_mesh(me0)
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
bm.faces.ensure_lookup_table()
# store weight values
weight = []
ob = bpy.data.objects.new("temp", me0)
for g in ob0.vertex_groups:
ob.vertex_groups.new(name=g.name)
for v in me0.vertices:
try:
#weight.append(v.groups[vertex_group_name].weight)
weight.append(ob.vertex_groups[vertex_group_name].weight(v.index))
except:
weight.append(0)
faces_mask = []
for f in bm.faces:
w_min = 2
w_max = 2
for v in f.verts:
w = weight[v.index]
if w_min == 2:
w_max = w_min = w
if w > w_max: w_max = w
if w < w_min: w_min = w
if w_min < iso_val and w_max > iso_val:
faces_mask.append(f)
break
filtered_edges = bm.edges# me0.edges
faces_todo = [f.select for f in bm.faces]
verts = []
edges = []
delete_edges = []
edges_id = {}
_filtered_edges = []
n_verts = len(bm.verts)
count = n_verts
for e in filtered_edges:
id0 = e.verts[0].index
id1 = e.verts[1].index
w0 = weight[id0]
w1 = weight[id1]
if w0 == w1: continue
elif w0 > iso_val and w1 > iso_val:
continue
elif w0 < iso_val and w1 < iso_val: continue
elif w0 == iso_val or w1 == iso_val: continue
else:
v0 = me0.vertices[id0].co
v1 = me0.vertices[id1].co
v = v0.lerp(v1, (iso_val-w0)/(w1-w0))
delete_edges.append(e)
verts.append(v)
edges_id[str(id0)+"_"+str(id1)] = count
edges_id[str(id1)+"_"+str(id0)] = count
count += 1
splitted_faces = []
switch = False
# splitting faces
for f in faces_mask:
# create sub-faces slots. Once a new vertex is reached it will
# change slot, storing the next vertices for a new face.
build_faces = [[],[]]
#switch = False
verts0 = list(me0.polygons[f.index].vertices)
verts1 = list(verts0)
verts1.append(verts1.pop(0)) # shift list
for id0, id1 in zip(verts0, verts1):
# add first vertex to active slot
build_faces[switch].append(id0)
# try to split edge
try:
# check if the edge must be splitted
new_vert = edges_id[str(id0)+"_"+str(id1)]
# add new vertex
build_faces[switch].append(new_vert)
# if there is an open face on the other slot
if len(build_faces[not switch]) > 0:
# store actual face
splitted_faces.append(build_faces[switch])
# reset actual faces and switch
build_faces[switch] = []
# change face slot
switch = not switch
# continue previous face
build_faces[switch].append(new_vert)
except: pass
if len(build_faces[not switch]) == 2:
build_faces[not switch].append(id0)
if len(build_faces[not switch]) > 2:
splitted_faces.append(build_faces[not switch])
# add last face
splitted_faces.append(build_faces[switch])
# adding new vertices
_new_vert = bm.verts.new
for v in verts: _new_vert(v)
bm.verts.ensure_lookup_table()
# deleting old edges/faces
_remove_edge = bm.edges.remove
bm.edges.ensure_lookup_table()
remove_edges = []
for e in delete_edges: _remove_edge(e)
bm.verts.ensure_lookup_table()
# adding new faces
_new_face = bm.faces.new
missed_faces = []
for f in splitted_faces:
try:
face_verts = [bm.verts[i] for i in f]
_new_face(face_verts)
except:
missed_faces.append(f)
# Mask geometry
if(True):
_remove_vert = bm.verts.remove
all_weight = weight + [iso_val+0.0001]*len(verts)
weight = []
for w, v in zip(all_weight, bm.verts):
if w < iso_val: _remove_vert(v)
else: weight.append(w)
# Create mesh and object
name = ob0.name + '_ContourMask_{:.3f}'.format(iso_val)
me = bpy.data.meshes.new(name)
bm.to_mesh(me)
bm.free()
ob = bpy.data.objects.new(name, me)
# Link object to scene and make active
scn = context.scene
context.collection.objects.link(ob)
context.view_layer.objects.active = ob
ob.select_set(True)
ob0.select_set(False)
# generate new vertex group
for g in ob0.vertex_groups:
ob.vertex_groups.new(name=g.name)
if iso_val != 1: mult = 1/(1-iso_val)
else: mult = 1
for id in range(len(weight)):
if self.normalize_weight: w = (weight[id]-iso_val)*mult
else: w = weight[id]
ob.vertex_groups[vertex_group_name].add([id], w, 'REPLACE')
ob.vertex_groups.active_index = group_id
# align new object
ob.matrix_world = ob0.matrix_world
# Add Solidify
if self.bool_solidify and True:
ob.modifiers.new(type='SOLIDIFY', name='Solidify')
ob.modifiers['Solidify'].thickness = self.thickness
ob.modifiers['Solidify'].offset = self.offset
ob.modifiers['Solidify'].vertex_group = vertex_group_name
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
print("Contour Mask time: " + str(timeit.default_timer() - start_time) + " sec")
bpy.data.meshes.remove(me0)
return {'FINISHED'}
class weight_contour_mask_wip(Operator):
bl_idname = "object.weight_contour_mask"
bl_label = "Contour Mask"
bl_description = ("")
bl_options = {'REGISTER', 'UNDO'}
use_modifiers : BoolProperty(
name="Use Modifiers", default=True,
description="Apply all the modifiers")
iso : FloatProperty(
name="Iso Value", default=0.5, soft_min=0, soft_max=1,
description="Threshold value")
bool_solidify : BoolProperty(
name="Solidify", default=True, description="Add Solidify Modifier")
normalize_weight : BoolProperty(
name="Normalize Weight", default=True,
description="Normalize weight of remaining vertices")
@classmethod
def poll(cls, context):
return len(context.object.vertex_groups) > 0
def execute(self, context):
start_time = timeit.default_timer()
try:
check = context.object.vertex_groups[0]
except:
self.report({'ERROR'}, "The object doesn't have Vertex Groups")
return {'CANCELLED'}
ob0 = bpy.context.object
iso_val = self.iso
group_id = ob0.vertex_groups.active_index
vertex_group_name = ob0.vertex_groups[group_id].name
#bpy.ops.object.mode_set(mode='EDIT')
#bpy.ops.mesh.select_all(action='SELECT')
#bpy.ops.object.mode_set(mode='OBJECT')
if self.use_modifiers:
me0 = simple_to_mesh(ob0)#ob0.to_mesh(preserve_all_data_layers=True, depsgraph=bpy.context.evaluated_depsgraph_get()).copy()
else:
me0 = ob0.data.copy()
# generate new bmesh
bm = bmesh.new()
bm.from_mesh(me0)
# store weight values
weight = []
ob = bpy.data.objects.new("temp", me0)
for g in ob0.vertex_groups:
ob.vertex_groups.new(name=g.name)
weight = get_weight_numpy(ob.vertex_groups[vertex_group_name], len(me0.vertices))
me0, bm, weight = contour_bmesh(me0, bm, weight, iso_val)
# Mask geometry
mask = weight >= iso_val
weight = weight[mask]
mask = np.logical_not(mask)
delete_verts = np.array(bm.verts)[mask]
#for v in delete_verts: bm.verts.remove(v)
# Create mesh and object
name = ob0.name + '_ContourMask_{:.3f}'.format(iso_val)
me = bpy.data.meshes.new(name)
bm.to_mesh(me)
bm.free()
ob = bpy.data.objects.new(name, me)
# Link object to scene and make active
scn = context.scene
context.collection.objects.link(ob)
context.view_layer.objects.active = ob
ob.select_set(True)
ob0.select_set(False)
# generate new vertex group
for g in ob0.vertex_groups:
ob.vertex_groups.new(name=g.name)
if iso_val != 1: mult = 1/(1-iso_val)
else: mult = 1
for id in range(len(weight)):
if self.normalize_weight: w = (weight[id]-iso_val)*mult
else: w = weight[id]
ob.vertex_groups[vertex_group_name].add([id], w, 'REPLACE')
ob.vertex_groups.active_index = group_id
# align new object
ob.matrix_world = ob0.matrix_world
# Add Solidify
if self.bool_solidify and True:
ob.modifiers.new(type='SOLIDIFY', name='Solidify')
ob.modifiers['Solidify'].thickness = 0.05
ob.modifiers['Solidify'].offset = 0
ob.modifiers['Solidify'].vertex_group = vertex_group_name
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
print("Contour Mask time: " + str(timeit.default_timer() - start_time) + " sec")
bpy.data.meshes.remove(me0)
return {'FINISHED'}
class weight_contour_curves(Operator):
bl_idname = "object.weight_contour_curves"
bl_label = "Contour Curves"
bl_description = ("")
bl_options = {'REGISTER', 'UNDO'}
use_modifiers : BoolProperty(
name="Use Modifiers", default=True,
description="Apply all the modifiers")
min_iso : FloatProperty(
name="Min Value", default=0., soft_min=0, soft_max=1,
description="Minimum weight value")
max_iso : FloatProperty(
name="Max Value", default=1, soft_min=0, soft_max=1,
description="Maximum weight value")
n_curves : IntProperty(
name="Curves", default=3, soft_min=1, soft_max=10,
description="Number of Contour Curves")
min_rad : FloatProperty(
name="Min Radius", default=1, soft_min=0, soft_max=1,
description="Change radius according to Iso Value")
max_rad : FloatProperty(
name="Max Radius", default=1, soft_min=0, soft_max=1,
description="Change radius according to Iso Value")
@classmethod
def poll(cls, context):
ob = context.object
return len(ob.vertex_groups) > 0 or ob.type == 'CURVE'
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self, width=350)
def execute(self, context):
start_time = timeit.default_timer()
try:
check = context.object.vertex_groups[0]
except:
self.report({'ERROR'}, "The object doesn't have Vertex Groups")
return {'CANCELLED'}
ob0 = context.object
group_id = ob0.vertex_groups.active_index
vertex_group_name = ob0.vertex_groups[group_id].name
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.object.mode_set(mode='OBJECT')
if self.use_modifiers:
me0 = simple_to_mesh(ob0) #ob0.to_mesh(preserve_all_data_layers=True, depsgraph=bpy.context.evaluated_depsgraph_get()).copy()
else:
me0 = ob0.data.copy()
# generate new bmesh
bm = bmesh.new()
bm.from_mesh(me0)
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
bm.faces.ensure_lookup_table()
# store weight values
weight = []
ob = bpy.data.objects.new("temp", me0)
for g in ob0.vertex_groups:
ob.vertex_groups.new(name=g.name)
weight = get_weight_numpy(ob.vertex_groups[vertex_group_name], len(bm.verts))
#filtered_edges = bm.edges
total_verts = np.zeros((0,3))
total_segments = []
radius = []
# start iterate contours levels
vertices = get_vertices_numpy(me0)
filtered_edges = get_edges_id_numpy(me0)
faces_weight = [np.array([weight[v] for v in p.vertices]) for p in me0.polygons]
fw_min = np.array([np.min(fw) for fw in faces_weight])
fw_max = np.array([np.max(fw) for fw in faces_weight])
bm_faces = np.array(bm.faces)
### Spiral
normals = np.array([v.normal for v in me0.vertices])
for c in range(self.n_curves):
min_iso = min(self.min_iso, self.max_iso)
max_iso = max(self.min_iso, self.max_iso)
try:
delta_iso = (max_iso-min_iso)/(self.n_curves-1)
iso_val = c*delta_iso + min_iso
if iso_val < 0: iso_val = (min_iso + max_iso)/2
except:
iso_val = (min_iso + max_iso)/2
# remove passed faces
bool_mask = iso_val < fw_max
bm_faces = bm_faces[bool_mask]
fw_min = fw_min[bool_mask]
fw_max = fw_max[bool_mask]
# mask faces
bool_mask = fw_min < iso_val
faces_mask = bm_faces[bool_mask]
n_verts = len(bm.verts)
count = len(total_verts)
# vertices indexes
id0 = filtered_edges[:,0]
id1 = filtered_edges[:,1]
# vertices weight
w0 = weight[id0]
w1 = weight[id1]
# weight condition
bool_w0 = w0 < iso_val
bool_w1 = w1 < iso_val
# mask all edges that have one weight value below the iso value
mask_new_verts = np.logical_xor(bool_w0, bool_w1)
id0 = id0[mask_new_verts]
id1 = id1[mask_new_verts]
# filter arrays
v0 = vertices[id0]
v1 = vertices[id1]
w0 = w0[mask_new_verts]
w1 = w1[mask_new_verts]
div = (w1-w0)
if div == 0: div = 0.000001
param = np.expand_dims((iso_val-w0)/div,axis=1)
verts = v0 + (v1-v0)*param
# indexes of edges with new vertices
edges_index = filtered_edges[mask_new_verts][:,2]
edges_id = {}
for i, id in enumerate(edges_index): edges_id[id] = i+len(total_verts)
# remove all edges completely below the iso value
mask_edges = np.logical_not(np.logical_and(bool_w0, bool_w1))
filtered_edges = filtered_edges[mask_edges]
if len(verts) == 0: continue
# finding segments
segments = []
for f in faces_mask:
seg = []
for e in f.edges:
try:
seg.append(edges_id[e.index])
if len(seg) == 2:
segments.append(seg)
seg = []
except: pass
#curves_points_indexes = find_curves(segments)
total_segments = total_segments + segments
total_verts = np.concatenate((total_verts,verts))
if self.min_rad != self.max_rad:
try:
iso_rad = c*(self.max_rad-self.min_rad)/(self.n_curves-1)+self.min_rad
if iso_rad < 0: iso_rad = (self.min_rad + self.max_rad)/2
except:
iso_rad = (self.min_rad + self.max_rad)/2
radius = radius + [iso_rad]*len(verts)
print("Contour Curves, computing time: " + str(timeit.default_timer() - start_time) + " sec")
bm.free()
bm = bmesh.new()
# adding new vertices _local for fast access
_new_vert = bm.verts.new
for v in total_verts: _new_vert(v)
bm.verts.ensure_lookup_table()
# adding new edges
_new_edge = bm.edges.new
for s in total_segments:
try:
pts = [bm.verts[i] for i in s]
_new_edge(pts)
except: pass
try:
name = ob0.name + '_ContourCurves'
me = bpy.data.meshes.new(name)
bm.to_mesh(me)
bm.free()
ob = bpy.data.objects.new(name, me)
# Link object to scene and make active
scn = context.scene
context.collection.objects.link(ob)
context.view_layer.objects.active = ob
ob.select_set(True)
ob0.select_set(False)
print("Contour Curves, bmesh time: " + str(timeit.default_timer() - start_time) + " sec")
bpy.ops.object.convert(target='CURVE')
ob = context.object
if not (self.min_rad == 0 and self.max_rad == 0):
if self.min_rad != self.max_rad:
count = 0
for s in ob.data.splines:
for p in s.points:
p.radius = radius[count]
count += 1
else:
for s in ob.data.splines:
for p in s.points:
p.radius = self.min_rad
ob.data.bevel_depth = 0.01
ob.data.fill_mode = 'FULL'
ob.data.bevel_resolution = 3
except:
self.report({'ERROR'}, "There are no values in the chosen range")
return {'CANCELLED'}
# align new object
ob.matrix_world = ob0.matrix_world
print("Contour Curves time: " + str(timeit.default_timer() - start_time) + " sec")
bpy.data.meshes.remove(me0)
bpy.data.meshes.remove(me)
return {'FINISHED'}
class tissue_weight_contour_curves_pattern(Operator):
bl_idname = "object.tissue_weight_contour_curves_pattern"
bl_label = "Contour Curves"
bl_description = ("")
bl_options = {'REGISTER', 'UNDO'}
use_modifiers : BoolProperty(
name="Use Modifiers", default=True,
description="Apply all the modifiers")
auto_bevel : BoolProperty(
name="Automatic Bevel", default=False,
description="Bevel depends on weight density")
min_iso : FloatProperty(
name="Min Value", default=0., soft_min=0, soft_max=1,
description="Minimum weight value")
max_iso : FloatProperty(
name="Max Value", default=1, soft_min=0, soft_max=1,
description="Maximum weight value")
n_curves : IntProperty(
name="Curves", default=10, soft_min=1, soft_max=100,
description="Number of Contour Curves")
min_rad = 1
max_rad = 1
in_displace : FloatProperty(
name="Displace A", default=0, soft_min=-10, soft_max=10,
description="Pattern displace strength")
out_displace : FloatProperty(
name="Displace B", default=2, soft_min=-10, soft_max=10,
description="Pattern displace strength")
in_steps : IntProperty(
name="Steps A", default=1, min=0, soft_max=10,
description="Number of layers to move inwards")
out_steps : IntProperty(
name="Steps B", default=1, min=0, soft_max=10,
description="Number of layers to move outwards")
limit_z : BoolProperty(
name="Limit Z", default=False,
description="Limit Pattern in Z")
merge : BoolProperty(
name="Merge Vertices", default=True,
description="Merge points")
merge_thres : FloatProperty(
name="Merge Threshold", default=0.01, min=0, soft_max=1,
description="Minimum Curve Radius")
bevel_depth : FloatProperty(
name="Bevel Depth", default=0, min=0, soft_max=1,
description="")
min_bevel_depth : FloatProperty(
name="Min Bevel Depth", default=0.1, min=0, soft_max=1,
description="")
max_bevel_depth : FloatProperty(
name="Max Bevel Depth", default=1, min=0, soft_max=1,
description="")
remove_open_curves : BoolProperty(
name="Remove Open Curves", default=False,
description="Remove Open Curves")
vertex_group_pattern : StringProperty(
name="Displace", default='',
description="Vertex Group used for pattern displace")
vertex_group_bevel : StringProperty(
name="Bevel", default='',
description="Variable Bevel depth")
object_name : StringProperty(
name="Active Object", default='',
description="")
try: vg_name = bpy.context.object.vertex_groups.active.name
except: vg_name = ''
vertex_group_contour : StringProperty(
name="Contour", default=vg_name,
description="Vertex Group used for contouring")
clean_distance : FloatProperty(
name="Clean Distance", default=0, min=0, soft_max=10,
description="Remove short segments")
spiralized: BoolProperty(
name='Spiralized', default=False,
description='Create a Spiral Contour. Works better with dense meshes.'
)
spiral_axis: FloatVectorProperty(
name="Spiral Axis", default=(0,0,1),
description="Axis of the Spiral (in local coordinates)"
)
spiral_rotation : FloatProperty(
name="Spiral Rotation", default=0, min=0, max=2*pi,
description=""
)
@classmethod
def poll(cls, context):
ob = context.object
return ob and len(ob.vertex_groups) > 0 or ob.type == 'CURVE'
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self, width=250)
def draw(self, context):
if not context.object.type == 'CURVE':
self.object_name = context.object.name
ob = bpy.data.objects[self.object_name]
if self.vertex_group_contour not in [vg.name for vg in ob.vertex_groups]:
self.vertex_group_contour = ob.vertex_groups.active.name
layout = self.layout
col = layout.column(align=True)
col.prop(self, "use_modifiers")
col.label(text="Contour Curves:")
col.prop_search(self, 'vertex_group_contour', ob, "vertex_groups", text='')
row = col.row(align=True)
row.prop(self,'min_iso')
row.prop(self,'max_iso')
col.prop(self,'n_curves')
col.separator()
col.label(text='Curves Bevel:')
col.prop(self,'auto_bevel')
if not self.auto_bevel:
col.prop_search(self, 'vertex_group_bevel', ob, "vertex_groups", text='')
if self.vertex_group_bevel != '' or self.auto_bevel:
row = col.row(align=True)
row.prop(self,'min_bevel_depth')
row.prop(self,'max_bevel_depth')
else:
col.prop(self,'bevel_depth')
col.separator()
col.label(text="Displace Pattern:")
col.prop_search(self, 'vertex_group_pattern', ob, "vertex_groups", text='')
if self.vertex_group_pattern != '':
row = col.row(align=True)
row.prop(self,'in_steps')
row.prop(self,'out_steps')
row = col.row(align=True)
row.prop(self,'in_displace')
row.prop(self,'out_displace')
col.prop(self,'limit_z')
col.separator()
row=col.row(align=True)
row.prop(self,'spiralized')
row.label(icon='MOD_SCREW')
if self.spiralized:
#row=col.row(align=True)
#row.prop(self,'spiral_axis')
#col.separator()
col.prop(self,'spiral_rotation')
col.separator()
col.label(text='Clean Curves:')
col.prop(self,'clean_distance')
col.prop(self,'remove_open_curves')
def execute(self, context):
n_curves = self.n_curves
start_time = timeit.default_timer()
try:
check = context.object.vertex_groups[0]
except:
self.report({'ERROR'}, "The object doesn't have Vertex Groups")
return {'CANCELLED'}
ob0 = bpy.data.objects[self.object_name]
dg = context.evaluated_depsgraph_get()
ob = ob0.evaluated_get(dg)
me0 = ob.data
# generate new bmesh
bm = bmesh.new()
bm.from_mesh(me0)
n_verts = len(bm.verts)
# store weight values
try:
weight = get_weight_numpy(ob.vertex_groups[self.vertex_group_contour], len(me0.vertices))
except:
bm.free()
self.report({'ERROR'}, "Please select a Vertex Group for contouring")
return {'CANCELLED'}
try:
pattern_weight = get_weight_numpy(ob.vertex_groups[self.vertex_group_pattern], len(me0.vertices))
except:
#self.report({'WARNING'}, "There is no Vertex Group assigned to the pattern displace")
pattern_weight = np.zeros(len(me0.vertices))
variable_bevel = False
try:
bevel_weight = get_weight_numpy(ob.vertex_groups[self.vertex_group_bevel], len(me0.vertices))
variable_bevel = True
except:
bevel_weight = np.ones(len(me0.vertices))
if self.auto_bevel:
# calc weight density
bevel_weight = np.ones(len(me0.vertices))*10000
bevel_weight = np.zeros(len(me0.vertices))
edges_length = np.array([e.calc_length() for e in bm.edges])
edges_dw = np.array([max(abs(weight[e.verts[0].index]-weight[e.verts[1].index]),0.000001) for e in bm.edges])
dens = edges_length/edges_dw
n_records = np.zeros(len(me0.vertices))
for i, e in enumerate(bm.edges):
for v in e.verts:
id = v.index
#bevel_weight[id] = min(bevel_weight[id], dens[i])
bevel_weight[id] += dens[i]
n_records[id] += 1
bevel_weight = bevel_weight/n_records
bevel_weight = (bevel_weight - min(bevel_weight))/(max(bevel_weight) - min(bevel_weight))
#bevel_weight = 1-bevel_weight
variable_bevel = True
#filtered_edges = bm.edges
total_verts = np.zeros((0,3))
total_radii = np.zeros((0,1))
total_segments = []# np.array([])
radius = []
# start iterate contours levels
vertices, normals = get_vertices_and_normals_numpy(me0)
filtered_edges = get_edges_id_numpy(me0)
min_iso = min(self.min_iso, self.max_iso)
max_iso = max(self.min_iso, self.max_iso)
# Spiral
if self.spiralized:
nx = normals[:,0]
ny = normals[:,1]
ang = self.spiral_rotation + weight*pi*n_curves+arctan2(nx,ny)
weight = sin(ang)/2+0.5
n_curves = 1
if n_curves > 1:
delta_iso = (max_iso-min_iso)/(n_curves-1)
else:
delta_iso = None
faces_weight = [np.array([weight[v] for v in p.vertices]) for p in me0.polygons]
fw_min = np.array([np.min(fw) for fw in faces_weight])
fw_max = np.array([np.max(fw) for fw in faces_weight])
bm_faces = np.array(bm.faces)
#print("Contour Curves, data loaded: " + str(timeit.default_timer() - start_time) + " sec")
step_time = timeit.default_timer()
for c in range(n_curves):
if delta_iso:
iso_val = c*delta_iso + min_iso
if iso_val < 0: iso_val = (min_iso + max_iso)/2
else:
iso_val = (min_iso + max_iso)/2
#if c == 0 and self.auto_bevel:
# remove passed faces
bool_mask = iso_val < fw_max
bm_faces = bm_faces[bool_mask]
fw_min = fw_min[bool_mask]
fw_max = fw_max[bool_mask]
# mask faces
bool_mask = fw_min < iso_val
faces_mask = bm_faces[bool_mask]
count = len(total_verts)
new_filtered_edges, edges_index, verts, bevel = contour_edges_pattern(self, c, len(total_verts), iso_val, vertices, normals, filtered_edges, weight, pattern_weight, bevel_weight)
if len(edges_index) > 0:
if self.auto_bevel and False:
bevel = 1-dens[edges_index]
bevel = bevel[:,np.newaxis]
if self.max_bevel_depth != self.min_bevel_depth:
min_radius = self.min_bevel_depth / max(0.0001,self.max_bevel_depth)
radii = min_radius + bevel*(1 - min_radius)
else:
radii = bevel
else:
continue
if verts[0,0] == None: continue
else: filtered_edges = new_filtered_edges
edges_id = {}
for i, id in enumerate(edges_index): edges_id[id] = i + count
if len(verts) == 0: continue
# finding segments
segments = []
for f in faces_mask:
seg = []
for e in f.edges:
try:
#seg.append(new_ids[np.where(edges_index == e.index)[0][0]])
seg.append(edges_id[e.index])
if len(seg) == 2:
segments.append(seg)
seg = []
except: pass
total_segments = total_segments + segments
total_verts = np.concatenate((total_verts, verts))
total_radii = np.concatenate((total_radii, radii))
if self.min_rad != self.max_rad:
try:
iso_rad = c*(self.max_rad-self.min_rad)/(self.n_curves-1)+self.min_rad
if iso_rad < 0: iso_rad = (self.min_rad + self.max_rad)/2
except:
iso_rad = (self.min_rad + self.max_rad)/2
radius = radius + [iso_rad]*len(verts)
#print("Contour Curves, points computing: " + str(timeit.default_timer() - step_time) + " sec")
step_time = timeit.default_timer()
if len(total_segments) > 0:
step_time = timeit.default_timer()
ordered_points = find_curves(total_segments, len(total_verts))
#print("Contour Curves, point ordered in: " + str(timeit.default_timer() - step_time) + " sec")
step_time = timeit.default_timer()
crv = curve_from_pydata(total_verts, total_radii, ordered_points, ob0.name + '_ContourCurves', self.remove_open_curves, merge_distance=self.clean_distance)
context.view_layer.objects.active = crv
if variable_bevel: crv.data.bevel_depth = self.max_bevel_depth
else: crv.data.bevel_depth = self.bevel_depth
crv.select_set(True)
ob0.select_set(False)
crv.matrix_world = ob0.matrix_world
#print("Contour Curves, curves created in: " + str(timeit.default_timer() - step_time) + " sec")
else:
bm.free()
self.report({'ERROR'}, "There are no values in the chosen range")
return {'CANCELLED'}
bm.free()
print("Contour Curves, total time: " + str(timeit.default_timer() - start_time) + " sec")
return {'FINISHED'}
class vertex_colors_to_vertex_groups(Operator):
bl_idname = "object.vertex_colors_to_vertex_groups"
bl_label = "Vertex Color"
bl_options = {'REGISTER', 'UNDO'}
bl_description = ("Convert the active Vertex Color into a Vertex Group.")
red : BoolProperty(
name="red channel", default=False, description="convert red channel")
green : BoolProperty(
name="green channel", default=False,
description="convert green channel")
blue : BoolProperty(
name="blue channel", default=False, description="convert blue channel")
value : BoolProperty(
name="value channel", default=True, description="convert value channel")
invert : BoolProperty(
name="invert", default=False, description="invert all color channels")
@classmethod
def poll(cls, context):
try:
return len(context.object.data.vertex_colors) > 0
except: return False
def execute(self, context):
obj = context.active_object
id = len(obj.vertex_groups)
id_red = id
id_green = id
id_blue = id
id_value = id
boolCol = len(obj.data.vertex_colors)
if(boolCol): col_name = obj.data.vertex_colors.active.name
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
if(self.red and boolCol):
bpy.ops.object.vertex_group_add()
bpy.ops.object.vertex_group_assign()
id_red = id
obj.vertex_groups[id_red].name = col_name + '_red'
id+=1
if(self.green and boolCol):
bpy.ops.object.vertex_group_add()
bpy.ops.object.vertex_group_assign()
id_green = id
obj.vertex_groups[id_green].name = col_name + '_green'
id+=1
if(self.blue and boolCol):
bpy.ops.object.vertex_group_add()
bpy.ops.object.vertex_group_assign()
id_blue = id
obj.vertex_groups[id_blue].name = col_name + '_blue'
id+=1
if(self.value and boolCol):
bpy.ops.object.vertex_group_add()
bpy.ops.object.vertex_group_assign()
id_value = id
obj.vertex_groups[id_value].name = col_name + '_value'
id+=1
mult = 1
if(self.invert): mult = -1
bpy.ops.object.mode_set(mode='OBJECT')
sub_red = 1 + self.value + self.blue + self.green
sub_green = 1 + self.value + self.blue
sub_blue = 1 + self.value
sub_value = 1
id = len(obj.vertex_groups)
if(id_red <= id and id_green <= id and id_blue <= id and id_value <= \
id and boolCol):
v_colors = obj.data.vertex_colors.active.data
i = 0
for f in obj.data.polygons:
for v in f.vertices:
gr = obj.data.vertices[v].groups
if(self.red): gr[min(len(gr)-sub_red, id_red)].weight = \
self.invert + mult * v_colors[i].color[0]
if(self.green): gr[min(len(gr)-sub_green, id_green)].weight\
= self.invert + mult * v_colors[i].color[1]
if(self.blue): gr[min(len(gr)-sub_blue, id_blue)].weight = \
self.invert + mult * v_colors[i].color[2]
if(self.value):
r = v_colors[i].color[0]
g = v_colors[i].color[1]
b = v_colors[i].color[2]
gr[min(len(gr)-sub_value, id_value)].weight\
= self.invert + mult * (0.2126*r + 0.7152*g + 0.0722*b)
i+=1
bpy.ops.paint.weight_paint_toggle()
return {'FINISHED'}
class vertex_group_to_vertex_colors(Operator):
bl_idname = "object.vertex_group_to_vertex_colors"
bl_label = "Vertex Group"
bl_options = {'REGISTER', 'UNDO'}
bl_description = ("Convert the active Vertex Group into a Vertex Color.")
channel : EnumProperty(
items=[('BLUE', 'Blue Channel', 'Convert to Blue Channel'),
('GREEN', 'Green Channel', 'Convert to Green Channel'),
('RED', 'Red Channel', 'Convert to Red Channel'),
('VALUE', 'Value Channel', 'Convert to Grayscale'),
('FALSE_COLORS', 'False Colors', 'Convert to False Colors')],
name="Convert to", description="Choose how to convert vertex group",
default="VALUE", options={'LIBRARY_EDITABLE'})
invert : BoolProperty(
name="invert", default=False, description="invert color channel")
@classmethod
def poll(cls, context):
return len(context.object.vertex_groups) > 0
def execute(self, context):
obj = context.active_object
me = obj.data
group_id = obj.vertex_groups.active_index
if (group_id == -1):
return {'FINISHED'}
bpy.ops.object.mode_set(mode='OBJECT')
group_name = obj.vertex_groups[group_id].name
me.vertex_colors.new()
colors_id = obj.data.vertex_colors.active_index
colors_name = group_name
if(self.channel == 'FALSE_COLORS'): colors_name += "_false_colors"
elif(self.channel == 'VALUE'): colors_name += "_value"
elif(self.channel == 'RED'): colors_name += "_red"
elif(self.channel == 'GREEN'): colors_name += "_green"
elif(self.channel == 'BLUE'): colors_name += "_blue"
context.object.data.vertex_colors[colors_id].name = colors_name
v_colors = obj.data.vertex_colors.active.data
bm = bmesh.new()
bm.from_mesh(me)
dvert_lay = bm.verts.layers.deform.active
weight = bmesh_get_weight_numpy(group_id,dvert_lay,bm.verts)
if self.invert: weight = 1-weight
loops_size = get_attribute_numpy(me.polygons, attribute='loop_total', mult=1)
n_colors = np.sum(loops_size)
verts = np.ones(n_colors)
me.polygons.foreach_get('vertices',verts)
splitted_weight = weight[verts.astype(int)][:,None]
r = np.zeros(splitted_weight.shape)
g = np.zeros(splitted_weight.shape)
b = np.zeros(splitted_weight.shape)
a = np.ones(splitted_weight.shape)
if(self.channel == 'FALSE_COLORS'):
mult = 0.6+0.4*splitted_weight
mask = splitted_weight < 0.25
g[mask] = splitted_weight[mask]*4
b[mask] = np.ones(splitted_weight.shape)[mask]
mask = np.where(np.logical_and(splitted_weight>=0.25, splitted_weight<0.5))
g[mask] = np.ones(splitted_weight.shape)[mask]
b[mask] = (1-(splitted_weight[mask]-0.25)*4)
mask = np.where(np.logical_and(splitted_weight>=0.5, splitted_weight<0.75))
r[mask] = (splitted_weight[mask]-0.5)*4
g[mask] = np.ones(splitted_weight.shape)[mask]
mask = 0.75 <= splitted_weight
r[mask] = np.ones(splitted_weight.shape)[mask]
g[mask] = (1-(splitted_weight[mask]-0.75)*4)
elif(self.channel == 'VALUE'):
r = splitted_weight
g = splitted_weight
b = splitted_weight
elif(self.channel == 'RED'):
r = splitted_weight
elif(self.channel == 'GREEN'):
g = splitted_weight
elif(self.channel == 'BLUE'):
b = splitted_weight
colors = np.concatenate((r,g,b,a),axis=1).flatten()
v_colors.foreach_set('color',colors)
bpy.ops.paint.vertex_paint_toggle()
context.object.data.vertex_colors[colors_id].active_render = True
return {'FINISHED'}
class vertex_group_to_uv(Operator):
bl_idname = "object.vertex_group_to_uv"
bl_label = "Vertex Group"
bl_options = {'REGISTER', 'UNDO'}
bl_description = ("Combine two Vertex Groups as UV Map Layer.")
vertex_group_u : StringProperty(
name="U", default='',
description="Vertex Group used for the U coordinate")
vertex_group_v : StringProperty(
name="V", default='',
description="Vertex Group used for the V coordinate")
normalize_weight : BoolProperty(
name="Normalize Weight", default=True,
description="Normalize weight values")
invert_u : BoolProperty(
name="Invert U", default=False, description="Invert U")
invert_v : BoolProperty(
name="Invert V", default=False, description="Invert V")
@classmethod
def poll(cls, context):
return len(context.object.vertex_groups) > 0
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self, width=250)
def draw(self, context):
ob = context.object
layout = self.layout
col = layout.column(align=True)
row = col.row(align=True)
row.prop_search(self, 'vertex_group_u', ob, "vertex_groups", text='')
row.separator()
row.prop_search(self, 'vertex_group_v', ob, "vertex_groups", text='')
row = col.row(align=True)
row.prop(self, "invert_u")
row.separator()
row.prop(self, "invert_v")
row = col.row(align=True)
row.prop(self, "normalize_weight")
def execute(self, context):
ob = context.active_object
me = ob.data
n_verts = len(me.vertices)
vg_keys = ob.vertex_groups.keys()
bool_u = self.vertex_group_u in vg_keys
bool_v = self.vertex_group_v in vg_keys
if bool_u or bool_v:
bm = bmesh.new()
bm.from_mesh(me)
dvert_lay = bm.verts.layers.deform.active
if bool_u:
u_index = ob.vertex_groups[self.vertex_group_u].index
u = bmesh_get_weight_numpy(u_index, dvert_lay, bm.verts)
if self.invert_u:
u = 1-u
if self.normalize_weight:
u = np.interp(u, (u.min(), u.max()), (0, 1))
else:
u = np.zeros(n_verts)
if bool_v:
v_index = ob.vertex_groups[self.vertex_group_v].index
v = bmesh_get_weight_numpy(v_index, dvert_lay, bm.verts)
if self.invert_v:
v = 1-v
if self.normalize_weight:
v = np.interp(v, (v.min(), v.max()), (0, 1))
else:
v = np.zeros(n_verts)
else:
u = v = np.zeros(n_verts)
uv_layer = me.uv_layers.new(name='Weight_to_UV')
loops_size = get_attribute_numpy(me.polygons, attribute='loop_total', mult=1)
n_data = np.sum(loops_size)
v_id = np.ones(n_data)
me.polygons.foreach_get('vertices',v_id)
v_id = v_id.astype(int)
split_u = u[v_id,None]
split_v = v[v_id,None]
uv = np.concatenate((split_u,split_v),axis=1).flatten()
uv_layer.data.foreach_set('uv',uv)
me.uv_layers.update()
return {'FINISHED'}
class curvature_to_vertex_groups(Operator):
bl_idname = "object.curvature_to_vertex_groups"
bl_label = "Curvature"
bl_options = {'REGISTER', 'UNDO'}
bl_description = ("Generate a Vertex Group based on the curvature of the"
"mesh. Is based on Dirty Vertex Color.")
invert : BoolProperty(
name="invert", default=False, description="invert values")
blur_strength : FloatProperty(
name="Blur Strength", default=1, min=0.001,
max=1, description="Blur strength per iteration")
blur_iterations : IntProperty(
name="Blur Iterations", default=1, min=0,
max=40, description="Number of times to blur the values")
min_angle : FloatProperty(
name="Min Angle", default=0, min=0,
max=pi/2, subtype='ANGLE', description="Minimum angle")
max_angle : FloatProperty(
name="Max Angle", default=pi, min=pi/2,
max=pi, subtype='ANGLE', description="Maximum angle")
invert : BoolProperty(
name="Invert", default=False,
description="Invert the curvature map")
def execute(self, context):
bpy.ops.object.mode_set(mode='OBJECT')
vertex_colors = context.active_object.data.vertex_colors
vertex_colors.new()
vertex_colors[-1].active = True
vertex_colors[-1].active_render = True
vertex_colors[-1].name = "Curvature"
for c in vertex_colors[-1].data: c.color = (1,1,1,1)
bpy.ops.object.mode_set(mode='VERTEX_PAINT')
bpy.ops.paint.vertex_color_dirt(
blur_strength=self.blur_strength,
blur_iterations=self.blur_iterations, clean_angle=self.max_angle,
dirt_angle=self.min_angle)
bpy.ops.object.vertex_colors_to_vertex_groups(invert=self.invert)
vertex_colors.remove(vertex_colors.active)
return {'FINISHED'}
class face_area_to_vertex_groups(Operator):
bl_idname = "object.face_area_to_vertex_groups"
bl_label = "Area"
bl_options = {'REGISTER', 'UNDO'}
bl_description = ("Generate a Vertex Group based on the area of individual"
"faces.")
invert : BoolProperty(
name="invert", default=False, description="invert values")
bounds : EnumProperty(
items=(('MANUAL', "Manual Bounds", ""),
('AUTOMATIC', "Automatic Bounds", "")),
default='AUTOMATIC', name="Bounds")
min_area : FloatProperty(
name="Min", default=0.01, soft_min=0, soft_max=1,
description="Faces with 0 weight")
max_area : FloatProperty(
name="Max", default=0.1, soft_min=0, soft_max=1,
description="Faces with 1 weight")
def draw(self, context):
layout = self.layout
layout.label(text="Bounds")
layout.prop(self, "bounds", text="")
if self.bounds == 'MANUAL':
layout.prop(self, "min_area")
layout.prop(self, "max_area")
def execute(self, context):
try: ob = context.object
except:
self.report({'ERROR'}, "Please select an Object")
return {'CANCELLED'}
ob.vertex_groups.new(name="Faces Area")
areas = [[] for v in ob.data.vertices]
for p in ob.data.polygons:
for v in p.vertices:
areas[v].append(p.area)
for i in range(len(areas)):
areas[i] = mean(areas[i])
if self.bounds == 'MANUAL':
min_area = self.min_area
max_area = self.max_area
elif self.bounds == 'AUTOMATIC':
min_area = min(areas)
max_area = max(areas)
elif self.bounds == 'COMPRESSION':
min_area = 1
max_area = min(areas)
elif self.bounds == 'TENSION':
min_area = 1
max_area = max(areas)
delta_area = max_area - min_area
if delta_area == 0:
delta_area = 0.0001
if self.bounds == 'MANUAL':
delta_area = 0.0001
else:
self.report({'ERROR'}, "The faces have the same areas")
#return {'CANCELLED'}
for i in range(len(areas)):
weight = (areas[i] - min_area)/delta_area
ob.vertex_groups[-1].add([i], weight, 'REPLACE')
ob.vertex_groups.update()
ob.data.update()
bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
return {'FINISHED'}
class random_weight(Operator):
bl_idname = "object.random_weight"
bl_label = "Random"
bl_options = {'REGISTER', 'UNDO'}
bl_description = ("Generate a random Vertex Group")
min_val : FloatProperty(
name="Min", default=0, soft_min=0, soft_max=1,
description="Minimum Value")
max_val : FloatProperty(
name="Max", default=1, soft_min=0, soft_max=1,
description="Maximum Value")
#def draw(self, context):
# layout = self.layout
# layout.prop(self, "min_area")
# layout.prop(self, "max_area")
@classmethod
def poll(cls, context):
return len(context.object.vertex_groups) > 0
def execute(self, context):
try: ob = context.object
except:
self.report({'ERROR'}, "Please select an Object")
return {'CANCELLED'}
#ob.vertex_groups.new(name="Random")
n_verts = len(ob.data.vertices)
weight = np.random.uniform(low=self.min_val, high=self.max_val, size=(n_verts,))
np.clip(weight, 0, 1, out=weight)
group_id = ob.vertex_groups.active_index
for i in range(n_verts):
ob.vertex_groups[group_id].add([i], weight[i], 'REPLACE')
ob.vertex_groups.update()
ob.data.update()
bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
return {'FINISHED'}
class harmonic_weight(Operator):
bl_idname = "object.harmonic_weight"
bl_label = "Harmonic"
bl_options = {'REGISTER', 'UNDO'}
bl_description = ("Create an harmonic variation of the active Vertex Group")
freq : FloatProperty(
name="Frequency", default=20, soft_min=0,
soft_max=100, description="Wave frequency")
amp : FloatProperty(
name="Amplitude", default=1, soft_min=0,
soft_max=10, description="Wave amplitude")
midlevel : FloatProperty(
name="Midlevel", default=0, min=-1,
max=1, description="Midlevel")
add : FloatProperty(
name="Add", default=0, min=-1,
max=1, description="Add to the Weight")
mult : FloatProperty(
name="Multiply", default=0, min=0,
max=1, description="Multiply for he Weight")
@classmethod
def poll(cls, context):
return len(context.object.vertex_groups) > 0
def execute(self, context):
ob = context.active_object
if len(ob.vertex_groups) > 0:
group_id = ob.vertex_groups.active_index
ob.vertex_groups.new(name="Harmonic")
for i in range(len(ob.data.vertices)):
try: val = ob.vertex_groups[group_id].weight(i)
except: val = 0
weight = self.amp*(math.sin(val*self.freq) - self.midlevel)/2 + 0.5 + self.add*val*(1-(1-val)*self.mult)
ob.vertex_groups[-1].add([i], weight, 'REPLACE')
ob.data.update()
else:
self.report({'ERROR'}, "Active object doesn't have vertex groups")
return {'CANCELLED'}
bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
return {'FINISHED'}
class tissue_weight_distance(Operator):
bl_idname = "object.tissue_weight_distance"
bl_label = "Weight Distance"
bl_options = {'REGISTER', 'UNDO'}
bl_description = ("Create a weight map according to the distance from the "
"selected vertices along the mesh surface")
mode : EnumProperty(
items=(('GEOD', "Geodesic Distance", ""),
('EUCL', "Euclidean Distance", ""),
('TOPO', "Topology Distance", "")),
default='GEOD', name="Distance Method")
normalize : BoolProperty(
name="Normalize", default=True,
description="Automatically remap the distance values from 0 to 1")
min_value : FloatProperty(
name="Min", default=0, min=0,
soft_max=100, description="Minimum Distance")
max_value : FloatProperty(
name="Max", default=10, min=0,
soft_max=100, description="Max Distance")
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self, width=250)
def fill_neighbors(self,verts,weight):
neigh = {}
for v0 in verts:
for f in v0.link_faces:
for v1 in f.verts:
if self.mode == 'GEOD':
dist = weight[v0.index] + (v0.co-v1.co).length
elif self.mode == 'TOPO':
dist = weight[v0.index] + 1.0
w1 = weight[v1.index]
if w1 == None or w1 > dist:
weight[v1.index] = dist
neigh[v1] = 0
if len(neigh) == 0: return weight
else: return self.fill_neighbors(neigh.keys(), weight)
def execute(self, context):
ob = context.object
old_mode = ob.mode
if old_mode != 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')
me = ob.data
# store weight values
weight = [None]*len(me.vertices)
if self.mode != 'EUCL':
bm = bmesh.new()
bm.from_mesh(me)
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
bm.faces.ensure_lookup_table()
selected = [v for v in bm.verts if v.select]
if len(selected) == 0:
bpy.ops.object.mode_set(mode=old_mode)
message = "Please, select one or more vertices"
self.report({'ERROR'}, message)
return {'CANCELLED'}
for v in selected: weight[v.index] = 0
weight = self.fill_neighbors(selected, weight)
bm.free()
else:
selected = [v for v in me.vertices if v.select]
kd = KDTree(len(selected))
for i, v in enumerate(selected):
kd.insert(v.co, i)
kd.balance()
for i,v in enumerate(me.vertices):
co, index, dist = kd.find(v.co)
weight[i] = dist
for i in range(len(weight)):
if weight[i] == None: weight[i] = 0
weight = np.array(weight)
max_dist = np.max(weight)
if self.normalize:
if max_dist > 0:
weight /= max_dist
else:
delta_value = self.max_value - self.min_value
if delta_value == 0: delta_value = 0.0000001
weight = (weight-self.min_value)/delta_value
if self.mode == 'TOPO':
vg = ob.vertex_groups.new(name='Distance: {:d}'.format(int(max_dist)))
else:
vg = ob.vertex_groups.new(name='Distance: {:.4f}'.format(max_dist))
for i, w in enumerate(weight):
vg.add([i], w, 'REPLACE')
bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
return {'FINISHED'}
class TISSUE_PT_color(Panel):
bl_label = "Tissue Tools"
bl_category = "Tissue"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
#bl_options = {'DEFAULT_CLOSED'}
bl_context = "vertexpaint"
def draw(self, context):
layout = self.layout
col = layout.column(align=True)
col.operator("object.vertex_colors_to_vertex_groups",
icon="GROUP_VERTEX", text="Convert to Weight")
class TISSUE_PT_weight(Panel):
bl_label = "Tissue Tools"
bl_category = "Tissue"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
#bl_options = {'DEFAULT_CLOSED'}
bl_context = "weightpaint"
def draw(self, context):
layout = self.layout
col = layout.column(align=True)
#if context.object.type == 'MESH' and context.mode == 'OBJECT':
#col.label(text="Transform:")
#col.separator()
#elif bpy.context.mode == 'PAINT_WEIGHT':
col.label(text="Weight Generate:")
#col.operator(
# "object.vertex_colors_to_vertex_groups", icon="GROUP_VCOL")
col.operator("object.face_area_to_vertex_groups", icon="FACESEL")
col.operator("object.curvature_to_vertex_groups", icon="SMOOTHCURVE")
col.operator("object.tissue_weight_distance", icon="TRACKING")
row = col.row(align=True)
try: row.operator("object.weight_formula", icon="CON_TRANSFORM")
except: row.operator("object.weight_formula")#, icon="CON_TRANSFORM")
row.operator("object.update_weight_formula", icon="FILE_REFRESH", text='')#, icon="CON_TRANSFORM")
#col.label(text="Weight Processing:")
col.separator()
# TO BE FIXED
col.operator("object.weight_laplacian", icon="SMOOTHCURVE")
col.label(text="Weight Edit:")
col.operator("object.harmonic_weight", icon="IPO_ELASTIC")
col.operator("object.random_weight", icon="RNDCURVE")
col.separator()
col.label(text="Deformation Analysis:")
col.operator("object.edges_deformation", icon="DRIVER_DISTANCE")#FULLSCREEN_ENTER")
col.operator("object.edges_bending", icon="DRIVER_ROTATIONAL_DIFFERENCE")#"MOD_SIMPLEDEFORM")
col.separator()
col.label(text="Weight Curves:")
#col.operator("object.weight_contour_curves", icon="MOD_CURVE")
col.operator("object.tissue_weight_streamlines", icon="ANIM")
col.operator("object.tissue_weight_contour_curves_pattern", icon="FORCE_TURBULENCE")
col.separator()
col.operator("object.weight_contour_displace", icon="MOD_DISPLACE")
col.operator("object.weight_contour_mask", icon="MOD_MASK")
col.separator()
col.label(text="Simulations:")
#col.operator("object.reaction_diffusion", icon="MOD_OCEAN")
col.operator("object.start_reaction_diffusion",
icon="EXPERIMENTAL",
text="Reaction-Diffusion")
col.separator()
col.label(text="Materials:")
col.operator("object.random_materials", icon='COLOR')
col.operator("object.weight_to_materials", icon='GROUP_VERTEX')
col.separator()
col.label(text="Weight Convert:")
col.operator("object.vertex_group_to_vertex_colors", icon="GROUP_VCOL",
text="Convert to Colors")
col.operator("object.vertex_group_to_uv", icon="UV",
text="Convert to UV")
#col.prop(context.object, "reaction_diffusion_run", icon="PLAY", text="Run Simulation")
####col.prop(context.object, "reaction_diffusion_run")
#col.separator()
#col.label(text="Vertex Color from:")
#col.operator("object.vertex_group_to_vertex_colors", icon="GROUP_VERTEX")
class start_reaction_diffusion(Operator):
bl_idname = "object.start_reaction_diffusion"
bl_label = "Start Reaction Diffusion"
bl_description = ("Run a Reaction-Diffusion based on existing Vertex Groups: A and B")
bl_options = {'REGISTER', 'UNDO'}
run : BoolProperty(
name="Run Reaction-Diffusion", default=True, description="Compute a new iteration on frame changes")
time_steps : IntProperty(
name="Steps", default=10, min=0, soft_max=50,
description="Number of Steps")
dt : FloatProperty(
name="dt", default=1, min=0, soft_max=0.2,
description="Time Step")
diff_a : FloatProperty(
name="Diff A", default=0.18, min=0, soft_max=2,
description="Diffusion A")
diff_b : FloatProperty(
name="Diff B", default=0.09, min=0, soft_max=2,
description="Diffusion B")
f : FloatProperty(
name="f", default=0.055, min=0, soft_min=0.01, soft_max=0.06, max=0.1, precision=4,
description="Feed Rate")
k : FloatProperty(
name="k", default=0.062, min=0, soft_min=0.035, soft_max=0.065, max=0.1, precision=4,
description="Kill Rate")
@classmethod
def poll(cls, context):
return context.object.type == 'MESH' and context.mode != 'EDIT_MESH'
def execute(self, context):
reaction_diffusion_add_handler(self, context)
set_animatable_fix_handler(self, context)
ob = context.object
ob.reaction_diffusion_settings.run = self.run
ob.reaction_diffusion_settings.dt = self.dt
ob.reaction_diffusion_settings.time_steps = self.time_steps
ob.reaction_diffusion_settings.f = self.f
ob.reaction_diffusion_settings.k = self.k
ob.reaction_diffusion_settings.diff_a = self.diff_a
ob.reaction_diffusion_settings.diff_b = self.diff_b
# check vertex group A
try:
vg = ob.vertex_groups['A']
except:
ob.vertex_groups.new(name='A')
# check vertex group B
try:
vg = ob.vertex_groups['B']
except:
ob.vertex_groups.new(name='B')
for v in ob.data.vertices:
ob.vertex_groups['A'].add([v.index], 1, 'REPLACE')
ob.vertex_groups['B'].add([v.index], 0, 'REPLACE')
ob.vertex_groups.update()
ob.data.update()
bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
return {'FINISHED'}
class reset_reaction_diffusion_weight(Operator):
bl_idname = "object.reset_reaction_diffusion_weight"
bl_label = "Reset Reaction Diffusion Weight"
bl_description = ("Set A and B weight to default values")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return context.object.type == 'MESH' and context.mode != 'EDIT_MESH'
def execute(self, context):
reaction_diffusion_add_handler(self, context)
set_animatable_fix_handler(self, context)
ob = context.object
# check vertex group A
try:
vg = ob.vertex_groups['A']
except:
ob.vertex_groups.new(name='A')
# check vertex group B
try:
vg = ob.vertex_groups['B']
except:
ob.vertex_groups.new(name='B')
for v in ob.data.vertices:
ob.vertex_groups['A'].add([v.index], 1, 'REPLACE')
ob.vertex_groups['B'].add([v.index], 0, 'REPLACE')
ob.vertex_groups.update()
ob.data.update()
bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
return {'FINISHED'}
class bake_reaction_diffusion(Operator):
bl_idname = "object.bake_reaction_diffusion"
bl_label = "Bake Data"
bl_description = ("Bake the Reaction-Diffusion to the cache directory")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return context.object.type == 'MESH' and context.mode != 'EDIT_MESH'
def execute(self, context):
ob = context.object
props = ob.reaction_diffusion_settings
if props.fast_bake:
bool_run = props.run
props.run = False
context.scene.frame_current = props.cache_frame_start
fast_bake_def(ob, frame_start=props.cache_frame_start, frame_end=props.cache_frame_end)
#create_fast_bake_def(ob, frame_start=props.cache_frame_start, frame_end=props.cache_frame_end)
context.scene.frame_current = props.cache_frame_end
props.run = bool_run
else:
for i in range(props.cache_frame_start, props.cache_frame_end):
context.scene.frame_current = i
reaction_diffusion_def(ob, bake=True)
props.bool_cache = True
return {'FINISHED'}
class reaction_diffusion_free_data(Operator):
bl_idname = "object.reaction_diffusion_free_data"
bl_label = "Free Data"
bl_description = ("Free Reaction-Diffusion data")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return context.object.type == 'MESH'
def execute(self, context):
ob = context.object
props = ob.reaction_diffusion_settings
props.bool_cache = False
folder = Path(props.cache_dir)
for i in range(props.cache_frame_start, props.cache_frame_end):
data_a = folder / "a_{:04d}".format(i)
if os.path.exists(data_a):
os.remove(data_a)
data_a = folder / "b_{:04d}".format(i)
if os.path.exists(data_a):
os.remove(data_a)
return {'FINISHED'}
from bpy.app.handlers import persistent
def reaction_diffusion_scene(scene, bake=False):
for ob in scene.objects:
if ob.reaction_diffusion_settings.run:
reaction_diffusion_def(ob)
def reaction_diffusion_def(ob, bake=False):
scene = bpy.context.scene
start = time.time()
if type(ob) == bpy.types.Scene: return None
props = ob.reaction_diffusion_settings
if bake or props.bool_cache:
if props.cache_dir == '':
letters = string.ascii_letters
random_name = ''.join(rnd.choice(letters) for i in range(6))
if bpy.context.blend_data.filepath == '':
folder = Path(bpy.context.preferences.filepaths.temporary_directory)
folder = folder / 'reaction_diffusion_cache' / random_name
else:
folder = '//' + Path(bpy.context.blend_data.filepath).stem
folder = Path(bpy.path.abspath(folder)) / 'reaction_diffusion_cache' / random_name
folder.mkdir(parents=True, exist_ok=True)
props.cache_dir = str(folder)
else:
folder = Path(props.cache_dir)
me = ob.data
n_edges = len(me.edges)
n_verts = len(me.vertices)
a = np.zeros(n_verts)
b = np.zeros(n_verts)
print("{:6d} Reaction-Diffusion: {}".format(scene.frame_current, ob.name))
if not props.bool_cache:
if props.bool_mod:
# hide deforming modifiers
mod_visibility = []
for m in ob.modifiers:
mod_visibility.append(m.show_viewport)
if not mod_preserve_shape(m): m.show_viewport = False
# evaluated mesh
dg = bpy.context.evaluated_depsgraph_get()
ob_eval = ob.evaluated_get(dg)
me = bpy.data.meshes.new_from_object(ob_eval, preserve_all_data_layers=True, depsgraph=dg)
# set original visibility
for v, m in zip(mod_visibility, ob.modifiers):
m.show_viewport = v
ob.modifiers.update()
bm = bmesh.new() # create an empty BMesh
bm.from_mesh(me) # fill it in from a Mesh
dvert_lay = bm.verts.layers.deform.active
dt = props.dt
time_steps = props.time_steps
f = props.f
k = props.k
diff_a = props.diff_a
diff_b = props.diff_b
scale = props.diff_mult
brush_mult = props.brush_mult
# store weight values
if 'dB' in ob.vertex_groups: db = np.zeros(n_verts)
if 'grad' in ob.vertex_groups: grad = np.zeros(n_verts)
if props.vertex_group_diff_a != '': diff_a = np.zeros(n_verts)
if props.vertex_group_diff_b != '': diff_b = np.zeros(n_verts)
if props.vertex_group_scale != '': scale = np.zeros(n_verts)
if props.vertex_group_f != '': f = np.zeros(n_verts)
if props.vertex_group_k != '': k = np.zeros(n_verts)
if props.vertex_group_brush != '': brush = np.zeros(n_verts)
else: brush = 0
group_index_a = ob.vertex_groups["A"].index
group_index_b = ob.vertex_groups["B"].index
a = bmesh_get_weight_numpy(group_index_a, dvert_lay, bm.verts)
b = bmesh_get_weight_numpy(group_index_b, dvert_lay, bm.verts)
if props.vertex_group_diff_a != '':
group_index = ob.vertex_groups[props.vertex_group_diff_a].index
diff_a = bmesh_get_weight_numpy(group_index, dvert_lay, bm.verts)
if props.invert_vertex_group_diff_a:
vg_bounds = (props.min_diff_a, props.max_diff_a)
else:
vg_bounds = (props.max_diff_a, props.min_diff_a)
diff_a = np.interp(diff_a, (0,1), vg_bounds)
if props.vertex_group_diff_b != '':
group_index = ob.vertex_groups[props.vertex_group_diff_b].index
diff_b = bmesh_get_weight_numpy(group_index, dvert_lay, bm.verts)
if props.invert_vertex_group_diff_b:
vg_bounds = (props.max_diff_b, props.min_diff_b)
else:
vg_bounds = (props.min_diff_b, props.max_diff_b)
diff_b = np.interp(diff_b, (0,1), vg_bounds)
if props.vertex_group_scale != '':
group_index = ob.vertex_groups[props.vertex_group_scale].index
scale = bmesh_get_weight_numpy(group_index, dvert_lay, bm.verts)
if props.invert_vertex_group_scale:
vg_bounds = (props.max_scale, props.min_scale)
else:
vg_bounds = (props.min_scale, props.max_scale)
scale = np.interp(scale, (0,1), vg_bounds)
if props.vertex_group_f != '':
group_index = ob.vertex_groups[props.vertex_group_f].index
f = bmesh_get_weight_numpy(group_index, dvert_lay, bm.verts)
if props.invert_vertex_group_f:
vg_bounds = (props.max_f, props.min_f)
else:
vg_bounds = (props.min_f, props.max_f)
f = np.interp(f, (0,1), vg_bounds, )
if props.vertex_group_k != '':
group_index = ob.vertex_groups[props.vertex_group_k].index
k = bmesh_get_weight_numpy(group_index, dvert_lay, bm.verts)
if props.invert_vertex_group_k:
vg_bounds = (props.max_k, props.min_k)
else:
vg_bounds = (props.min_k, props.max_k)
k = np.interp(k, (0,1), vg_bounds)
if props.vertex_group_brush != '':
group_index = ob.vertex_groups[props.vertex_group_brush].index
brush = bmesh_get_weight_numpy(group_index, dvert_lay, bm.verts)
brush *= brush_mult
#timeElapsed = time.time() - start
#print('RD - Read Vertex Groups:',timeElapsed)
#start = time.time()
diff_a *= scale
diff_b *= scale
edge_verts = [0]*n_edges*2
me.edges.foreach_get("vertices", edge_verts)
edge_verts = np.array(edge_verts)
if 'gradient' in ob.vertex_groups.keys() and False:
group_index = ob.vertex_groups['gradient'].index
gradient = bmesh_get_weight_numpy(group_index, dvert_lay, bm.verts)
arr = (np.arange(n_edges)*2).astype(int)
id0 = edge_verts[arr]
id1 = edge_verts[arr+1]
#gradient = np.abs(gradient[id0] - gradient[id1])
gradient = gradient[id1] - gradient[id0]
gradient /= np.max(gradient)
sign = np.sign(gradient)
sign[sign==0] = 1
gradient = (0.05*abs(gradient) + 0.95)*sign
#gradient *= (1-abs(gradient)
#gradient = 0.2*(1-gradient) + 0.95
#gradient = get_uv_edge_vectors(me)
#uv_dir = Vector((0.5,0.5,0)).normalized()
#gradient = np.array([abs(g.dot(uv_dir.normalized())) for g in gradient])
#gradient = (gradient + 0.5)/2
#gradient = np.array([max(0,g.dot(uv_dir.normalized())) for g in gradient])
timeElapsed = time.time() - start
print(' Preparation Time:',timeElapsed)
start = time.time()
try:
_f = f if type(f) is np.ndarray else np.array((f,))
_k = k if type(k) is np.ndarray else np.array((k,))
_diff_a = diff_a if type(diff_a) is np.ndarray else np.array((diff_a,))
_diff_b = diff_b if type(diff_b) is np.ndarray else np.array((diff_b,))
_brush = brush if type(brush) is np.ndarray else np.array((brush,))
#a, b = numba_reaction_diffusion_anisotropic(n_verts, n_edges, edge_verts, a, b, _brush, _diff_a, _diff_b, _f, _k, dt, time_steps, gradient)
a, b = numba_reaction_diffusion(n_verts, n_edges, edge_verts, a, b, _brush, _diff_a, _diff_b, _f, _k, dt, time_steps)
except:
print('Not using Numba! The simulation could be slow.')
arr = np.arange(n_edges)*2
id0 = edge_verts[arr] # first vertex indices for each edge
id1 = edge_verts[arr+1] # second vertex indices for each edge
for i in range(time_steps):
b += brush
lap_a = np.zeros(n_verts)
lap_b = np.zeros(n_verts)
lap_a0 = a[id1] - a[id0] # laplacian increment for first vertex of each edge
lap_b0 = b[id1] - b[id0] # laplacian increment for first vertex of each edge
np.add.at(lap_a, id0, lap_a0)
np.add.at(lap_b, id0, lap_b0)
np.add.at(lap_a, id1, -lap_a0)
np.add.at(lap_b, id1, -lap_b0)
ab2 = a*b**2
a += eval("(diff_a*lap_a - ab2 + f*(1-a))*dt")
b += eval("(diff_b*lap_b + ab2 - (k+f)*b)*dt")
#a += (diff_a*lap_a - ab2 + f*(1-a))*dt
#b += (diff_b*lap_b + ab2 - (k+f)*b)*dt
a = nan_to_num(a)
b = nan_to_num(b)
timeElapsed = time.time() - start
print(' Simulation Time:',timeElapsed)
if bake:
if not(os.path.exists(folder)):
os.mkdir(folder)
file_name = folder / "a_{:04d}".format(scene.frame_current)
a.tofile(file_name)
file_name = folder / "b_{:04d}".format(scene.frame_current)
b.tofile(file_name)
elif props.bool_cache:
try:
file_name = folder / "a_{:04d}".format(scene.frame_current)
a = np.fromfile(file_name)
file_name = folder / "b_{:04d}".format(scene.frame_current)
b = np.fromfile(file_name)
except:
print(' Cannot read cache.')
return
if props.update_weight_a or props.update_weight_b:
start = time.time()
if props.update_weight_a:
if 'A' in ob.vertex_groups.keys():
vg_a = ob.vertex_groups['A']
else:
vg_a = ob.vertex_groups.new(name='A')
else:
vg_a = None
if props.update_weight_b:
if 'B' in ob.vertex_groups.keys():
vg_b = ob.vertex_groups['B']
else:
vg_b = ob.vertex_groups.new(name='B')
else:
vg_b = None
if vg_a == vg_b == None:
pass
else:
if ob.mode == 'WEIGHT_PAINT':# or props.bool_cache:
# slower, but prevent crashes
for i in range(n_verts):
if vg_a: vg_a.add([i], a[i], 'REPLACE')
if vg_b: vg_b.add([i], b[i], 'REPLACE')
else:
if props.bool_mod or props.bool_cache:
#bm.free() # release old bmesh
bm = bmesh.new() # create an empty BMesh
bm.from_mesh(ob.data) # fill it in from a Mesh
dvert_lay = bm.verts.layers.deform.active
# faster, but can cause crashes while painting weight
if vg_a: index_a = vg_a.index
if vg_b: index_b = vg_b.index
for i, v in enumerate(bm.verts):
dvert = v[dvert_lay]
if vg_a: dvert[index_a] = a[i]
if vg_b: dvert[index_b] = b[i]
bm.to_mesh(ob.data)
bm.free()
print(' Writing Vertex Groups Time:',time.time() - start)
if props.normalize:
min_a = np.min(a)
max_a = np.max(a)
min_b = np.min(b)
max_b = np.max(b)
a = (a - min_a)/(max_a - min_a)
b = (b - min_b)/(max_b - min_b)
split_a = None
split_b = None
splitted = False
if props.update_colors:#_a or props.update_colors_b:
start = time.time()
loops_size = get_attribute_numpy(me.polygons, attribute='loop_total', mult=1)
n_colors = np.sum(loops_size)
v_id = np.ones(n_colors)
me.polygons.foreach_get('vertices',v_id)
v_id = v_id.astype(int)
#v_id = np.array([v for p in ob.data.polygons for v in p.vertices])
'''
if props.update_colors_b:
if 'B' in ob.data.vertex_colors.keys():
vc = ob.data.vertex_colors['B']
else:
vc = ob.data.vertex_colors.new(name='B')
c_val = b[v_id]
c_val = np.repeat(c_val, 4, axis=0)
vc.data.foreach_set('color',c_val)
if props.update_colors_a:
if 'A' in ob.data.vertex_colors.keys():
vc = ob.data.vertex_colors['A']
else:
vc = ob.data.vertex_colors.new(name='A')
c_val = a[v_id]
c_val = np.repeat(c_val, 4, axis=0)
vc.data.foreach_set('color',c_val)
'''
split_a = a[v_id,None]
split_b = b[v_id,None]
splitted = True
ones = np.ones((n_colors,1))
#rgba = np.concatenate((split_a,split_b,-split_b+split_a,ones),axis=1).flatten()
rgba = np.concatenate((split_a,split_b,ones,ones),axis=1).flatten()
if 'AB' in ob.data.vertex_colors.keys():
vc = ob.data.vertex_colors['AB']
else:
vc = ob.data.vertex_colors.new(name='AB')
vc.data.foreach_set('color',rgba)
ob.data.vertex_colors.update()
print(' Writing Vertex Colors Time:',time.time() - start)
if props.update_uv:
start = time.time()
if 'AB' in me.uv_layers.keys():
uv_layer = me.uv_layers['AB']
else:
uv_layer = me.uv_layers.new(name='AB')
if not splitted:
loops_size = get_attribute_numpy(me.polygons, attribute='loop_total', mult=1)
n_data = np.sum(loops_size)
v_id = np.ones(n_data)
me.polygons.foreach_get('vertices',v_id)
v_id = v_id.astype(int)
split_a = a[v_id,None]
split_b = b[v_id,None]
uv = np.concatenate((split_a,split_b),axis=1).flatten()
uv_layer.data.foreach_set('uv',uv)
me.uv_layers.update()
print(' Writing UV Map Time:',time.time() - start)
for ps in ob.particle_systems:
if ps.vertex_group_density == 'B' or ps.vertex_group_density == 'A':
ps.invert_vertex_group_density = not ps.invert_vertex_group_density
ps.invert_vertex_group_density = not ps.invert_vertex_group_density
if props.bool_mod and not props.bool_cache: bpy.data.meshes.remove(me)
def fast_bake_def(ob, frame_start=1, frame_end=250):
scene = bpy.context.scene
start = time.time()
if type(ob) == bpy.types.Scene: return None
props = ob.reaction_diffusion_settings
# Define cache folder
if props.cache_dir == '':
letters = string.ascii_letters
random_name = ''.join(rnd.choice(letters) for i in range(6))
if bpy.context.blend_data.filepath == '':
folder = Path(bpy.context.preferences.filepaths.temporary_directory)
folder = folder / 'reaction_diffusion_cache' / random_name
else:
folder = '//' + Path(bpy.context.blend_data.filepath).stem
folder = Path(bpy.path.abspath(folder)) / 'reaction_diffusion_cache' / random_name
folder.mkdir(parents=True, exist_ok=True)
props.cache_dir = str(folder)
else:
folder = Path(props.cache_dir)
if props.bool_mod:
# hide deforming modifiers
mod_visibility = []
for m in ob.modifiers:
mod_visibility.append(m.show_viewport)
if not mod_preserve_shape(m): m.show_viewport = False
# evaluated mesh
dg = bpy.context.evaluated_depsgraph_get()
ob_eval = ob.evaluated_get(dg)
me = bpy.data.meshes.new_from_object(ob_eval, preserve_all_data_layers=True, depsgraph=dg)
# set original visibility
for v, m in zip(mod_visibility, ob.modifiers):
m.show_viewport = v
ob.modifiers.update()
else:
me = ob.data
bm = bmesh.new() # create an empty BMesh
bm.from_mesh(me) # fill it in from a Mesh
dvert_lay = bm.verts.layers.deform.active
n_edges = len(me.edges)
n_verts = len(me.vertices)
a = np.zeros(n_verts)
b = np.zeros(n_verts)
group_index_a = ob.vertex_groups["A"].index
group_index_b = ob.vertex_groups["B"].index
dt = props.dt
time_steps = props.time_steps
f = props.f
k = props.k
diff_a = props.diff_a
diff_b = props.diff_b
scale = props.diff_mult
brush_mult = props.brush_mult
# store weight values
if 'dB' in ob.vertex_groups: db = np.zeros(n_verts)
if 'grad' in ob.vertex_groups: grad = np.zeros(n_verts)
if props.vertex_group_diff_a != '': diff_a = np.zeros(n_verts)
if props.vertex_group_diff_b != '': diff_b = np.zeros(n_verts)
if props.vertex_group_scale != '': scale = np.zeros(n_verts)
if props.vertex_group_f != '': f = np.zeros(n_verts)
if props.vertex_group_k != '': k = np.zeros(n_verts)
if props.vertex_group_brush != '': brush = np.zeros(n_verts)
else: brush = 0
a = bmesh_get_weight_numpy(group_index_a, dvert_lay, bm.verts)
b = bmesh_get_weight_numpy(group_index_b, dvert_lay, bm.verts)
if props.vertex_group_diff_a != '':
group_index = ob.vertex_groups[props.vertex_group_diff_a].index
diff_a = bmesh_get_weight_numpy(group_index, dvert_lay, bm.verts)
if props.invert_vertex_group_diff_a:
vg_bounds = (props.min_diff_a, props.max_diff_a)
else:
vg_bounds = (props.max_diff_a, props.min_diff_a)
diff_a = np.interp(diff_a, (0,1), vg_bounds)
if props.vertex_group_diff_b != '':
group_index = ob.vertex_groups[props.vertex_group_diff_b].index
diff_b = bmesh_get_weight_numpy(group_index, dvert_lay, bm.verts)
if props.invert_vertex_group_diff_b:
vg_bounds = (props.max_diff_b, props.min_diff_b)
else:
vg_bounds = (props.min_diff_b, props.max_diff_b)
diff_b = np.interp(diff_b, (0,1), vg_bounds)
if props.vertex_group_scale != '':
group_index = ob.vertex_groups[props.vertex_group_scale].index
scale = bmesh_get_weight_numpy(group_index, dvert_lay, bm.verts)
if props.invert_vertex_group_scale:
vg_bounds = (props.max_scale, props.min_scale)
else:
vg_bounds = (props.min_scale, props.max_scale)
scale = np.interp(scale, (0,1), vg_bounds)
if props.vertex_group_f != '':
group_index = ob.vertex_groups[props.vertex_group_f].index
f = bmesh_get_weight_numpy(group_index, dvert_lay, bm.verts)
if props.invert_vertex_group_f:
vg_bounds = (props.max_f, props.min_f)
else:
vg_bounds = (props.min_f, props.max_f)
f = np.interp(f, (0,1), vg_bounds, )
if props.vertex_group_k != '':
group_index = ob.vertex_groups[props.vertex_group_k].index
k = bmesh_get_weight_numpy(group_index, dvert_lay, bm.verts)
if props.invert_vertex_group_k:
vg_bounds = (props.max_k, props.min_k)
else:
vg_bounds = (props.min_k, props.max_k)
k = np.interp(k, (0,1), vg_bounds)
if props.vertex_group_brush != '':
group_index = ob.vertex_groups[props.vertex_group_brush].index
brush = bmesh_get_weight_numpy(group_index, dvert_lay, bm.verts)
brush *= brush_mult
diff_a *= scale
diff_b *= scale
edge_verts = [0]*n_edges*2
me.edges.foreach_get("vertices", edge_verts)
gradient = get_uv_edge_vectors(me)
uv_dir = Vector((0.5,0.5,0))
#gradient = [abs(g.dot(uv_dir)) for g in gradient]
gradient = [max(0,g.dot(uv_dir)) for g in gradient]
timeElapsed = time.time() - start
print(' Preparation Time:',timeElapsed)
start = time.time()
try:
edge_verts = np.array(edge_verts)
_f = f if type(f) is np.ndarray else np.array((f,))
_k = k if type(k) is np.ndarray else np.array((k,))
_diff_a = diff_a if type(diff_a) is np.ndarray else np.array((diff_a,))
_diff_b = diff_b if type(diff_b) is np.ndarray else np.array((diff_b,))
_brush = brush if type(brush) is np.ndarray else np.array((brush,))
run_rd = False
for j in range(props.cache_frame_start, props.cache_frame_end+1):
start2 = time.time()
print("{:6d} Reaction-Diffusion: {}".format(j, ob.name))
if run_rd:
b += _brush
a, b = numba_reaction_diffusion(n_verts, n_edges, edge_verts, a, b, _brush, _diff_a, _diff_b, _f, _k, dt, time_steps)
else:
run_rd = True
if not(os.path.exists(folder)):
os.mkdir(folder)
file_name = folder / "a_{:04d}".format(j)
a.tofile(file_name)
file_name = folder / "b_{:04d}".format(j)
b.tofile(file_name)
timeElapsed = time.time() - start2
print(' Simulation Time:',timeElapsed)
except:
print('Not using Numba! The simulation could be slow.')
edge_verts = np.array(edge_verts)
arr = np.arange(n_edges)*2
id0 = edge_verts[arr] # first vertex indices for each edge
id1 = edge_verts[arr+1] # second vertex indices for each edge
for j in range(props.cache_frame_start, props.cache_frame_end):
for i in range(time_steps):
b += brush
lap_a = np.zeros(n_verts)
lap_b = np.zeros(n_verts)
lap_a0 = a[id1] - a[id0] # laplacian increment for first vertex of each edge
lap_b0 = b[id1] - b[id0] # laplacian increment for first vertex of each edge
np.add.at(lap_a, id0, lap_a0)
np.add.at(lap_b, id0, lap_b0)
np.add.at(lap_a, id1, -lap_a0)
np.add.at(lap_b, id1, -lap_b0)
ab2 = a*b**2
a += eval("(diff_a*lap_a - ab2 + f*(1-a))*dt")
b += eval("(diff_b*lap_b + ab2 - (k+f)*b)*dt")
a = nan_to_num(a)
b = nan_to_num(b)
if not(os.path.exists(folder)):
os.mkdir(folder)
file_name = folder / "a_{:04d}".format(j)
a.tofile(file_name)
file_name = folder / "b_{:04d}".format(j)
b.tofile(file_name)
if ob.mode == 'WEIGHT_PAINT':
# slower, but prevent crashes
vg_a = ob.vertex_groups['A']
vg_b = ob.vertex_groups['B']
for i in range(n_verts):
vg_a.add([i], a[i], 'REPLACE')
vg_b.add([i], b[i], 'REPLACE')
else:
if props.bool_mod:
bm.free() # release old bmesh
bm = bmesh.new() # create an empty BMesh
bm.from_mesh(ob.data) # fill it in from a Mesh
dvert_lay = bm.verts.layers.deform.active
# faster, but can cause crashes while painting weight
for i, v in enumerate(bm.verts):
dvert = v[dvert_lay]
dvert[group_index_a] = a[i]
dvert[group_index_b] = b[i]
bm.to_mesh(ob.data)
# Update Vertex Colors
if 'A' in ob.data.vertex_colors or 'B' in ob.data.vertex_colors:
v_id = np.array([v for p in ob.data.polygons for v in p.vertices])
if 'B' in ob.data.vertex_colors:
c_val = b[v_id]
c_val = np.repeat(c_val, 4, axis=0)
vc = ob.data.vertex_colors['B']
vc.data.foreach_set('color',c_val.tolist())
if 'A' in ob.data.vertex_colors:
c_val = a[v_id]
c_val = np.repeat(c_val, 4, axis=0)
vc = ob.data.vertex_colors['A']
vc.data.foreach_set('color',c_val.tolist())
for ps in ob.particle_systems:
if ps.vertex_group_density == 'B' or ps.vertex_group_density == 'A':
ps.invert_vertex_group_density = not ps.invert_vertex_group_density
ps.invert_vertex_group_density = not ps.invert_vertex_group_density
if props.bool_mod: bpy.data.meshes.remove(me)
bm.free()
timeElapsed = time.time() - start
print(' Closing Time:',timeElapsed)
def create_fast_bake_def(ob, frame_start=1, frame_end=250):
scene = bpy.context.scene
start = time.time()
if type(ob) == bpy.types.Scene: return None
props = ob.reaction_diffusion_settings
dt = props.dt
time_steps = props.time_steps
scale = props.diff_mult
if props.cache_dir == '':
letters = string.ascii_letters
random_name = ''.join(rnd.choice(letters) for i in range(6))
if bpy.context.blend_data.filepath == '':
folder = Path(bpy.context.preferences.filepaths.temporary_directory)
folder = folder / 'reaction_diffusion_cache' / random_name
else:
folder = '//' + Path(bpy.context.blend_data.filepath).stem
folder = Path(bpy.path.abspath(folder)) / 'reaction_diffusion_cache' / random_name
folder.mkdir(parents=True, exist_ok=True)
props.cache_dir = str(folder)
else:
folder = Path(props.cache_dir)
if props.bool_mod:
# hide deforming modifiers
mod_visibility = []
for m in ob.modifiers:
mod_visibility.append(m.show_viewport)
if not mod_preserve_shape(m): m.show_viewport = False
# evaluated mesh
dg = bpy.context.evaluated_depsgraph_get()
ob_eval = ob.evaluated_get(dg)
me = bpy.data.meshes.new_from_object(ob_eval, preserve_all_data_layers=True, depsgraph=dg)
# set original visibility
for v, m in zip(mod_visibility, ob.modifiers):
m.show_viewport = v
ob.modifiers.update()
else:
me = ob.data
bm = bmesh.new() # create an empty BMesh
bm.from_mesh(me) # fill it in from a Mesh
verts = get_vertices_numpy(me)
dvert_lay = bm.verts.layers.deform.active
n_edges = len(me.edges)
n_verts = len(me.vertices)
group_index_x = ob.vertex_groups["x"].index
group_index_y = ob.vertex_groups["y"].index
group_index_module = ob.vertex_groups["module"].index
group_index_values = ob.vertex_groups["values"].index
if not props.bool_cache:
time_steps = props.time_steps
# store weight values
if 'dB' in ob.vertex_groups: db = np.zeros(n_verts)
if 'grad' in ob.vertex_groups: grad = np.zeros(n_verts)
vec_x = np.zeros(n_verts)
vec_y = np.zeros(n_verts)
vec_module = np.zeros(n_verts)
values = np.zeros(n_verts)
vec_x = bmesh_get_weight_numpy(group_index_x, dvert_lay, bm.verts)
vec_y = bmesh_get_weight_numpy(group_index_y, dvert_lay, bm.verts)
vec_module = bmesh_get_weight_numpy(group_index_module, dvert_lay, bm.verts)
values = bmesh_get_weight_numpy(group_index_values, dvert_lay, bm.verts)
field = np.concatenate((vec_x[:,None],vec_y[:,None],vec_y[:,None]*0),axis=1)
field = field*2-1
field[:,2] = 0
edge_verts = get_edges_numpy(me)
id0 = edge_verts[:,0]
id1 = edge_verts[:,1]
vert0 = verts[id0]
vert1 = verts[id1]
vec = vert1-vert0
edge_field = (field[id0] + field[id1])/2 # average vector associated to the edge
print(vert0.shape)
print(field.shape)
print(edge_field.shape)
# normalize vectors
vec /= np.linalg.norm(vec,axis=1)[:,None]
edge_field /= np.linalg.norm(edge_field,axis=1)[:,None]
edge_flow = np.einsum('...j,...j', vec, edge_field)
#sign = (edge_flow>0).astype(int)
#values[edge_verts[sign]] += values[edge_verts[1-sign]]*
#values[verts0] += values[verts1]*edge_flow
timeElapsed = time.time() - start
print(' Preparation Time:',timeElapsed)
start = time.time()
# Preserve energy
mult = np.zeros(values.shape)
#mult[id0] -= edge_flow
#mult[id1] += edge_flow
np.add.at(mult,id0,-edge_flow)
np.add.at(mult,id1,edge_flow)
print("mult")
mult = scale/mult
print(mult)
print(np.sum(mult))
#try:
print(vec)
print(edge_flow)
print(edge_flow)
bool_run = False
for j in range(props.cache_frame_start, props.cache_frame_end+1):
start2 = time.time()
print("{:6d} Reaction-Diffusion: {}".format(j, ob.name))
if bool_run:
print(values)
#for i in range(1):
values = integrate_field(n_edges,id0,id1,values,edge_flow,mult,time_steps)
#values0 = values
#np.add.at(values, id0, values0[id1]*edge_flow*mult[id1])
#np.add.at(values, id1, -values0[id0]*edge_flow*mult[id0])
#np.add.at(values, id0, values0[id1]*edge_flow*mult)
#np.add.at(values, id1, -values0[id0]*edge_flow*mult)
#values[id1] += values0[id0]*edge_flow/mult[id1]*dt
#values[id0] -= values0[id1]*edge_flow/mult[id0]*dt
#values[id1] = edge_flow
#values[id1] += edge_flow
#a, b = numba_reaction_diffusion(n_verts, n_edges, edge_verts, a, b, _brush, _diff_a, _diff_b, _f, _k, dt, time_steps)
'''
lap_a = np.zeros(n_verts)
lap_b = np.zeros(n_verts)
lap_a0 = a[id1] - a[id0] # laplacian increment for first vertex of each edge
lap_b0 = b[id1] - b[id0] # laplacian increment for first vertex of each edge
np.add.at(lap_a, id0, lap_a0)
np.add.at(lap_b, id0, lap_b0)
np.add.at(lap_a, id1, -lap_a0)
np.add.at(lap_b, id1, -lap_b0)
'''
else:
bool_run = True
if not(os.path.exists(folder)):
os.mkdir(folder)
file_name = folder / "a_{:04d}".format(j)
values.tofile(file_name)
file_name = folder / "b_{:04d}".format(j)
values.tofile(file_name)
timeElapsed = time.time() - start2
print(' Simulation Time:',timeElapsed)
if props.bool_mod: bpy.data.meshes.remove(me)
bm.free()
timeElapsed = time.time() - start
print(' Closing Time:',timeElapsed)
class TISSUE_PT_reaction_diffusion(Panel):
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "data"
bl_label = "Tissue Reaction-Diffusion"
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
return 'A' and 'B' in context.object.vertex_groups
def draw(self, context):
reaction_diffusion_add_handler(self, context)
ob = context.object
props = ob.reaction_diffusion_settings
layout = self.layout
col = layout.column(align=True)
row = col.row(align=True)
if not ("A" and "B" in ob.vertex_groups):
row.operator("object.start_reaction_diffusion",
icon="EXPERIMENTAL",
text="Reaction-Diffusion")
else:
row.operator("object.start_reaction_diffusion",
icon="EXPERIMENTAL",
text="Reset Reaction-Diffusion")
row = col.row(align=True)
row.prop(props, "run", text="Run Reaction-Diffusion")
col = layout.column(align=True)
row = col.row(align=True)
row.prop(props, "time_steps")
row.prop(props, "dt")
row.enabled = not props.bool_cache
col.separator()
row = col.row(align=True)
col1 = row.column(align=True)
col1.prop(props, "diff_a")
col1.enabled = props.vertex_group_diff_a == '' and not props.bool_cache
col1 = row.column(align=True)
col1.prop(props, "diff_b")
col1.enabled = props.vertex_group_diff_b == '' and not props.bool_cache
row = col.row(align=True)
row.prop(props, "diff_mult")
row.enabled = props.vertex_group_scale == '' and not props.bool_cache
#col.separator()
row = col.row(align=True)
col1 = row.column(align=True)
col1.prop(props, "f")
col1.enabled = props.vertex_group_f == '' and not props.bool_cache
col1 = row.column(align=True)
col1.prop(props, "k")
col1.enabled = props.vertex_group_k == '' and not props.bool_cache
col.separator()
col.label(text='Cache:')
#col.prop(props, "bool_cache")
col.prop(props, "cache_dir", text='')
col.separator()
row = col.row(align=True)
row.prop(props, "cache_frame_start")
row.prop(props, "cache_frame_end")
col.separator()
if props.bool_cache:
col.operator("object.reaction_diffusion_free_data")
else:
row = col.row(align=True)
row.operator("object.bake_reaction_diffusion")
file = bpy.context.blend_data.filepath
temp = bpy.context.preferences.filepaths.temporary_directory
if file == temp == props.cache_dir == '':
row.enabled = False
col.label(text="Cannot use cache", icon='ERROR')
col.label(text='please save the Blender or set a Cache directory')
col.prop(props, "fast_bake")
col.separator()
col.label(text='Output attributes:')
row = col.row(align=True)
col2 = row.column(align=True)
row2 = col2.row(align=True)
row2.prop(props, "update_weight_a", icon='GROUP_VERTEX', text='A')
row2.prop(props, "update_weight_b", icon='GROUP_VERTEX', text='B')
col2.enabled = props.bool_cache
row.separator()
#row.prop(props, "update_colors_a", icon='GROUP_VCOL', text='A')
#row.prop(props, "update_colors_b", icon='GROUP_VCOL', text='B')
row.prop(props, "update_colors", icon='GROUP_VCOL', text='AB')
row.separator()
row.prop(props, "update_uv", icon='GROUP_UVS', text='AB')
col.prop(props,'normalize')
#col.prop_search(props, 'vertex_group_diff_a', ob, "vertex_groups", text='Diff A')
#col.prop_search(props, 'vertex_group_diff_b', ob, "vertex_groups", text='Diff B')
#col.prop_search(props, 'vertex_group_scale', ob, "vertex_groups", text='Scale')
#col.prop_search(props, 'vertex_group_f', ob, "vertex_groups", text='f')
#col.prop_search(props, 'vertex_group_k', ob, "vertex_groups", text='k')
class TISSUE_PT_reaction_diffusion_weight(Panel):
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "data"
bl_parent_id = "TISSUE_PT_reaction_diffusion"
bl_label = "Vertex Groups"
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
return 'A' and 'B' in context.object.vertex_groups
def draw(self, context):
ob = context.object
props = ob.reaction_diffusion_settings
layout = self.layout
#layout.use_property_split = True
col = layout.column(align=True)
col.prop(props, "bool_mod")
if props.bool_mod and props.fast_bake:
col.label(text="When Fast Bake is on, the modifiers", icon='ERROR')
col.label(text=" are used only for the first frame")
col.separator()
insert_weight_parameter(col, ob, 'brush', text='Brush:')
insert_weight_parameter(col, ob, 'diff_a', text='Diff A:')
insert_weight_parameter(col, ob, 'diff_b', text='Diff B:')
insert_weight_parameter(col, ob, 'scale', text='Scale:')
insert_weight_parameter(col, ob, 'f', text='f:')
insert_weight_parameter(col, ob, 'k', text='k:')
col.enabled = not props.bool_cache
def insert_weight_parameter(col, ob, name, text=''):
props = ob.reaction_diffusion_settings
split = col.split(factor=0.25, align=True)
col2 = split.column(align=True)
col2.label(text=text)
col2 = split.column(align=True)
row2 = col2.row(align=True)
row2.prop_search(props, 'vertex_group_' + name, ob, "vertex_groups", text='')
if name != 'brush':
row2.prop(props, "invert_vertex_group_" + name, text="", toggle=True, icon='ARROW_LEFTRIGHT')
if 'vertex_group_' + name in props:
if props['vertex_group_' + name] != '':
if name == 'brush':
col2.prop(props, "brush_mult")
else:
row2 = col2.row(align=True)
row2.prop(props, "min_" + name, text="Min")
row2 = col2.row(align=True)
row2.prop(props, "max_" + name, text="Max")
col.separator()
def contour_edges_pattern(operator, c, verts_count, iso_val, vertices, normals, filtered_edges, weight, pattern_weight, bevel_weight):
# vertices indexes
id0 = filtered_edges[:,0]
id1 = filtered_edges[:,1]
# vertices weight
w0 = weight[id0]
w1 = weight[id1]
# weight condition
bool_w0 = w0 < iso_val
bool_w1 = w1 < iso_val
# mask all edges that have one weight value below the iso value
mask_new_verts = np.logical_xor(bool_w0, bool_w1)
if not mask_new_verts.any():
return np.array([[None]]), {}, np.array([[None]]), np.array([[None]])
id0 = id0[mask_new_verts]
id1 = id1[mask_new_verts]
# filter arrays
v0 = vertices[id0]
v1 = vertices[id1]
n0 = normals[id0]
n1 = normals[id1]
w0 = w0[mask_new_verts]
w1 = w1[mask_new_verts]
pattern0 = pattern_weight[id0]
pattern1 = pattern_weight[id1]
try:
bevel0 = bevel_weight[id0]
bevel1 = bevel_weight[id1]
except: pass
### Spiral
#edge_nor = (n0+n1)/2
#shift = np.arctan2(edge_nor[:,0], edge_nor[:,1])/2/pi*delta_iso
#param = (iso_val + shift - w0)/(w1-w0)
param = (iso_val - w0)/(w1-w0)
# pattern displace
#mult = 1 if c%2 == 0 else -1
if c%(operator.in_steps + operator.out_steps) < operator.in_steps:
mult = operator.in_displace
else:
mult = operator.out_displace
pattern_value = pattern0 + (pattern1-pattern0)*param
try:
bevel_value = bevel0 + (bevel1-bevel0)*param
bevel_value = np.expand_dims(bevel_value,axis=1)
except: bevel_value = None
disp = pattern_value * mult
param = np.expand_dims(param,axis=1)
disp = np.expand_dims(disp,axis=1)
verts = v0 + (v1-v0)*param
norm = n0 + (n1-n0)*param
if operator.limit_z: disp *= 1-abs(np.expand_dims(norm[:,2], axis=1))
verts = verts + norm*disp
#verts = verts[np.flip(np.argsort(shift))]
#verts = verts[np.argsort(shift)]
# indexes of edges with new vertices
edges_index = filtered_edges[mask_new_verts][:,2]
# remove all edges completely below the iso value
#mask_edges = np.logical_not(np.logical_and(bool_w0, bool_w1))
#filtered_edges = filtered_edges[mask_edges]
return filtered_edges, edges_index, verts, bevel_value
def contour_bmesh(me, bm, weight, iso_val):
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
bm.faces.ensure_lookup_table()
# store weight values
vertices = get_vertices_numpy(me)
faces_mask = np.array(bm.faces)
filtered_edges = get_edges_id_numpy(me)
n_verts = len(bm.verts)
#############################
# vertices indexes
id0 = filtered_edges[:,0]
id1 = filtered_edges[:,1]
# vertices weight
w0 = weight[id0]
w1 = weight[id1]
# weight condition
bool_w0 = w0 < iso_val
bool_w1 = w1 < iso_val
# mask all edges that have one weight value below the iso value
mask_new_verts = np.logical_xor(bool_w0, bool_w1)
if not mask_new_verts.any(): return np.array([[None]]), {}, np.array([[None]])
id0 = id0[mask_new_verts]
id1 = id1[mask_new_verts]
# filter arrays
v0 = vertices[id0]
v1 = vertices[id1]
w0 = w0[mask_new_verts]
w1 = w1[mask_new_verts]
param = (iso_val-w0)/(w1-w0)
param = np.expand_dims(param,axis=1)
verts = v0 + (v1-v0)*param
# indexes of edges with new vertices
#edges_index = filtered_edges[mask_new_verts][:,2]
edges_id = {}
for i, e in enumerate(filtered_edges):
#edges_id[id] = i + n_verts
edges_id['{}_{}'.format(e[0],e[1])] = i + n_verts
edges_id['{}_{}'.format(e[1],e[0])] = i + n_verts
'''
for e in filtered_edges:
id0 = e.verts[0].index
id1 = e.verts[1].index
w0 = weight[id0]
w1 = weight[id1]
if w0 == w1: continue
elif w0 > iso_val and w1 > iso_val:
continue
elif w0 < iso_val and w1 < iso_val: continue
elif w0 == iso_val or w1 == iso_val: continue
else:
v0 = me0.vertices[id0].co
v1 = me0.vertices[id1].co
v = v0.lerp(v1, (iso_val-w0)/(w1-w0))
delete_edges.append(e)
verts.append(v)
edges_id[str(id0)+"_"+str(id1)] = count
edges_id[str(id1)+"_"+str(id0)] = count
count += 1
'''
splitted_faces = []
switch = False
# splitting faces
for f in faces_mask:
# create sub-faces slots. Once a new vertex is reached it will
# change slot, storing the next vertices for a new face.
build_faces = [[],[]]
#switch = False
verts0 = list(me.polygons[f.index].vertices)
verts1 = list(verts0)
verts1.append(verts1.pop(0)) # shift list
for id0, id1 in zip(verts0, verts1):
# add first vertex to active slot
build_faces[switch].append(id0)
# try to split edge
try:
# check if the edge must be splitted
new_vert = edges_id['{}_{}'.format(id0,id1)]
# add new vertex
build_faces[switch].append(new_vert)
# if there is an open face on the other slot
if len(build_faces[not switch]) > 0:
# store actual face
splitted_faces.append(build_faces[switch])
# reset actual faces and switch
build_faces[switch] = []
# change face slot
switch = not switch
# continue previous face
build_faces[switch].append(new_vert)
except: pass
if len(build_faces[not switch]) == 2:
build_faces[not switch].append(id0)
if len(build_faces[not switch]) > 2:
splitted_faces.append(build_faces[not switch])
# add last face
splitted_faces.append(build_faces[switch])
# adding new vertices use fast local method access
_new_vert = bm.verts.new
for v in verts: _new_vert(v)
bm.verts.ensure_lookup_table()
# deleting old edges/faces
bm.edges.ensure_lookup_table()
remove_edges = [bm.edges[i] for i in filtered_edges[:,2]]
#for e in remove_edges: bm.edges.remove(e)
#for e in delete_edges: bm.edges.remove(e)
bm.verts.ensure_lookup_table()
# adding new faces use fast local method access
_new_face = bm.faces.new
missed_faces = []
for f in splitted_faces:
try:
face_verts = [bm.verts[i] for i in f]
_new_face(face_verts)
except:
missed_faces.append(f)
#me = bpy.data.meshes.new('_tissue_tmp_')
bm.to_mesh(me)
weight = np.concatenate((weight, np.ones(len(verts))*iso_val))
return me, bm, weight
class tissue_weight_streamlines(Operator):
bl_idname = "object.tissue_weight_streamlines"
bl_label = "Streamlines Curves"
bl_description = ("")
bl_options = {'REGISTER', 'UNDO'}
mode : EnumProperty(
items=(
('VERTS', "Verts", "Follow vertices"),
('EDGES', "Edges", "Follow Edges")
),
default='VERTS',
name="Streamlines path mode"
)
interpolation : EnumProperty(
items=(
('POLY', "Poly", "Generate Polylines"),
('NURBS', "NURBS", "Generate Nurbs curves")
),
default='POLY',
name="Interpolation mode"
)
use_modifiers : BoolProperty(
name="Use Modifiers", default=True,
description="Apply all the modifiers")
use_selected : BoolProperty(
name="Use Selected Vertices", default=False,
description="Use selected vertices as Seed")
same_weight : BoolProperty(
name="Same Weight", default=True,
description="Continue the streamlines when the weight is the same")
min_iso : FloatProperty(
name="Min Value", default=0., soft_min=0, soft_max=1,
description="Minimum weight value")
max_iso : FloatProperty(
name="Max Value", default=1, soft_min=0, soft_max=1,
description="Maximum weight value")
rand_seed : IntProperty(
name="Seed", default=0, min=0, soft_max=10,
description="Random Seed")
n_curves : IntProperty(
name="Curves", default=50, soft_min=1, soft_max=100000,
description="Number of Curves")
min_rad = 1
max_rad = 1
pos_steps : IntProperty(
name="High Steps", default=50, min=0, soft_max=100,
description="Number of steps in the direction of high weight")
neg_steps : IntProperty(
name="Low Steps", default=50, min=0, soft_max=100,
description="Number of steps in the direction of low weight")
bevel_depth : FloatProperty(
name="Bevel Depth", default=0, min=0, soft_max=1,
description="")
min_bevel_depth : FloatProperty(
name="Min Bevel Depth", default=0.1, min=0, soft_max=1,
description="")
max_bevel_depth : FloatProperty(
name="Max Bevel Depth", default=1, min=0, soft_max=1,
description="")
rand_dir : FloatProperty(
name="Randomize", default=0, min=0, max=1,
description="Randomize streamlines directions (Slower)")
vertex_group_seeds : StringProperty(
name="Displace", default='',
description="Vertex Group used for pattern displace")
vertex_group_bevel : StringProperty(
name="Bevel", default='',
description="Variable Bevel depth")
object_name : StringProperty(
name="Active Object", default='',
description="")
try: vg_name = bpy.context.object.vertex_groups.active.name
except: vg_name = ''
vertex_group_streamlines : StringProperty(
name="Flow", default=vg_name,
description="Vertex Group used for streamlines")
@classmethod
def poll(cls, context):
ob = context.object
return ob and len(ob.vertex_groups) > 0 or ob.type == 'CURVE'
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self, width=250)
def draw(self, context):
if not context.object.type == 'CURVE':
self.object_name = context.object.name
ob = bpy.data.objects[self.object_name]
if self.vertex_group_streamlines not in [vg.name for vg in ob.vertex_groups]:
self.vertex_group_streamlines = ob.vertex_groups.active.name
layout = self.layout
col = layout.column(align=True)
row = col.row(align=True)
row.prop(self, 'mode', expand=True,
slider=True, toggle=False, icon_only=False, event=False,
full_event=False, emboss=True, index=-1)
col.prop(self, "use_modifiers")
col.label(text="Streamlines Curves:")
row = col.row(align=True)
row.prop(self, 'interpolation', expand=True,
slider=True, toggle=False, icon_only=False, event=False,
full_event=False, emboss=True, index=-1)
col.separator()
col.prop_search(self, 'vertex_group_streamlines', ob, "vertex_groups", text='')
if not (self.use_selected or context.mode == 'EDIT_MESH'):
row = col.row(align=True)
row.prop(self,'n_curves')
#row.enabled = context.mode != 'EDIT_MESH'
row = col.row(align=True)
row.prop(self,'rand_seed')
#row.enabled = context.mode != 'EDIT_MESH'
row = col.row(align=True)
row.prop(self,'neg_steps')
row.prop(self,'pos_steps')
#row = col.row(align=True)
#row.prop(self,'min_iso')
#row.prop(self,'max_iso')
col.prop(self, "same_weight")
col.separator()
col.label(text='Curves Bevel:')
col.prop_search(self, 'vertex_group_bevel', ob, "vertex_groups", text='')
if self.vertex_group_bevel != '':
row = col.row(align=True)
row.prop(self,'min_bevel_depth')
row.prop(self,'max_bevel_depth')
else:
col.prop(self,'bevel_depth')
col.separator()
col.prop(self, "rand_dir")
def execute(self, context):
start_time = timeit.default_timer()
try:
check = context.object.vertex_groups[0]
except:
self.report({'ERROR'}, "The object doesn't have Vertex Groups")
return {'CANCELLED'}
ob = bpy.data.objects[self.object_name]
ob.select_set(False)
seeds = []
if bpy.context.mode == 'EDIT_MESH':
self.use_selected = True
bpy.ops.object.mode_set(mode='OBJECT')
#ob = bpy.context.object
#me = simple_to_mesh(ob)
ob = convert_object_to_mesh(ob, apply_modifiers=self.use_modifiers)
#dg = context.evaluated_depsgraph_get()
#ob = ob.evaluated_get(dg)
me = ob.data
if self.use_selected:
# generate new bmesh
bm = bmesh.new()
bm.from_mesh(me)
print(len(me.vertices))
#for v in me.vertices:
# if v.select: seeds.append(v.index)
for v in bm.verts:
if v.select: seeds.append(v.index)
bm.free()
n_verts = len(me.vertices)
n_edges = len(me.edges)
n_faces = len(me.polygons)
# store weight values
try:
weight = get_weight_numpy(ob.vertex_groups[self.vertex_group_streamlines], n_verts)
except:
bpy.data.objects.remove(ob)
self.report({'ERROR'}, "Please select a Vertex Group for streamlines")
return {'CANCELLED'}
variable_bevel = False
bevel_weight = None
bevel_depth = self.bevel_depth
try:
if self.min_bevel_depth == self.max_bevel_depth:
#bevel_weight = np.ones((n_verts))
bevel_depth = self.min_bevel_depth
else:
b0 = min(self.min_bevel_depth, self.max_bevel_depth)
b1 = max(self.min_bevel_depth, self.max_bevel_depth)
bevel_weight = get_weight_numpy(ob.vertex_groups[self.vertex_group_bevel], n_verts)
if self.min_bevel_depth > self.max_bevel_depth:
bevel_weight = 1-bevel_weight
bevel_weight = b0/b1 + bevel_weight*((b1-b0)/b1)
bevel_depth = b1
variable_bevel = True
except:
pass#bevel_weight = np.ones((n_verts))
if not seeds:
np.random.seed(self.rand_seed)
seeds = np.random.randint(n_verts, size=self.n_curves)
#weight = np.array(get_weight(ob.vertex_groups.active, n_verts))
curves_pts = []
curves_weight = []
neigh = [[] for i in range(n_verts)]
if self.mode == 'EDGES':
# store neighbors
for e in me.edges:
ev = e.vertices
neigh[ev[0]].append(ev[1])
neigh[ev[1]].append(ev[0])
elif self.mode == 'VERTS':
# store neighbors
for p in me.polygons:
face_verts = [v for v in p.vertices]
n_face_verts = len(face_verts)
for i in range(n_face_verts):
fv = face_verts.copy()
neigh[fv.pop(i)] += fv
neigh_weight = [weight[n].tolist() for n in neigh]
# evaluate direction
next_vert = [-1]*n_verts
if self.rand_dir > 0:
for i in range(n_verts):
n = neigh[i]
nw = neigh_weight[i]
sorted_nw = neigh_weight[i].copy()
sorted_nw.sort()
for w in sorted_nw:
neigh[i] = [n[nw.index(w)] for w in sorted_nw]
else:
if self.pos_steps > 0:
for i in range(n_verts):
n = neigh[i]
nw = neigh_weight[i]
max_w = max(nw)
if self.same_weight:
if max_w >= weight[i]:
next_vert[i] = n[nw.index(max(nw))]
else:
if max_w > weight[i]:
next_vert[i] = n[nw.index(max(nw))]
if self.neg_steps > 0:
prev_vert = [-1]*n_verts
for i in range(n_verts):
n = neigh[i]
nw = neigh_weight[i]
min_w = min(nw)
if self.same_weight:
if min_w <= weight[i]:
prev_vert[i] = n[nw.index(min(nw))]
else:
if min_w < weight[i]:
prev_vert[i] = n[nw.index(min(nw))]
co = [0]*3*n_verts
me.vertices.foreach_get('co', co)
co = np.array(co).reshape((-1,3))
# create streamlines
curves = []
for i in seeds:
next_pts = [i]
for j in range(self.pos_steps):
if self.rand_dir > 0:
n = neigh[next_pts[-1]]
next = n[int(len(n) * (1-random.random() * self.rand_dir))]
else:
next = next_vert[next_pts[-1]]
if next > 0:
if next not in next_pts: next_pts.append(next)
else: break
prev_pts = [i]
for j in range(self.neg_steps):
if self.rand_dir > 0:
n = neigh[prev_pts[-1]]
prev = n[int(len(n) * random.random() * self.rand_dir)]
else:
prev = prev_vert[prev_pts[-1]]
if prev > 0:
if prev not in prev_pts:
prev_pts.append(prev)
else: break
next_pts = np.array(next_pts).astype('int')
prev_pts = np.flip(prev_pts[1:]).astype('int')
all_pts = np.concatenate((prev_pts, next_pts))
if len(all_pts) > 1:
curves.append(all_pts)
crv = nurbs_from_vertices(curves, co, bevel_weight, ob.name + '_Streamlines', True, self.interpolation)
crv.data.bevel_depth = bevel_depth
crv.matrix_world = ob.matrix_world
bpy.data.objects.remove(ob)
print("Streamlines Curves, total time: " + str(timeit.default_timer() - start_time) + " sec")
return {'FINISHED'}