Tissue: Update to 3.52

From https://github.com/alessandro-zomparelli/tissue/releases/tag/v0-3-52
with https://github.com/alessandro-zomparelli/tissue/pull/131 applied.

Fixes T80702
Fixes T73136
This commit is contained in:
Aaron Carlisle 2022-03-01 10:59:09 -05:00
parent b587911d7f
commit da9a50a46e
Notes: blender-bot 2023-02-14 19:02:15 +01:00
Referenced by commit d6f0fb5b: File headers: use SPDX license identifiers
Referenced by commit d6f0fb5b, File headers: use SPDX license identifiers
Referenced by issue #90681, mesh_tissue missing icon warning
Referenced by issue #80702, Tissues add-on Lag after use
Referenced by issue #73136, Built in Tissue Tools addon slows down blender
16 changed files with 12625 additions and 5055 deletions

47
mesh_tissue/README.md Normal file
View File

@ -0,0 +1,47 @@
# Tissue
![cover](http://www.co-de-it.com/wordpress/wp-content/uploads/2015/07/tissue_graphics.jpg)
Tissue - Blender's add-on for computational design by Co-de-iT
http://www.co-de-it.com/wordpress/code/blender-tissue
Tissue is already shipped with both Blender. However I recommend to update the default version downloading manually the most recent one, for more updated features and more stability.
### Blender 2.93
Tissue v0.3.52 for Blender 2.93 (latest stable release): https://github.com/alessandro-zomparelli/tissue/releases/tag/v0-3-52
Development branch (usually the most updated version): https://github.com/alessandro-zomparelli/tissue/tree/b290-dev
### Blender 2.79 (unsupported)
Tissue v0.3.4 for Blender 2.79b (latest stable release): https://github.com/alessandro-zomparelli/tissue/releases/tag/v0-3-4
Development branch (most updated version): https://github.com/alessandro-zomparelli/tissue/tree/dev1
### Installation:
1. Start Blender. Go to "Edit" and then "Preferences"
2. Open the "Add-ons" preferences
3. Click "install..." and point Blender at the downloaded zip file (on OSX it may have extracted the zip automatically, that won't work, so you have to zip the extracted folder again)
4. You may see now two different versions of Tissue, activate only the second one and ignore the first one
### Documentation
Tissue documentation for Blender 2.80: https://github.com/alessandro-zomparelli/tissue/wiki
### Issues
Please help me keeping Tissue stable and updated, report any issues or feedback here: https://github.com/alessandro-zomparelli/tissue/issues
### Contribute
Tissue is free and open-source. I really think that this is the power of Blender and I wanted to give my small contribution to it.
If you like my work and you want to help me, please consider to support me on **Patreon**, where I share some tips about Blender, Tissue and scripting: https://www.patreon.com/alessandrozomparelli
[![Patreon](http://alessandrozomparelli.com/wp-content/uploads/2020/04/patreon-transparent-vector-small.png)](https://www.patreon.com/alessandrozomparelli)
A special thanks to all my patrons, in particular to my **Tissue Supporters**: *TomaLaboratory*, *Scott Shorter*, *Garrett Post*, *Kairomon*, *Art Evans*, *Justin Davis*, *John Wise*, *Avi Bryant*, *Ahmed Saber*, *SlimeSound Production*, *Steffen Meier*.
Many thanks,
Alessandro

View File

@ -33,9 +33,9 @@
bl_info = {
"name": "Tissue",
"author": "Alessandro Zomparelli (Co-de-iT)",
"version": (0, 3, 25),
"blender": (2, 80, 0),
"location": "Sidebar > Edit Tab",
"version": (0, 3, 52),
"blender": (2, 93, 0),
"location": "",
"description": "Tools for Computational Design",
"warning": "",
"doc_url": "{BLENDER_MANUAL_URL}/addons/mesh/tissue.html",
@ -47,59 +47,107 @@ bl_info = {
if "bpy" in locals():
import importlib
importlib.reload(tessellate_numpy)
importlib.reload(colors_groups_exchanger)
importlib.reload(tissue_properties)
importlib.reload(weight_tools)
importlib.reload(dual_mesh)
importlib.reload(lattice)
importlib.reload(uv_to_mesh)
importlib.reload(utils)
importlib.reload(config)
importlib.reload(material_tools)
importlib.reload(curves_tools)
importlib.reload(polyhedra)
else:
from . import tessellate_numpy
from . import colors_groups_exchanger
from . import tissue_properties
from . import weight_tools
from . import dual_mesh
from . import lattice
from . import uv_to_mesh
from . import utils
from . import config
from . import material_tools
from . import curves_tools
from . import polyhedra
import bpy
from bpy.props import PointerProperty, CollectionProperty, BoolProperty
classes = (
tessellate_numpy.tissue_tessellate_prop,
tessellate_numpy.tessellate,
tessellate_numpy.update_tessellate,
tessellate_numpy.TISSUE_PT_tessellate,
tessellate_numpy.rotate_face,
tessellate_numpy.TISSUE_PT_tessellate_object,
colors_groups_exchanger.face_area_to_vertex_groups,
colors_groups_exchanger.vertex_colors_to_vertex_groups,
colors_groups_exchanger.vertex_group_to_vertex_colors,
colors_groups_exchanger.TISSUE_PT_weight,
colors_groups_exchanger.TISSUE_PT_color,
colors_groups_exchanger.weight_contour_curves,
colors_groups_exchanger.weight_contour_mask,
colors_groups_exchanger.weight_contour_displace,
colors_groups_exchanger.harmonic_weight,
colors_groups_exchanger.edges_deformation,
colors_groups_exchanger.edges_bending,
colors_groups_exchanger.weight_laplacian,
colors_groups_exchanger.reaction_diffusion,
colors_groups_exchanger.start_reaction_diffusion,
colors_groups_exchanger.TISSUE_PT_reaction_diffusion,
colors_groups_exchanger.reset_reaction_diffusion_weight,
colors_groups_exchanger.formula_prop,
colors_groups_exchanger.reaction_diffusion_prop,
colors_groups_exchanger.weight_formula,
colors_groups_exchanger.curvature_to_vertex_groups,
colors_groups_exchanger.weight_formula_wiki,
classes = (
config.tissuePreferences,
config.tissue_install_numba,
tissue_properties.tissue_prop,
tissue_properties.tissue_tessellate_prop,
tessellate_numpy.tissue_tessellate,
tessellate_numpy.tissue_update_tessellate,
tessellate_numpy.tissue_update_tessellate_deps,
tessellate_numpy.TISSUE_PT_tessellate,
tessellate_numpy.tissue_rotate_face_left,
tessellate_numpy.tissue_rotate_face_right,
tessellate_numpy.tissue_rotate_face_flip,
tessellate_numpy.TISSUE_PT_tessellate_object,
tessellate_numpy.TISSUE_PT_tessellate_frame,
tessellate_numpy.TISSUE_PT_tessellate_component,
tessellate_numpy.TISSUE_PT_tessellate_thickness,
tessellate_numpy.TISSUE_PT_tessellate_direction,
tessellate_numpy.TISSUE_PT_tessellate_options,
tessellate_numpy.TISSUE_PT_tessellate_coordinates,
tessellate_numpy.TISSUE_PT_tessellate_rotation,
tessellate_numpy.TISSUE_PT_tessellate_selective,
tessellate_numpy.TISSUE_PT_tessellate_morphing,
tessellate_numpy.TISSUE_PT_tessellate_iterations,
tessellate_numpy.tissue_render_animation,
weight_tools.face_area_to_vertex_groups,
weight_tools.vertex_colors_to_vertex_groups,
weight_tools.vertex_group_to_vertex_colors,
weight_tools.vertex_group_to_uv,
weight_tools.TISSUE_PT_weight,
weight_tools.TISSUE_PT_color,
weight_tools.weight_contour_curves,
weight_tools.tissue_weight_contour_curves_pattern,
weight_tools.weight_contour_mask,
weight_tools.weight_contour_displace,
weight_tools.harmonic_weight,
weight_tools.edges_deformation,
weight_tools.edges_bending,
weight_tools.weight_laplacian,
weight_tools.reaction_diffusion,
weight_tools.start_reaction_diffusion,
weight_tools.TISSUE_PT_reaction_diffusion,
weight_tools.TISSUE_PT_reaction_diffusion_weight,
weight_tools.reset_reaction_diffusion_weight,
weight_tools.formula_prop,
weight_tools.reaction_diffusion_prop,
weight_tools.weight_formula,
weight_tools.update_weight_formula,
weight_tools.curvature_to_vertex_groups,
weight_tools.weight_formula_wiki,
weight_tools.tissue_weight_distance,
weight_tools.random_weight,
weight_tools.bake_reaction_diffusion,
weight_tools.reaction_diffusion_free_data,
weight_tools.tissue_weight_streamlines,
dual_mesh.dual_mesh,
dual_mesh.dual_mesh_tessellated,
lattice.lattice_along_surface,
uv_to_mesh.uv_to_mesh
material_tools.random_materials,
material_tools.weight_to_materials,
curves_tools.tissue_to_curve_prop,
curves_tools.tissue_convert_to_curve,
curves_tools.tissue_convert_to_curve_update,
curves_tools.TISSUE_PT_convert_to_curve,
uv_to_mesh.uv_to_mesh,
polyhedra.polyhedra_wireframe
)
def register():
@ -107,28 +155,29 @@ def register():
for cls in classes:
bpy.utils.register_class(cls)
#bpy.utils.register_module(__name__)
bpy.types.Object.tissue = PointerProperty(
type=tissue_properties.tissue_prop
)
bpy.types.Object.tissue_tessellate = PointerProperty(
type=tessellate_numpy.tissue_tessellate_prop
type=tissue_properties.tissue_tessellate_prop
)
bpy.types.Object.tissue_to_curve = PointerProperty(
type=curves_tools.tissue_to_curve_prop
)
bpy.types.Object.formula_settings = CollectionProperty(
type=colors_groups_exchanger.formula_prop
type=weight_tools.formula_prop
)
bpy.types.Object.reaction_diffusion_settings = PointerProperty(
type=colors_groups_exchanger.reaction_diffusion_prop
type=weight_tools.reaction_diffusion_prop
)
# colors_groups_exchanger
bpy.app.handlers.frame_change_post.append(colors_groups_exchanger.reaction_diffusion_def)
# weight_tools
bpy.app.handlers.frame_change_post.append(weight_tools.reaction_diffusion_def)
#bpy.app.handlers.frame_change_post.append(tessellate_numpy.anim_tessellate)
def unregister():
from bpy.utils import unregister_class
for cls in classes:
bpy.utils.unregister_class(cls)
#tessellate_numpy.unregister()
#colors_groups_exchanger.unregister()
#dual_mesh.unregister()
#lattice.unregister()
#uv_to_mesh.unregister()
del bpy.types.Object.tissue_tessellate

File diff suppressed because it is too large Load Diff

63
mesh_tissue/config.py Normal file
View File

@ -0,0 +1,63 @@
import bpy
from bpy.props import (
IntProperty,
BoolProperty
)
evaluatedDepsgraph = None
class tissuePreferences(bpy.types.AddonPreferences):
bl_idname = __package__
print_stats : IntProperty(
name="Print Stats",
description="Print in the console all details about the computing time.",
default=1,
min=0,
max=4
)
use_numba_tess : BoolProperty(
name="Numba Tessellate",
description="Boost the Tessellation using Numba module. It will be slower during the first execution",
default=True
)
def draw(self, context):
from .utils_pip import Pip
Pip._ensure_user_site_package()
layout = self.layout
layout.prop(self, "print_stats")
import importlib
numba_spec = importlib.util.find_spec('numba')
found = numba_spec is not None
if found:
layout.label(text='Numba module installed correctly!', icon='INFO')
layout.prop(self, "use_numba_tess")
else:
layout.label(text='Numba module not installed!', icon='ERROR')
layout.label(text='Installing Numba will make Tissue faster', icon='INFO')
row = layout.row()
row.operator('scene.tissue_install_numba')
layout.label(text='Internet connection required. It may take few minutes', icon='URL')
class tissue_install_numba(bpy.types.Operator):
bl_idname = "scene.tissue_install_numba"
bl_label = "Install Numba"
bl_description = ("Install Numba python module")
bl_options = {'REGISTER'}
def execute(self, context):
try:
from .utils_pip import Pip
#Pip.upgrade_pip()
Pip.install('llvmlite')
Pip.install('numba')
from numba import jit, njit, guvectorize, float64, int32, prange
bool_numba = True
print('Tissue: Numba successfully installed!')
self.report({'INFO'}, 'Tissue: Numba successfully installed!')
except:
print('Tissue: Numba not loaded correctly. Try restarting Blender')
return {'FINISHED'}

803
mesh_tissue/curves_tools.py Normal file
View File

@ -0,0 +1,803 @@
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# #
# (c) Alessandro Zomparelli #
# (2017) #
# #
# http://www.co-de-it.com/ #
# #
# ############################################################################ #
import bpy, bmesh
from bpy.types import Operator
from bpy.props import (
IntProperty,
BoolProperty,
EnumProperty,
PointerProperty,
StringProperty,
FloatProperty
)
from bpy.types import (
Operator,
Panel,
PropertyGroup,
)
import numpy as np
from mathutils import Vector
from math import pi
from .utils import (
find_curves,
update_curve_from_pydata,
simple_to_mesh,
convert_object_to_mesh,
get_weight_numpy,
loops_from_bmesh,
get_mesh_before_subs
)
import time
def anim_curve_active(self, context):
ob = context.object
props = ob.tissue_to_curve
try:
props.object.name
if not ob.tissue.bool_lock:
bpy.ops.object.tissue_convert_to_curve_update()
except: pass
class tissue_to_curve_prop(PropertyGroup):
object : PointerProperty(
type=bpy.types.Object,
name="",
description="Source object",
update = anim_curve_active
)
bool_smooth : BoolProperty(
name="Smooth Shading",
default=True,
description="Output faces with smooth shading rather than flat shaded",
update = anim_curve_active
)
bool_lock : BoolProperty(
name="Lock",
description="Prevent automatic update on settings changes or if other objects have it in the hierarchy.",
default=False,
update = anim_curve_active
)
bool_dependencies : BoolProperty(
name="Update Dependencies",
description="Automatically updates source object as well, when possible",
default=False,
update = anim_curve_active
)
bool_run : BoolProperty(
name="Animatable Curve",
description="Automatically recompute the conversion when the frame is changed.",
default = False
)
use_modifiers : BoolProperty(
name="Use Modifiers",
default=True,
description="Automatically apply Modifiers and Shape Keys",
update = anim_curve_active
)
subdivision_mode : EnumProperty(
items=(
('ALL', "All", ""),
('CAGE', "Cage", ""),
('INNER', "Inner", "")
),
default='CAGE',
name="Subdivided Edges",
update = anim_curve_active
)
use_endpoint_u : BoolProperty(
name="Endpoint U",
default=True,
description="Make all open nurbs curve meet the endpoints",
update = anim_curve_active
)
clean_distance : FloatProperty(
name="Merge Distance", default=0, min=0, soft_max=10,
description="Merge Distance",
update = anim_curve_active
)
nurbs_order : IntProperty(
name="Order", default=4, min=2, max=6,
description="Nurbs order",
update = anim_curve_active
)
system : IntProperty(
name="System", default=0, min=0,
description="Particle system index",
update = anim_curve_active
)
bounds_selection : EnumProperty(
items=(
('ALL', "All", ""),
('BOUNDS', "Boundaries", ""),
('INNER', "Inner", "")
),
default='ALL',
name="Boundary Selection",
update = anim_curve_active
)
periodic_selection : EnumProperty(
items=(
('ALL', "All", ""),
('OPEN', "Open", ""),
('CLOSED', "Closed", "")
),
default='ALL',
name="Periodic Selection",
update = anim_curve_active
)
spline_type : EnumProperty(
items=(
('POLY', "Poly", ""),
('BEZIER', "Bezier", ""),
('NURBS', "NURBS", "")
),
default='POLY',
name="Spline Type",
update = anim_curve_active
)
mode : EnumProperty(
items=(
('LOOPS', "Loops", ""),
('EDGES', "Edges", ""),
('PARTICLES', "Particles", "")
),
default='LOOPS',
name="Conversion Mode",
update = anim_curve_active
)
vertex_group : StringProperty(
name="Radius", default='',
description="Vertex Group used for variable radius",
update = anim_curve_active
)
invert_vertex_group : BoolProperty(default=False,
description='Inverte the value of the Vertex Group',
update = anim_curve_active
)
vertex_group_factor : FloatProperty(
name="Factor",
default=0,
min=0,
max=1,
description="Depth bevel factor to use for zero vertex group influence",
update = anim_curve_active
)
only_sharp : BoolProperty(
default=False,
name="Only Sharp Edges",
description='Convert only Sharp edges',
update = anim_curve_active
)
pattern_depth : FloatProperty(
name="Depth",
default=0.02,
min=0,
soft_max=10,
description="Displacement pattern depth",
update = anim_curve_active
)
pattern_offset : FloatProperty(
name="Offset",
default=0,
soft_min=-1,
soft_max=1,
description="Displacement pattern offset",
update = anim_curve_active
)
pattern0 : IntProperty(
name="Step 0",
default=0,
min=0,
soft_max=10,
description="Pattern step 0",
update = anim_curve_active
)
pattern1 : IntProperty(
name="Step 1",
default=0,
min=0,
soft_max=10,
description="Pattern step 1",
update = anim_curve_active
)
class tissue_convert_to_curve(Operator):
bl_idname = "object.tissue_convert_to_curve"
bl_label = "Tissue Convert to Curve"
bl_description = "Convert selected mesh to Curve object"
bl_options = {'REGISTER', 'UNDO'}
object : StringProperty(
name="",
description="Source object",
default = ""
)
bool_smooth : BoolProperty(
name="Smooth Shading",
default=True,
description="Output faces with smooth shading rather than flat shaded"
)
use_modifiers : BoolProperty(
name="Use Modifiers",
default=True,
description="Automatically apply Modifiers and Shape Keys"
)
subdivision_mode : EnumProperty(
items=(
('ALL', "All", ""),
('CAGE', "Cage", ""),
('INNER', "Inner", "")
),
default='CAGE',
name="Subdivided Edges"
)
use_endpoint_u : BoolProperty(
name="Endpoint U",
default=True,
description="Make all open nurbs curve meet the endpoints"
)
nurbs_order : IntProperty(
name="Order", default=4, min=2, max=6,
description="Nurbs order"
)
system : IntProperty(
name="System", default=0, min=0,
description="Particle system index"
)
clean_distance : FloatProperty(
name="Merge Distance", default=0, min=0, soft_max=10,
description="Merge Distance"
)
spline_type : EnumProperty(
items=(
('POLY', "Poly", ""),
('BEZIER', "Bezier", ""),
('NURBS', "NURBS", "")
),
default='POLY',
name="Spline Type"
)
bounds_selection : EnumProperty(
items=(
('ALL', "All", ""),
('BOUNDS', "Boundaries", ""),
('INNER', "Inner", "")
),
default='ALL',
name="Boundary Selection"
)
periodic_selection : EnumProperty(
items=(
('ALL', "All", ""),
('OPEN', "Open", ""),
('CLOSED', "Closed", "")
),
default='ALL',
name="Periodic Selection"
)
mode : EnumProperty(
items=(
('LOOPS', "Loops", ""),
('EDGES', "Edges", ""),
('PARTICLES', "Particles", "")
),
default='LOOPS',
name="Conversion Mode"
)
vertex_group : StringProperty(
name="Radius", default='',
description="Vertex Group used for variable radius"
)
invert_vertex_group : BoolProperty(default=False,
description='Inverte the value of the Vertex Group'
)
vertex_group_factor : FloatProperty(
name="Factor",
default=0,
min=0,
max=1,
description="Depth bevel factor to use for zero vertex group influence"
)
only_sharp : BoolProperty(
default=False,
name="Only Sharp Edges",
description='Convert only Sharp edges'
)
pattern_depth : FloatProperty(
name="Depth",
default=0.02,
min=0,
soft_max=10,
description="Displacement pattern depth"
)
pattern_offset : FloatProperty(
name="Offset",
default=0,
soft_min=-1,
soft_max=1,
description="Displacement pattern offset"
)
pattern0 : IntProperty(
name="Step 0",
default=0,
min=0,
soft_max=10,
description="Pattern step 0"
)
pattern1 : IntProperty(
name="Step 1",
default=0,
min=0,
soft_max=10,
description="Pattern step 1"
)
@classmethod
def poll(cls, context):
try:
#bool_tessellated = context.object.tissue_tessellate.generator != None
ob = context.object
return ob.type in ('MESH','CURVE','SURFACE','FONT') and ob.mode == 'OBJECT'# and bool_tessellated
except:
return False
def invoke(self, context, event):
self.object = context.object.name
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
ob = context.object
ob0 = bpy.data.objects[self.object]
#props = ob.tissue_to_curve
layout = self.layout
col = layout.column(align=True)
row = col.row(align=True)
#row.label(text='Object: ' + self.object)
#row.prop_search(self, "object", context.scene, "objects")
#row.prop(self, "use_modifiers")#, icon='MODIFIER', text='')
col.separator()
col.label(text='Conversion Mode:')
row = col.row(align=True)
row.prop(
self, "mode", text="Conversion Mode", icon='NONE', expand=True,
slider=False, toggle=False, icon_only=False, event=False,
full_event=False, emboss=True, index=-1)
if self.mode == 'PARTICLES':
col.separator()
col.prop(self, "system")
col.separator()
if self.mode in ('LOOPS', 'EDGES'):
row = col.row(align=True)
row.prop(self, "use_modifiers")
col2 = row.column(align=True)
if self.use_modifiers:
col2.prop(self, "subdivision_mode", text='', icon='NONE', expand=False,
slider=True, toggle=False, icon_only=False, event=False,
full_event=False, emboss=True, index=-1)
col2.enabled = False
for m in bpy.data.objects[self.object].modifiers:
if m.type in ('SUBSURF','MULTIRES'): col2.enabled = True
col.separator()
row = col.row(align=True)
row.label(text='Filter Edges:')
col2 = row.column(align=True)
col2.prop(self, "bounds_selection", text='', icon='NONE', expand=False,
slider=True, toggle=False, icon_only=False, event=False,
full_event=False, emboss=True, index=-1)
col2.prop(self, 'only_sharp')
col.separator()
if self.mode == 'LOOPS':
row = col.row(align=True)
row.label(text='Filter Loops:')
row.prop(self, "periodic_selection", text='', icon='NONE', expand=False,
slider=True, toggle=False, icon_only=False, event=False,
full_event=False, emboss=True, index=-1)
col.separator()
col.label(text='Spline Type:')
row = col.row(align=True)
row.prop(
self, "spline_type", text="Spline Type", icon='NONE', expand=True,
slider=False, toggle=False, icon_only=False, event=False,
full_event=False, emboss=True, index=-1)
if self.spline_type == 'NURBS':
col.separator()
col.label(text='Nurbs splines:')
row = col.row(align=True)
row.prop(self, "use_endpoint_u")
row.prop(self, "nurbs_order")
col.separator()
col.prop(self, "bool_smooth")
if ob0.type == 'MESH' and self.mode != 'PARTICLES':
col.separator()
col.label(text='Variable Radius:')
row = col.row(align=True)
row.prop_search(self, 'vertex_group', ob0, "vertex_groups", text='')
row.prop(self, "invert_vertex_group", text="", toggle=True, icon='ARROW_LEFTRIGHT')
row.prop(self, "vertex_group_factor")
col.separator()
col.label(text='Clean curves:')
col.prop(self, "clean_distance")
col.separator()
col.label(text='Displacement Pattern:')
row = col.row(align=True)
row.prop(self, "pattern0")
row.prop(self, "pattern1")
row = col.row(align=True)
row.prop(self, "pattern_depth")
row.prop(self, "pattern_offset")
def execute(self, context):
ob = context.active_object
crv = bpy.data.curves.new(ob.name + '_Curve', type='CURVE')
crv.dimensions = '3D'
new_ob = bpy.data.objects.new(ob.name + '_Curve', crv)
bpy.context.collection.objects.link(new_ob)
context.view_layer.objects.active = new_ob
new_ob.select_set(True)
ob.select_set(False)
new_ob.matrix_world = ob.matrix_world
new_ob.tissue.tissue_type = 'TO_CURVE'
new_ob.tissue.bool_lock = True
props = new_ob.tissue_to_curve
props.object = ob
props.use_modifiers = self.use_modifiers
props.subdivision_mode = self.subdivision_mode
props.clean_distance = self.clean_distance
props.spline_type = self.spline_type
props.mode = self.mode
props.use_endpoint_u = self.use_endpoint_u
props.nurbs_order = self.nurbs_order
props.vertex_group = self.vertex_group
props.vertex_group_factor = self.vertex_group_factor
props.invert_vertex_group = self.invert_vertex_group
props.bool_smooth = self.bool_smooth
props.system = self.system
props.periodic_selection = self.periodic_selection
props.bounds_selection = self.bounds_selection
props.only_sharp = self.only_sharp
props.pattern0 = self.pattern0
props.pattern1 = self.pattern1
props.pattern_depth = self.pattern_depth
props.pattern_offset = self.pattern_offset
new_ob.tissue.bool_lock = False
bpy.ops.object.tissue_convert_to_curve_update()
return {'FINISHED'}
class tissue_convert_to_curve_update(Operator):
bl_idname = "object.tissue_convert_to_curve_update"
bl_label = "Tissue Update Curve"
bl_description = "Update Curve object"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
try:
ob = context.object
bool_curve = ob.tissue_to_curve.object != None
return ob.type == 'CURVE' and ob.mode == 'OBJECT' and bool_curve
except:
return False
def execute(self, context):
start_time = time.time()
ob = context.object
props = ob.tissue_to_curve
ob0 = props.object
if props.mode == 'PARTICLES':
eval_ob = ob0.evaluated_get(context.evaluated_depsgraph_get())
system_id = min(props.system, len(eval_ob.particle_systems))
psystem = eval_ob.particle_systems[system_id]
ob.data.splines.clear()
particles = psystem.particles
for id,p in enumerate(particles):
s = ob.data.splines.new('POLY')
if psystem.settings.type == 'HAIR':
n_pts = len(p.hair_keys)
pts = [0]*3*n_pts
p.hair_keys.foreach_get('co',pts)
co = np.array(pts).reshape((-1,3))
else:
n_pts = 2**psystem.settings.display_step + 1
pts = []
for i in range(n_pts):
vec = psystem.co_hair(eval_ob, particle_no=id,step=i)
vec = ob0.matrix_world.inverted() @ vec
pts.append(vec)
co = np.array(pts)
w = np.ones(n_pts).reshape((n_pts,1))
co = np.concatenate((co,w),axis=1).reshape((n_pts*4))
s.points.add(n_pts-1)
s.points.foreach_set('co',co)
else:
_ob0 = ob0
ob0 = convert_object_to_mesh(ob0, apply_modifiers=props.use_modifiers)
me = ob0.data
n_verts = len(me.vertices)
verts = [0]*n_verts*3
me.vertices.foreach_get('co',verts)
verts = np.array(verts).reshape((-1,3))
normals = [0]*n_verts*3
me.vertices.foreach_get('normal',normals)
normals = np.array(normals).reshape((-1,3))
#tilt = np.degrees(np.arcsin(normals[:,2]))
#tilt = np.arccos(normals[:,2])/2
verts = np.array(verts).reshape((-1,3))
if props.mode in ('LOOPS','EDGES'):
bm = bmesh.new()
bm.from_mesh(me)
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
bm.faces.ensure_lookup_table()
todo_edges = list(bm.edges)
if props.use_modifiers and props.subdivision_mode != 'ALL':
me0, subs = get_mesh_before_subs(_ob0)
n_edges0 = len(me0.edges)
bpy.data.meshes.remove(me0)
if props.subdivision_mode == 'CAGE':
todo_edges = todo_edges[:n_edges0*(2**subs)]
elif props.subdivision_mode == 'INNER':
todo_edges = todo_edges[n_edges0*(2**subs):]
if props.only_sharp:
_todo_edges = []
sharp_verts = []
for e in todo_edges:
edge = me.edges[e.index]
if edge.use_edge_sharp:
_todo_edges.append(e)
sharp_verts.append(edge.vertices[0])
sharp_verts.append(edge.vertices[1])
todo_edges = _todo_edges
if props.bounds_selection == 'BOUNDS': todo_edges = [e for e in todo_edges if len(e.link_faces)<2]
elif props.bounds_selection == 'INNER': todo_edges = [e for e in todo_edges if len(e.link_faces)>1]
if props.mode == 'EDGES':
ordered_points = [[e.verts[0].index, e.verts[1].index] for e in todo_edges]
elif props.mode == 'LOOPS':
vert_loops, edge_loops = loops_from_bmesh(todo_edges)
if props.only_sharp:
ordered_points = []
for loop in vert_loops:
loop_points = []
for v in loop:
if v.index in sharp_verts:
loop_points.append(v.index)
else:
if len(loop_points)>1:
ordered_points.append(loop_points)
loop_points = []
if len(loop_points)>1:
ordered_points.append(loop_points)
#ordered_points = [[v.index for v in loop if v.index in sharp_verts] for loop in vert_loops]
else:
ordered_points = [[v.index for v in loop] for loop in vert_loops]
if props.periodic_selection == 'CLOSED':
ordered_points = [points for points in ordered_points if points[0] == points[-1]]
elif props.periodic_selection == 'OPEN':
ordered_points = [points for points in ordered_points if points[0] != points[-1]]
else:
try:
ordered_points = find_curves(edges, n_verts)
except:
bpy.data.objects.remove(ob0)
return {'CANCELLED'}
try:
weight = get_weight_numpy(ob0.vertex_groups[props.vertex_group], n_verts)
if props.invert_vertex_group: weight = 1-weight
fact = props.vertex_group_factor
if fact > 0:
weight = weight*(1-fact) + fact
except:
weight = None
# Set curves Tilt
'''
tilt = []
for points in ordered_points:
if points[0] == points[-1]: # Closed curve
pts0 = [points[-1]] + points[:-1] # i-1
pts1 = points[:] # i
pts2 = points[1:] + [points[0]] # 1+1
else: # Open curve
pts0 = [points[0]] + points[:-1] # i-1
pts1 = points[:] # i
pts2 = points[1:] + [points[-1]] # i+1
curve_tilt = []
for i0, i1, i2 in zip(pts0, pts1, pts2):
pt0 = Vector(verts[i0])
pt1 = Vector(verts[i1])
pt2 = Vector(verts[i2])
tan1 = (pt1-pt0).normalized()
tan2 = (pt2-pt1).normalized()
vec_tan = -(tan1 + tan2).normalized()
vec2 = vec_tan.cross(Vector((0,0,1)))
vec_z = vec_tan.cross(vec2)
nor = normals[i1]
if vec_z.length == 0:
vec_z = Vector(nor)
ang = vec_z.angle(nor)
if nor[2] < 0: ang = 2*pi-ang
#if vec_tan[0] > vec_tan[1] and nor[0]>0: ang = -ang
#if vec_tan[0] > vec_tan[2] and nor[0]>0: ang = -ang
#if vec_tan[0] < vec_tan[1] and nor[1]>0: ang = -ang
#if nor[0]*nor[1]*nor[2] < 0: ang = -ang
if nor[2] == 0: ang = -5*pi/4
#ang = max(ang, np.arccos(nor[2]))
curve_tilt.append(ang)
#curve_tilt.append(np.arccos(nor[2]))
tilt.append(curve_tilt)
'''
depth = props.pattern_depth
offset = props.pattern_offset
pattern = [props.pattern0,props.pattern1]
update_curve_from_pydata(ob.data, verts, normals, weight, ordered_points, merge_distance=props.clean_distance, pattern=pattern, depth=depth, offset=offset)
bpy.data.objects.remove(ob0)
for s in ob.data.splines:
s.type = props.spline_type
if s.type == 'NURBS':
s.use_endpoint_u = props.use_endpoint_u
s.order_u = props.nurbs_order
ob.data.splines.update()
if not props.bool_smooth: bpy.ops.object.shade_flat()
end_time = time.time()
print('Tissue: object "{}" converted to Curve in {:.4f} sec'.format(ob.name, end_time-start_time))
return {'FINISHED'}
class TISSUE_PT_convert_to_curve(Panel):
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "data"
bl_label = "Tissue Convert to Curve"
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
try:
#bool_curve = context.object.tissue_to_curve.object != None
ob = context.object
return ob.type == 'CURVE' and ob.tissue.tissue_type == 'TO_CURVE'
except:
return False
def draw(self, context):
ob = context.object
props = ob.tissue_to_curve
layout = self.layout
#layout.use_property_split = True
#layout.use_property_decorate = False
col = layout.column(align=True)
row = col.row(align=True)
#col.operator("object.tissue_convert_to_curve_update", icon='FILE_REFRESH', text='Refresh')
row.operator("object.tissue_update_tessellate_deps", icon='FILE_REFRESH', text='Refresh') ####
lock_icon = 'LOCKED' if ob.tissue.bool_lock else 'UNLOCKED'
#lock_icon = 'PINNED' if props.bool_lock else 'UNPINNED'
deps_icon = 'LINKED' if ob.tissue.bool_dependencies else 'UNLINKED'
row.prop(ob.tissue, "bool_dependencies", text="", icon=deps_icon)
row.prop(ob.tissue, "bool_lock", text="", icon=lock_icon)
col2 = row.column(align=True)
col2.prop(ob.tissue, "bool_run", text="",icon='TIME')
col2.enabled = not ob.tissue.bool_lock
col.separator()
row = col.row(align=True)
row.prop_search(props, "object", context.scene, "objects")
row.prop(props, "use_modifiers", icon='MODIFIER', text='')
col.separator()
col.label(text='Conversion Mode:')
row = col.row(align=True)
row.prop(
props, "mode", icon='NONE', expand=True,
slider=False, toggle=False, icon_only=False, event=False,
full_event=False, emboss=True, index=-1)
if props.mode == 'PARTICLES':
col.separator()
col.prop(props, "system")
col.separator()
if props.mode in ('LOOPS', 'EDGES'):
row = col.row(align=True)
row.prop(props, "use_modifiers")
col2 = row.column(align=True)
if props.use_modifiers:
col2.prop(props, "subdivision_mode", text='', icon='NONE', expand=False,
slider=True, toggle=False, icon_only=False, event=False,
full_event=False, emboss=True, index=-1)
col2.enabled = False
for m in props.object.modifiers:
if m.type in ('SUBSURF','MULTIRES'): col2.enabled = True
col.separator()
row = col.row(align=True)
row.label(text='Filter Edges:')
col2 = row.column(align=True)
col2.prop(props, "bounds_selection", text='', icon='NONE', expand=False,
slider=True, toggle=False, icon_only=False, event=False,
full_event=False, emboss=True, index=-1)
col2.prop(props, 'only_sharp')
col.separator()
if props.mode == 'LOOPS':
row = col.row(align=True)
row.label(text='Filter Loops:')
row.prop(props, "periodic_selection", text='', icon='NONE', expand=False,
slider=True, toggle=False, icon_only=False, event=False,
full_event=False, emboss=True, index=-1)
col.separator()
col.label(text='Spline Type:')
row = col.row(align=True)
row.prop(
props, "spline_type", text="Spline Type", icon='NONE', expand=True,
slider=False, toggle=False, icon_only=False, event=False,
full_event=False, emboss=True, index=-1)
if props.spline_type == 'NURBS':
col.separator()
col.label(text='Nurbs Splines:')
row = col.row(align=True)
row.prop(props, "use_endpoint_u")
row.prop(props, "nurbs_order")
col.separator()
col.prop(props, "bool_smooth")
if props.object.type == 'MESH':
col.separator()
col.label(text='Variable Radius:')
row = col.row(align=True)
row.prop_search(props, 'vertex_group', props.object, "vertex_groups", text='')
row.prop(props, "invert_vertex_group", text="", toggle=True, icon='ARROW_LEFTRIGHT')
row.prop(props, "vertex_group_factor")
col.separator()
col.label(text='Clean Curves:')
col.prop(props, "clean_distance")
col.separator()
col.label(text='Displacement Pattern:')
row = col.row(align=True)
row.prop(props, "pattern0")
row.prop(props, "pattern1")
row = col.row(align=True)
row.prop(props, "pattern_depth")
row.prop(props, "pattern_offset")

View File

@ -57,29 +57,40 @@ class dual_mesh_tessellated(Operator):
('QUAD', 'Quad Faces', ''),
('TRI', 'Triangles', '')],
name="Source Faces",
description="Source polygons",
default="QUAD",
description="Triangles works with any geometry." \
"Quad option is faster when the object has only Quads",
default="TRI",
options={'LIBRARY_EDITABLE'}
)
link_component : BoolProperty(
name="Editable Component",
default=False,
description="Add Component Object to the Scene"
)
def execute(self, context):
auto_layer_collection()
ob0 = context.object
name1 = "DualMesh_{}_Component".format(self.source_faces)
# Generate component
if self.source_faces == 'QUAD':
verts = [(0.0, 0.0, 0.0), (0.0, 0.5, 0.0),
verts = [(1.0, 0.0, 0.0), (0.5, 0.0, 0.0),
(0.0, 0.0, 0.0), (0.0, 0.5, 0.0),
(0.0, 1.0, 0.0), (0.5, 1.0, 0.0),
(1.0, 1.0, 0.0), (1.0, 0.5, 0.0),
(1.0, 0.0, 0.0), (0.5, 0.0, 0.0),
(1/3, 1/3, 0.0), (2/3, 2/3, 0.0)]
(2/3, 1/3, 0.0), (1/3, 2/3, 0.0)]
edges = [(0,1), (1,2), (2,3), (3,4), (4,5), (5,6), (6,7),
(7,0), (1,8), (8,7), (3,9), (9,5), (8,9)]
faces = [(7,8,1,0), (8,9,3,2,1), (9,5,4,3), (9,8,7,6,5)]
else:
verts = [(0.0,0.0,0.0), (0.5,0.0,0.0), (1.0,0.0,0.0), (0.0,1.0,0.0), (0.5,1.0,0.0), (1.0,1.0,0.0)]
edges = [(0,1), (1,2), (2,5), (5,4), (4,3), (3,0), (1,4)]
faces = [(0,1,4,3), (1,2,5,4)]
verts = [(0.0, 0.0, 0.0), (1.0, 0.0, 0.0),
(0.0, 1.0, 0.0), (1.0, 1.0, 0.0),
(0.5, 1/3, 0.0), (0.0, 0.5, 0.0),
(1.0, 0.5, 0.0), (0.5, 0.0, 0.0)]
edges = [(0,5), (1,7), (3,6), (2,3), (2,5), (1,6), (0,7),
(4,5), (4,7), (4,6)]
faces = [(5,0,7,4), (7,1,6,4), (3,2,5,4,6)]
# check pre-existing component
try:
@ -94,38 +105,38 @@ class dual_mesh_tessellated(Operator):
me = bpy.data.meshes.new("Dual-Mesh") # add a new mesh
me.from_pydata(verts, edges, faces)
me.update(calc_edges=True, calc_edges_loose=True)
if self.source_faces == 'QUAD': n_seams = 8
else: n_seams = 6
for i in range(n_seams): me.edges[i].use_seam = True
if self.source_faces == 'QUAD': seams = (0,1,2,3,4,5,6,9)
else: seams = (0,1,2,3,4,5,7)
for i in seams: me.edges[i].use_seam = True
ob1 = bpy.data.objects.new(name1, me)
bpy.context.collection.objects.link(ob1)
# fix visualization issue
bpy.context.view_layer.objects.active = ob1
ob1.select_set(True)
bpy.ops.object.editmode_toggle()
bpy.ops.object.editmode_toggle()
ob1.select_set(False)
# hide component
ob1.hide_select = True
ob1.hide_render = True
ob1.hide_viewport = True
if self.link_component:
context.collection.objects.link(ob1)
context.view_layer.objects.active = ob1
ob1.select_set(True)
bpy.ops.object.editmode_toggle()
bpy.ops.object.editmode_toggle()
ob1.select_set(False)
ob1.hide_render = True
ob = convert_object_to_mesh(ob0,False,False)
ob.name = 'DualMesh'
#ob = bpy.data.objects.new("DualMesh", convert_object_to_mesh(ob0,False,False))
#bpy.context.collection.objects.link(ob)
#bpy.context.view_layer.objects.active = ob
#ob.select_set(True)
ob.tissue.tissue_type = 'TESSELLATE'
ob.tissue.bool_lock = True
ob.tissue_tessellate.component = ob1
ob.tissue_tessellate.generator = ob0
ob.tissue_tessellate.gen_modifiers = self.apply_modifiers
ob.tissue_tessellate.merge = True
ob.tissue_tessellate.bool_dissolve_seams = True
if self.source_faces == 'TRI': ob.tissue_tessellate.fill_mode = 'FAN'
bpy.ops.object.update_tessellate()
if self.source_faces == 'TRI': ob.tissue_tessellate.fill_mode = 'TRI'
bpy.ops.object.tissue_update_tessellate()
ob.tissue.bool_lock = False
ob.location = ob0.location
ob.matrix_world = ob0.matrix_world
return {'FINISHED'}
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
class dual_mesh(Operator):
bl_idname = "object.dual_mesh"
bl_label = "Convert to Dual Mesh"
@ -151,9 +162,9 @@ class dual_mesh(Operator):
items=[
('BEAUTY', 'Beauty', 'Arrange the new triangles evenly'),
('CLIP', 'Clip',
'Split the polygons with an ear clipping algorithm')],
name="Polygon Method",
description="Method for splitting the polygons into triangles",
'Split the N-gon with an ear clipping algorithm')],
name="N-gon Method",
description="Method for splitting the N-gons into triangles",
default="BEAUTY",
options={'LIBRARY_EDITABLE'}
)
@ -172,12 +183,12 @@ class dual_mesh(Operator):
mode = context.mode
if mode == 'EDIT_MESH':
mode = 'EDIT'
act = bpy.context.active_object
act = context.active_object
if mode != 'OBJECT':
sel = [act]
bpy.ops.object.mode_set(mode='OBJECT')
else:
sel = bpy.context.selected_objects
sel = context.selected_objects
doneMeshes = []
for ob0 in sel:
@ -206,7 +217,7 @@ class dual_mesh(Operator):
ob.data = ob.data.copy()
bpy.ops.object.select_all(action='DESELECT')
ob.select_set(True)
bpy.context.view_layer.objects.active = ob0
context.view_layer.objects.active = ob0
bpy.ops.object.mode_set(mode='EDIT')
# prevent borders erosion
@ -272,23 +283,23 @@ class dual_mesh(Operator):
bpy.ops.object.mode_set(mode='EDIT')
# select quad faces
bpy.context.tool_settings.mesh_select_mode = (False, False, True)
context.tool_settings.mesh_select_mode = (False, False, True)
bpy.ops.mesh.select_face_by_sides(number=4, extend=False)
# deselect boundaries
bpy.ops.object.mode_set(mode='OBJECT')
for i in bound_v:
bpy.context.active_object.data.vertices[i].select = False
context.active_object.data.vertices[i].select = False
for i in bound_e:
bpy.context.active_object.data.edges[i].select = False
context.active_object.data.edges[i].select = False
for i in bound_p:
bpy.context.active_object.data.polygons[i].select = False
context.active_object.data.polygons[i].select = False
bpy.ops.object.mode_set(mode='EDIT')
bpy.context.tool_settings.mesh_select_mode = (False, False, True)
context.tool_settings.mesh_select_mode = (False, False, True)
bpy.ops.mesh.edge_face_add()
bpy.context.tool_settings.mesh_select_mode = (True, False, False)
context.tool_settings.mesh_select_mode = (True, False, False)
bpy.ops.mesh.select_all(action='DESELECT')
# delete boundaries
@ -330,11 +341,12 @@ class dual_mesh(Operator):
for o in clones:
o.data = ob.data
bm.free()
for o in sel:
o.select_set(True)
bpy.context.view_layer.objects.active = act
context.view_layer.objects.active = act
bpy.ops.object.mode_set(mode=mode)
return {'FINISHED'}

View File

@ -147,7 +147,7 @@ def grid_from_mesh(mesh, swap_uv):
if len(faces_loop) == 0:
running_grid = False
bm.free()
return verts_grid, edges_grid, faces_grid
@ -240,12 +240,20 @@ class lattice_along_surface(Operator):
soft_max=1,
description="Lattice displace"
)
weight_factor : FloatProperty(
name="Factor",
default=0,
min=0.000,
max=1.000,
precision=3,
description="Thickness factor to use for zero vertex group influence"
)
grid_object = ""
source_object = ""
@classmethod
def poll(cls, context):
try: return bpy.context.object.mode == 'OBJECT'
try: return context.object.mode == 'OBJECT'
except: return False
def draw(self, context):
@ -264,6 +272,9 @@ class lattice_along_surface(Operator):
)
row = col.row()
row.prop(self, "use_groups")
if self.use_groups:
row = col.row()
row.prop(self, "weight_factor")
col.separator()
col.label(text="Scale:")
col.prop(
@ -292,16 +303,16 @@ class lattice_along_surface(Operator):
def execute(self, context):
if self.source_object == self.grid_object == "" or True:
if len(bpy.context.selected_objects) != 2:
if len(context.selected_objects) != 2:
self.report({'ERROR'}, "Please, select two objects")
return {'CANCELLED'}
grid_obj = bpy.context.object
grid_obj = context.object
if grid_obj.type not in ('MESH', 'CURVE', 'SURFACE'):
self.report({'ERROR'}, "The surface object is not valid. Only Mesh,"
"Curve and Surface objects are allowed.")
return {'CANCELLED'}
obj = None
for o in bpy.context.selected_objects:
for o in context.selected_objects:
if o.name != grid_obj.name and o.type in \
('MESH', 'CURVE', 'SURFACE', 'FONT'):
obj = o
@ -320,9 +331,9 @@ class lattice_along_surface(Operator):
grid_obj = bpy.data.objects[self.grid_object]
obj = bpy.data.objects[self.source_object]
obj_me = simple_to_mesh(obj)# obj.to_mesh(bpy.context.depsgraph, apply_modifiers=True)
for o in bpy.context.selected_objects: o.select_set(False)
for o in context.selected_objects: o.select_set(False)
grid_obj.select_set(True)
bpy.context.view_layer.objects.active = grid_obj
context.view_layer.objects.active = grid_obj
temp_grid_obj = grid_obj.copy()
temp_grid_obj.data = simple_to_mesh(grid_obj)
@ -333,7 +344,7 @@ class lattice_along_surface(Operator):
if len(grid_mesh.polygons) > 64 * 64:
bpy.data.objects.remove(temp_grid_obj)
bpy.context.view_layer.objects.active = obj
context.view_layer.objects.active = obj
obj.select_set(True)
self.report({'ERROR'}, "Maximum resolution allowed for Lattice is 64")
return {'CANCELLED'}
@ -362,7 +373,7 @@ class lattice_along_surface(Operator):
bb = max - min
lattice_loc = (max + min) / 2
bpy.ops.object.add(type='LATTICE')
lattice = bpy.context.active_object
lattice = context.active_object
lattice.location = lattice_loc
lattice.scale = Vector((bb.x / self.scale_x, bb.y / self.scale_y,
bb.z / self.scale_z))
@ -374,16 +385,14 @@ class lattice_along_surface(Operator):
if bb.z == 0:
lattice.scale.z = 1
bpy.context.view_layer.objects.active = obj
context.view_layer.objects.active = obj
bpy.ops.object.modifier_add(type='LATTICE')
obj.modifiers[-1].object = lattice
# set as parent
if self.set_parent:
obj.select_set(True)
lattice.select_set(True)
bpy.context.view_layer.objects.active = lattice
bpy.ops.object.parent_set(type='LATTICE')
override = {'active_object': obj, 'selected_objects' : [lattice,obj]}
bpy.ops.object.parent_set(override, type='OBJECT', keep_transform=False)
# reading grid structure
verts_grid, edges_grid, faces_grid = grid_from_mesh(
@ -399,15 +408,19 @@ class lattice_along_surface(Operator):
lattice.data.points_u = nu
lattice.data.points_v = nv
lattice.data.points_w = nw
if self.use_groups:
vg = temp_grid_obj.vertex_groups.active
weight_factor = self.weight_factor
for i in range(nu):
for j in range(nv):
for w in range(nw):
if self.use_groups:
try:
displace = temp_grid_obj.vertex_groups.active.weight(
verts_grid[i][j]) * scale_normal * bb.z
weight_influence = vg.weight(verts_grid[i][j])
except:
displace = 0#scale_normal * bb.z
weight_influence = 0
weight_influence = weight_influence * (1 - weight_factor) + weight_factor
displace = weight_influence * scale_normal * bb.z
else:
displace = scale_normal * bb.z
target_point = (grid_mesh.vertices[verts_grid[i][j]].co +
@ -433,7 +446,7 @@ class lattice_along_surface(Operator):
lattice.select_set(True)
obj.select_set(False)
bpy.ops.object.delete(use_global=False)
bpy.context.view_layer.objects.active = obj
context.view_layer.objects.active = obj
obj.select_set(True)
bpy.ops.object.modifier_remove(modifier=obj.modifiers[-1].name)
if nu > 64 or nv > 64:
@ -448,18 +461,18 @@ class lattice_along_surface(Operator):
#lattice.select_set(False)
obj.select_set(False)
#bpy.ops.object.delete(use_global=False)
bpy.context.view_layer.objects.active = lattice
context.view_layer.objects.active = lattice
lattice.select_set(True)
if self.high_quality_lattice:
bpy.context.object.data.points_w = 8
context.object.data.points_w = 8
else:
bpy.context.object.data.use_outside = True
context.object.data.use_outside = True
if self.hide_lattice:
bpy.ops.object.hide_view_set(unselected=False)
bpy.context.view_layer.objects.active = obj
context.view_layer.objects.active = obj
obj.select_set(True)
lattice.select_set(False)

View File

@ -0,0 +1,231 @@
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# #
# (c) Alessandro Zomparelli #
# (2020) #
# #
# http://www.co-de-it.com/ #
# #
################################################################################
import bpy
import numpy as np
import colorsys
from numpy import *
from bpy.types import (
Operator,
Panel
)
from bpy.props import (
BoolProperty,
EnumProperty,
FloatProperty,
IntProperty,
StringProperty,
FloatVectorProperty,
IntVectorProperty
)
from .utils import *
class random_materials(Operator):
bl_idname = "object.random_materials"
bl_label = "Random Materials"
bl_description = "Assign random materials to the faces of the mesh"
bl_options = {'REGISTER', 'UNDO'}
prefix : StringProperty(
name="Prefix", default="Random.", description="Name prefix")
color_A : FloatVectorProperty(name="Color A",
subtype='COLOR_GAMMA',
min=0,
max=1,
default=[0,0,0])
color_B : FloatVectorProperty(name="Color B",
subtype='COLOR_GAMMA',
min=0,
max=1,
default=[1,1,1])
hue : FloatProperty(name="Hue", min=0, max=1, default=0.5)
hue_variation : FloatProperty(name="Hue Variation", min=0, max=1, default=0.6)
seed : IntProperty(
name="Seed", default=0, description="Random seed")
count : IntProperty(
name="Count", default=3, min=2, description="Count of random materials")
generate_materials : BoolProperty(
name="Generate Materials", default=False, description="Automatically generates new materials")
random_colors : BoolProperty(
name="Random Colors", default=True, description="Colors are automatically generated")
executed = False
@classmethod
def poll(cls, context):
try: return context.object.type == 'MESH'
except: return False
def draw(self, context):
layout = self.layout
col = layout.column(align=True)
col.prop(self, "seed")
col.prop(self, "generate_materials")
if self.generate_materials:
col.prop(self, "prefix")
col.separator()
col.prop(self, "count")
#row = col.row(align=True)
col.separator()
col.label(text='Colors:')
col.prop(self, "hue")
col.prop(self, "hue_variation")
#col.prop(self, "random_colors")
if not self.random_colors:
col.prop(self, "color_A")
col.prop(self, "color_B")
def execute(self, context):
bpy.ops.object.mode_set(mode='OBJECT')
ob = context.active_object
if len(ob.material_slots) == 0 and not self.executed:
self.generate_materials = True
if self.generate_materials:
colA = self.color_A
colB = self.color_B
h1 = (self.hue - self.hue_variation/2)
h2 = (self.hue + self.hue_variation/2)
count = self.count
ob.data.materials.clear()
materials = []
for i in range(count):
mat_name = '{}{:03d}'.format(self.prefix,i)
mat = bpy.data.materials.new(mat_name)
if self.random_colors:
mat.diffuse_color = colorsys.hsv_to_rgb((h1 + (h2-h1)/(count)*i)%1, 1, 1)[:] + (1,)
else:
mat.diffuse_color = list(colA + (colB - colA)/(count-1)*i) + [1]
ob.data.materials.append(mat)
else:
count = len(ob.material_slots)
np.random.seed(seed=self.seed)
n_faces = len(ob.data.polygons)
if count > 0:
rand = list(np.random.randint(count, size=n_faces))
ob.data.polygons.foreach_set('material_index',rand)
ob.data.update()
self.executed = True
return {'FINISHED'}
class weight_to_materials(Operator):
bl_idname = "object.weight_to_materials"
bl_label = "Weight to Materials"
bl_description = "Assign materials to the faces of the mesh according to the active Vertex Group"
bl_options = {'REGISTER', 'UNDO'}
prefix : StringProperty(
name="Prefix", default="Weight.", description="Name prefix")
hue : FloatProperty(name="Hue", min=0, max=1, default=0.5)
hue_variation : FloatProperty(name="Hue Variation", min=0, max=1, default=0.3)
count : IntProperty(
name="Count", default=3, min=2, description="Count of random materials")
generate_materials : BoolProperty(
name="Generate Materials", default=False, description="Automatically generates new materials")
mode : EnumProperty(
items=(
('MIN', "Min", "Use the min weight value"),
('MAX', "Max", "Use the max weight value"),
('MEAN', "Mean", "Use the mean weight value")
),
default='MEAN',
name="Mode"
)
vg = None
@classmethod
def poll(cls, context):
try: return context.object.type == 'MESH'
except: return False
def draw(self, context):
layout = self.layout
col = layout.column(align=True)
col.prop(self, "mode")
col.prop(self, "generate_materials")
if self.generate_materials:
col.prop(self, "prefix")
col.separator()
col.prop(self, "count")
#row = col.row(align=True)
col.separator()
col.label(text='Colors:')
col.prop(self, "hue")
col.prop(self, "hue_variation")
def execute(self, context):
ob = context.active_object
if self.vg == None:
self.vg = ob.vertex_groups.active_index
vg = ob.vertex_groups[self.vg]
if vg == None:
self.report({'ERROR'}, "The selected object doesn't have any Vertex Group")
return {'CANCELLED'}
weight = get_weight_numpy(vg, len(ob.data.vertices))
if self.generate_materials:
h1 = (self.hue - self.hue_variation/2)
h2 = (self.hue + self.hue_variation/2)
count = self.count
ob.data.materials.clear()
materials = []
for i in range(count):
mat_name = '{}{:03d}'.format(self.prefix,i)
mat = bpy.data.materials.new(mat_name)
mat.diffuse_color = colorsys.hsv_to_rgb((h1 + (h2-h1)/(count)*i)%1, 1, 1)[:] + (1,)
ob.data.materials.append(mat)
else:
count = len(ob.material_slots)
faces_weight = []
for p in ob.data.polygons:
verts_id = np.array([v for v in p.vertices])
face_weight = weight[verts_id]
if self.mode == 'MIN': w = face_weight.min()
if self.mode == 'MAX': w = face_weight.max()
if self.mode == 'MEAN': w = face_weight.mean()
faces_weight.append(w)
faces_weight = np.array(faces_weight)
faces_weight = faces_weight * count
faces_weight.astype('int')
ob.data.polygons.foreach_set('material_index',list(faces_weight))
ob.data.update()
bpy.ops.object.mode_set(mode='OBJECT')
return {'FINISHED'}

View File

@ -17,19 +17,196 @@
# ##### END GPL LICENSE BLOCK #####
import numpy as np
try:
from numba import jit
import time
import sys
bool_numba = False
try:
from .utils_pip import Pip
Pip._ensure_user_site_package()
from numba import jit, njit, guvectorize, float64, int32, prange
from numba.typed import List
bool_numba = True
except:
pass
'''
try:
from .utils_pip import Pip
#Pip.upgrade_pip()
Pip.install('llvmlite')
Pip.install('numba')
from numba import jit, njit, guvectorize, float64, int32, prange
bool_numba = True
print('Tissue: Numba successfully installed!')
except:
print('Tissue: Numba not loaded correctly. Try restarting Blender')
'''
if bool_numba:
#from numba import jit, njit, guvectorize, float64, int32, prange
@njit(parallel=True)
def numba_reaction_diffusion(n_verts, n_edges, edge_verts, a, b, brush, diff_a, diff_b, f, k, dt, time_steps):
arr = np.arange(n_edges)*2
id0 = edge_verts[arr]
id1 = edge_verts[arr+1]
for i in range(time_steps):
lap_a, lap_b = rd_init_laplacian(n_verts)
numba_rd_laplacian(id0, id1, a, b, lap_a, lap_b)
numba_rd_core(a, b, lap_a, lap_b, diff_a, diff_b, f, k, dt)
numba_set_ab(a,b,brush)
return a,b
@njit(parallel=False)
def integrate_field(n_edges, id0, id1, values, edge_flow, mult, time_steps):
#n_edges = len(edge_flow)
for i in range(time_steps):
values0 = values
for j in range(n_edges):
v0 = id0[j]
v1 = id1[j]
values[v0] -= values0[v1] * edge_flow[j] * 0.001#mult[v1]
values[v1] += values0[v0] * edge_flow[j] * 0.001#mult[v0]
for j in range(n_edges):
v0 = id0[j]
v1 = id1[j]
values[v0] = max(values[v0],0)
values[v1] = max(values[v1],0)
return values
@njit(parallel=True)
def numba_reaction_diffusion_anisotropic(n_verts, n_edges, edge_verts, a, b, brush, diff_a, diff_b, f, k, dt, time_steps, grad):
arr = np.arange(n_edges)*2
id0 = edge_verts[arr]
id1 = edge_verts[arr+1]
#grad = weight_grad[id0] - weight_grad[id1]
#grad = np.abs(grad)
#grad /= abs(np.max(grad))
#grad = grad*0.98 + 0.02
for i in range(time_steps):
lap_a, lap_b = rd_init_laplacian(n_verts)
numba_rd_laplacian_anisotropic(id0, id1, a, b, lap_a, lap_b, grad)
numba_rd_core(a, b, lap_a, lap_b, diff_a, diff_b, f, k, dt)
numba_set_ab(a,b,brush)
return a,b
#@guvectorize(['(float64[:] ,float64[:] , float64[:], float64[:], float64[:], float64[:], float64[:], float64[:], float64)'],'(n),(n),(n),(n),(n),(n),(n),(n),()',target='parallel')
@njit(parallel=True)
def numba_rd_core(a, b, lap_a, lap_b, diff_a, diff_b, f, k, dt):
n = len(a)
_f = np.full(n, f[0]) if len(f) == 1 else f
_k = np.full(n, k[0]) if len(k) == 1 else k
_diff_a = np.full(n, diff_a[0]) if len(diff_a) == 1 else diff_a
_diff_b = np.full(n, diff_b[0]) if len(diff_b) == 1 else diff_b
for i in prange(n):
fi = _f[i]
ki = _k[i]
diff_ai = _diff_a[i]
diff_bi = _diff_b[i]
ab2 = a[i]*b[i]**2
a[i] += (diff_ai * lap_a[i] - ab2 + fi*(1-a[i]))*dt
b[i] += (diff_bi * lap_b[i] + ab2 - (ki+fi)*b[i])*dt
@njit(parallel=True)
def numba_rd_core_(a, b, lap_a, lap_b, diff_a, diff_b, f, k, dt):
ab2 = a*b**2
a += (diff_a*lap_a - ab2 + f*(1-a))*dt
b += (diff_b*lap_b + ab2 - (k+f)*b)*dt
@njit(parallel=True)
def numba_set_ab(a, b, brush):
n = len(a)
_brush = np.full(n, brush[0]) if len(brush) == 1 else brush
for i in prange(len(b)):
b[i] += _brush[i]
if b[i] < 0: b[i] = 0
elif b[i] > 1: b[i] = 1
if a[i] < 0: a[i] = 0
elif a[i] > 1: a[i] = 1
#@guvectorize(['(float64[:] ,float64[:] ,float64[:] , float64[:], float64[:], float64[:])'],'(m),(m),(n),(n),(n),(n)',target='parallel')
@njit(parallel=True)
def numba_rd_laplacian(id0, id1, a, b, lap_a, lap_b):
for i in prange(len(id0)):
v0 = id0[i]
v1 = id1[i]
lap_a[v0] += a[v1] - a[v0]
lap_a[v1] += a[v0] - a[v1]
lap_b[v0] += b[v1] - b[v0]
lap_b[v1] += b[v0] - b[v1]
#return lap_a, lap_b
@njit(parallel=True)
def numba_rd_laplacian_anisotropic(id0, id1, a, b, lap_a, lap_b, grad):
for i in prange(len(id0)):
v0 = id0[i]
v1 = id1[i]
lap_a[v0] += (a[v1] - a[v0])
lap_a[v1] += (a[v0] - a[v1])
lap_b[v0] -= (b[v1] - b[v0])*grad[i]
lap_b[v1] += (b[v0] - b[v1])*grad[i]
#return lap_a, lap_b
@njit(parallel=True)
def numba_rd_neigh_vertices(edge_verts):
n_edges = len(edge_verts)/2
id0 = np.zeros(n_edges)
id1 = np.zeros(n_edges)
for i in prange(n_edges):
id0[i] = edge_verts[i*2] # first vertex indices for each edge
id1[i] = edge_verts[i*2+1] # second vertex indices for each edge
return id0, id1
#@guvectorize(['(float64[:] ,float64[:] , float64[:], float64[:], float64[:])'],'(m),(n),(n),(n),(n)',target='parallel')
@njit(parallel=True)
#@njit
def numba_rd_laplacian_(edge_verts, a, b, lap_a, lap_b):
for i in prange(len(edge_verts)/2):
v0 = edge_verts[i*2]
v1 = edge_verts[i*2+1]
lap_a[v0] += a[v1] - a[v0]
lap_a[v1] += a[v0] - a[v1]
lap_b[v0] += b[v1] - b[v0]
lap_b[v1] += b[v0] - b[v1]
#return lap_a, lap_b
@njit(parallel=True)
def rd_fill_laplacian(lap_a, lap_b, id0, id1, lap_a0, lap_b0):
#for i, j, la0, lb0 in zip(id0,id1,lap_a0,lap_b0):
for index in prange(len(id0)):
i = id0[index]
j = id1[index]
la0 = lap_a0[index]
lb0 = lap_b0[index]
lap_a[i] += la0
lap_b[i] += lb0
lap_a[j] -= la0
lap_b[j] -= lb0
@njit(parallel=True)
def rd_init_laplacian(n_verts):
lap_a = np.zeros(n_verts)
lap_b = np.zeros(n_verts)
return lap_a, lap_b
'''
@jit
def numba_reaction_diffusion(n_verts, n_edges, edge_verts, a, b, diff_a, diff_b, f, k, dt, time_steps):
def numba_reaction_diffusion(n_verts, n_edges, edge_verts, a, b, diff_a, diff_b, f, k, dt, time_steps, db):
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
#dgrad = abs(grad[id1] - grad[id0])
for i in range(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
b += db
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
#lap_a0 *= dgrad
#lap_b0 *= dgrad
for i, j, la0, lb0 in zip(id0,id1,lap_a0,lap_b0):
lap_a[i] += la0
@ -42,5 +219,198 @@ try:
a += (diff_a*lap_a - ab2 + f*(1-a))*dt
b += (diff_b*lap_b + ab2 - (k+f)*b)*dt
return a, b
except:
pass
'''
'''
@njit(parallel=True)
def numba_lerp2_(v00, v10, v01, v11, vx, vy):
sh = v00.shape
co2 = np.zeros((sh[0],len(vx),sh[-1]))
for i in prange(len(v00)):
for j in prange(len(vx)):
for k in prange(len(v00[0][0])):
co0 = v00[i][0][k] + (v10[i][0][k] - v00[i][0][k]) * vx[j][0]
co1 = v01[i][0][k] + (v11[i][0][k] - v01[i][0][k]) * vx[j][0]
co2[i][j][k] = co0 + (co1 - co0) * vy[j][0]
return co2
@njit(parallel=True)
def numba_lerp2_vec(v0, vx, vy):
n_faces = v0.shape[0]
co2 = np.zeros((n_faces,len(vx),3))
for i in prange(n_faces):
for j in prange(len(vx)):
for k in prange(3):
co0 = v0[i][0][k] + (v0[i][1][k] - v0[i][0][k]) * vx[j][0]
co1 = v0[i][3][k] + (v0[i][2][k] - v0[i][3][k]) * vx[j][0]
co2[i][j][k] = co0 + (co1 - co0) * vy[j][0]
return co2
@njit(parallel=True)
def numba_lerp2__(val, vx, vy):
n_faces = len(val)
co2 = np.zeros((n_faces,len(vx),1))
for i in prange(n_faces):
for j in prange(len(vx)):
co0 = val[i][0] + (val[i][1] - val[i][0]) * val[j][0]
co1 = val[i][3] + (val[i][2] - val[i][3]) * val[j][0]
co2[i][j][0] = co0 + (co1 - co0) * vy[j][0]
return co2
'''
@njit(parallel=True)
def numba_combine_and_flatten(arrays):
n_faces = len(arrays)
n_verts = len(arrays[0])
new_list = [0.0]*n_faces*n_verts*3
for i in prange(n_faces):
for j in prange(n_verts):
for k in prange(3):
new_list[i*n_verts*3+j*3+k] = arrays[i][j,k]
return new_list
@njit(parallel=True)
def numba_calc_thickness_area_weight(co2,n2,vz,a,weight):
shape = co2.shape
n_patches = shape[0]
n_verts = shape[1]
n_co = shape[2]
nn = n2.shape[1]-1
na = a.shape[1]-1
nw = weight.shape[1]-1
co3 = np.zeros((n_patches,n_verts,n_co))
for i in prange(n_patches):
for j in prange(n_verts):
for k in prange(n_co):
co3[i,j,k] = co2[i,j,k] + n2[i,min(j,nn),k] * vz[0,j,0] * a[i,min(j,na),0] * weight[i,min(j,nw),0]
return co3
'''
@njit(parallel=True)
def numba_calc_thickness_area(co2,n2,vz,a):
shape = co2.shape
n_patches = shape[0]
n_verts = shape[1]
n_co = shape[2]
#co3 = [0.0]*n_patches*n_verts*n_co #np.zeros((n_patches,n_verts,n_co))
co3 = np.zeros((n_patches,n_verts,n_co))
for i in prange(n_patches):
for j in prange(n_verts):
for k in prange(n_co):
#co3[i,j,k] = co2[i,j,k] + n2[i,j,k] * vz[0,j,0] * a[i,j,0]
co3[i,j,k] = co2[i,j,k] + n2[i,min(j,nor_len),k] * vz[0,j,0] * a[i,j,0]
return co3
'''
@njit(parallel=True)
def numba_calc_thickness_weight(co2,n2,vz,weight):
shape = co2.shape
n_patches = shape[0]
n_verts = shape[1]
n_co = shape[2]
nn = n2.shape[1]-1
nw = weight.shape[1]-1
co3 = np.zeros((n_patches,n_verts,n_co))
for i in prange(n_patches):
for j in prange(n_verts):
for k in prange(n_co):
co3[i,j,k] = co2[i,j,k] + n2[i,min(j,nn),k] * vz[0,j,0] * weight[i,min(j,nw),0]
return co3
@njit(parallel=True)
def numba_calc_thickness(co2,n2,vz):
shape = co2.shape
n_patches = shape[0]
n_verts = shape[1]
n_co = shape[2]
nn = n2.shape[1]-1
co3 = np.zeros((n_patches,n_verts,n_co))
for i in prange(n_patches):
for j in prange(n_verts):
for k in prange(n_co):
co3[i,j,k] = co2[i,j,k] + n2[i,min(j,nn),k] * vz[0,j,0]
return co3
@njit(parallel=True)
def numba_interp_points(v00, v10, v01, v11, vx, vy):
n_patches = v00.shape[0]
n_verts = vx.shape[1]
n_verts0 = v00.shape[1]
n_co = v00.shape[2]
vxy = np.zeros((n_patches,n_verts,n_co))
for i in prange(n_patches):
for j in prange(n_verts):
j0 = min(j,n_verts0-1)
for k in prange(n_co):
co0 = v00[i,j0,k] + (v10[i,j0,k] - v00[i,j0,k]) * vx[0,j,0]
co1 = v01[i,j0,k] + (v11[i,j0,k] - v01[i,j0,k]) * vx[0,j,0]
vxy[i,j,k] = co0 + (co1 - co0) * vy[0,j,0]
return vxy
@njit(parallel=True)
def numba_interp_points_sk(v00, v10, v01, v11, vx, vy):
n_patches = v00.shape[0]
n_sk = v00.shape[1]
n_verts = v00.shape[2]
n_co = v00.shape[3]
vxy = np.zeros((n_patches,n_sk,n_verts,n_co))
for i in prange(n_patches):
for sk in prange(n_sk):
for j in prange(n_verts):
for k in prange(n_co):
co0 = v00[i,sk,j,k] + (v10[i,sk,j,k] - v00[i,sk,j,k]) * vx[0,sk,j,0]
co1 = v01[i,sk,j,k] + (v11[i,sk,j,k] - v01[i,sk,j,k]) * vx[0,sk,j,0]
vxy[i,sk,j,k] = co0 + (co1 - co0) * vy[0,sk,j,0]
return vxy
@njit
def numba_lerp(v0, v1, x):
return v0 + (v1 - v0) * x
@njit
def numba_lerp2(v00, v10, v01, v11, vx, vy):
co0 = numba_lerp(v00, v10, vx)
co1 = numba_lerp(v01, v11, vx)
co2 = numba_lerp(co0, co1, vy)
return co2
@njit(parallel=True)
def numba_lerp2_________________(v00, v10, v01, v11, vx, vy):
ni = len(v00)
nj = len(v00[0])
nk = len(v00[0][0])
co2 = np.zeros((ni,nj,nk))
for i in prange(ni):
for j in prange(nj):
for k in prange(nk):
_v00 = v00[i,j,k]
_v01 = v01[i,j,k]
_v10 = v10[i,j,k]
_v11 = v11[i,j,k]
co0 = _v00 + (_v10 - _v00) * vx[i,j,k]
co1 = _v01 + (_v11 - _v01) * vx[i,j,k]
co2[i,j,k] = co0 + (co1 - co0) * vy[i,j,k]
return co2
@njit(parallel=True)
def numba_lerp2_4(v00, v10, v01, v11, vx, vy):
ni = len(v00)
nj = len(v00[0])
nk = len(v00[0][0])
nw = len(v00[0][0][0])
co2 = np.zeros((ni,nj,nk,nw))
for i in prange(ni):
for j in prange(nj):
for k in prange(nk):
for w in prange(nw):
_v00 = v00[i,j,k]
_v01 = v01[i,j,k]
_v10 = v10[i,j,k]
_v11 = v11[i,j,k]
co0 = _v00 + (_v10 - _v00) * vx[i,j,k]
co1 = _v01 + (_v11 - _v01) * vx[i,j,k]
co2[i,j,k] = co0 + (co1 - co0) * vy[i,j,k]
return co2
#except:
# print("Tissue: Numba cannot be installed. Try to restart Blender.")
# pass

557
mesh_tissue/polyhedra.py Normal file
View File

@ -0,0 +1,557 @@
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# ---------------------------- ADAPTIVE DUPLIFACES --------------------------- #
# ------------------------------- version 0.84 ------------------------------- #
# #
# Creates duplicates of selected mesh to active morphing the shape according #
# to target faces. #
# #
# (c) Alessandro Zomparelli #
# (2017) #
# #
# http://www.co-de-it.com/ #
# #
# ############################################################################ #
import bpy
from bpy.types import (
Operator,
Panel,
PropertyGroup,
)
from bpy.props import (
BoolProperty,
EnumProperty,
FloatProperty,
IntProperty,
StringProperty,
PointerProperty
)
from mathutils import Vector, Quaternion, Matrix
import numpy as np
from math import *
import random, time, copy
import bmesh
from .utils import *
class polyhedra_wireframe(Operator):
bl_idname = "object.polyhedra_wireframe"
bl_label = "Tissue Polyhedra Wireframe"
bl_description = "Generate wireframes around the faces.\
\nDoesn't works with boundary edges.\
\n(Experimental)"
bl_options = {'REGISTER', 'UNDO'}
thickness : FloatProperty(
name="Thickness", default=0.1, min=0.001, soft_max=200,
description="Wireframe thickness"
)
subdivisions : IntProperty(
name="Segments", default=1, min=1, soft_max=10,
description="Max sumber of segments, used for the longest edge"
)
#regular_sections : BoolProperty(
# name="Regular Sections", default=False,
# description="Turn inner loops into polygons"
# )
dissolve_inners : BoolProperty(
name="Dissolve Inners", default=False,
description="Dissolve inner edges"
)
@classmethod
def poll(cls, context):
try:
#bool_tessellated = context.object.tissue_tessellate.generator != None
ob = context.object
return ob.type == 'MESH' and ob.mode == 'OBJECT'# and bool_tessellated
except:
return False
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def execute(self, context):
merge_dist = self.thickness*0.001
subs = self.subdivisions
start_time = time.time()
ob = context.object
me = simple_to_mesh(ob)
bm = bmesh.new()
bm.from_mesh(me)
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
bm.faces.ensure_lookup_table()
# Subdivide edges
proportional_subs = True
if subs > 1 and proportional_subs:
wire_length = [e.calc_length() for e in bm.edges]
all_edges = list(bm.edges)
max_segment = max(wire_length)/subs
split_edges = [[] for i in range(subs+1)]
for e, l in zip(all_edges, wire_length):
split_edges[int(l//max_segment)].append(e)
for i in range(2,subs):
perc = {}
for e in split_edges[i]:
perc[e]=0.1
bmesh.ops.bisect_edges(bm, edges=split_edges[i], cuts=i, edge_percents=perc)
### Create double faces
double_faces = []
double_layer_edge = []
double_layer_piece = []
for f in bm.faces:
verts0 = [v.co for v in f.verts]
verts1 = [v.co for v in f.verts]
verts1.reverse()
double_faces.append(verts0)
double_faces.append(verts1)
# Create new bmesh object and data layers
bm1 = bmesh.new()
# Create faces and assign Edge Layers
for verts in double_faces:
new_verts = []
for v in verts:
vert = bm1.verts.new(v)
new_verts.append(vert)
bm1.faces.new(new_verts)
bm1.verts.ensure_lookup_table()
bm1.edges.ensure_lookup_table()
bm1.faces.ensure_lookup_table()
n_faces = len(bm.faces)
n_doubles = len(bm1.faces)
polyhedra = []
for e in bm.edges:
done = []
# ERROR: Naked edges
e_faces = len(e.link_faces)
if e_faces < 2:
bm.free()
bm1.free()
message = "Naked edges are not allowed"
self.report({'ERROR'}, message)
return {'CANCELLED'}
edge_vec = e.verts[1].co - e.verts[0].co
# run first face
for i1 in range(e_faces-1):
f1 = e.link_faces[i1]
#edge_verts1 = [v.index for v in f1.verts if v in e.verts]
verts1 = [v.index for v in f1.verts]
va1 = verts1.index(e.verts[0].index)
vb1 = verts1.index(e.verts[1].index)
# chech if order of the edge matches the order of the face
dir1 = va1 == (vb1+1)%len(verts1)
edge_vec1 = edge_vec if dir1 else -edge_vec
# run second face
faces2 = []
normals2 = []
for i2 in range(i1+1,e_faces):
#for i2 in range(n_faces):
if i1 == i2: continue
f2 = e.link_faces[i2]
f2.normal_update()
#edge_verts2 = [v.index for v in f2.verts if v in e.verts]
verts2 = [v.index for v in f2.verts]
va2 = verts2.index(e.verts[0].index)
vb2 = verts2.index(e.verts[1].index)
# chech if order of the edge matches the order of the face
dir2 = va2 == (vb2+1)%len(verts2)
# check for normal consistency
if dir1 != dir2:
# add face
faces2.append(f2.index+1)
normals2.append(f2.normal)
else:
# add flipped face
faces2.append(-(f2.index+1))
normals2.append(-f2.normal)
# find first polyhedra (positive)
plane_x = f1.normal # normal
plane_y = plane_x.cross(edge_vec1) # tangent face perp edge
id1 = (f1.index+1)
min_angle0 = 10000
# check consistent faces
if id1 not in done:
id2 = None
min_angle = min_angle0
for i2, n2 in zip(faces2,normals2):
v2 = flatten_vector(-n2, plane_x, plane_y)
angle = vector_rotation(v2)
if angle < min_angle:
id2 = i2
min_angle = angle
if id2: done.append(id2)
new_poly = True
# add to existing polyhedron
for p in polyhedra:
if id1 in p or id2 in p:
new_poly = False
if id2 not in p: p.append(id2)
if id1 not in p: p.append(id1)
break
# start new polyhedron
if new_poly: polyhedra.append([id1, id2])
# find second polyhedra (negative)
plane_x = -f1.normal # normal
plane_y = plane_x.cross(-edge_vec1) # tangent face perp edge
id1 = -(f1.index+1)
if id1 not in done:
id2 = None
min_angle = min_angle0
for i2, n2 in zip(faces2, normals2):
v2 = flatten_vector(n2, plane_x, plane_y)
angle = vector_rotation(v2)
if angle < min_angle:
id2 = -i2
min_angle = angle
done.append(id2)
add = True
for p in polyhedra:
if id1 in p or id2 in p:
add = False
if id2 not in p: p.append(id2)
if id1 not in p: p.append(id1)
break
if add: polyhedra.append([id1, id2])
for i in range(len(bm1.faces)):
for j in (False,True):
if j: id = i+1
else: id = -(i+1)
join = []
keep = []
for p in polyhedra:
if id in p: join += p
else: keep.append(p)
if len(join) > 0:
keep.append(list(dict.fromkeys(join)))
polyhedra = keep
for i, p in enumerate(polyhedra):
for j in p:
bm1.faces[j].material_index = i
end_time = time.time()
print('Tissue: Polyhedra wireframe, found {} polyhedra in {:.4f} sec'.format(len(polyhedra), end_time-start_time))
delete_faces = []
wireframe_faces = []
not_wireframe_faces = []
flat_faces = []
bm.free()
#bmesh.ops.bisect_edges(bm1, edges=bm1.edges, cuts=3)
end_time = time.time()
print('Tissue: Polyhedra wireframe, subdivide edges in {:.4f} sec'.format(end_time-start_time))
bm1.faces.index_update()
#merge_verts = []
for p in polyhedra:
delete_faces_poly = []
wireframe_faces_poly = []
faces_id = [(f-1)*2 if f > 0 else (-f-1)*2+1 for f in p]
faces_id_neg = [(-f-1)*2 if -f > 0 else (f-1)*2+1 for f in p]
merge_verts = []
faces = [bm1.faces[f_id] for f_id in faces_id]
for f in faces:
delete = False
if f.index in delete_faces: continue
'''
cen = f.calc_center_median()
for e in f.edges:
mid = (e.verts[0].co + e.verts[1].co)/2
vec1 = e.verts[0].co - e.verts[1].co
vec2 = mid - cen
ang = Vector.angle(vec1,vec2)
length = vec2.length
#length = sin(ang)*length
if length < self.thickness/2:
delete = True
'''
if False:
sides = len(f.verts)
for i in range(sides):
v = f.verts[i].co
v0 = f.verts[(i-1)%sides].co
v1 = f.verts[(i+1)%sides].co
vec0 = v0 - v
vec1 = v1 - v
ang = (pi - vec0.angle(vec1))/2
length = min(vec0.length, vec1.length)*sin(ang)
if length < self.thickness/2:
delete = True
break
if delete:
delete_faces_poly.append(f.index)
else:
wireframe_faces_poly.append(f.index)
merge_verts += [v for v in f.verts]
if len(wireframe_faces_poly) < 2:
delete_faces += faces_id
not_wireframe_faces += faces_id_neg
else:
wireframe_faces += wireframe_faces_poly
flat_faces += delete_faces_poly
#wireframe_faces = list(dict.fromkeys(wireframe_faces))
bmesh.ops.remove_doubles(bm1, verts=merge_verts, dist=merge_dist)
bm1.edges.ensure_lookup_table()
bm1.faces.ensure_lookup_table()
bm1.faces.index_update()
wireframe_faces = [i for i in wireframe_faces if i not in not_wireframe_faces]
wireframe_faces = list(dict.fromkeys(wireframe_faces))
flat_faces = list(dict.fromkeys(flat_faces))
end_time = time.time()
print('Tissue: Polyhedra wireframe, merge and delete in {:.4f} sec'.format(end_time-start_time))
poly_me = me.copy()
bm1.to_mesh(poly_me)
poly_me.update()
new_ob = bpy.data.objects.new("Polyhedra", poly_me)
context.collection.objects.link(new_ob)
############# FRAME #############
bm1.faces.index_update()
wireframe_faces = [bm1.faces[i] for i in wireframe_faces]
original_faces = wireframe_faces
#bmesh.ops.remove_doubles(bm1, verts=merge_verts, dist=0.001)
# detect edge loops
loops = []
boundaries_mat = []
neigh_face_center = []
face_normals = []
# compute boundary frames
new_faces = []
wire_length = []
vert_ids = []
# append regular faces
for f in original_faces:
loop = list(f.verts)
loops.append(loop)
boundaries_mat.append([f.material_index for v in loop])
f.normal_update()
face_normals.append([f.normal for v in loop])
push_verts = []
inner_loops = []
for loop_index, loop in enumerate(loops):
is_boundary = loop_index < len(neigh_face_center)
materials = boundaries_mat[loop_index]
new_loop = []
loop_ext = [loop[-1]] + loop + [loop[0]]
# calc tangents
tangents = []
for i in range(len(loop)):
# vertices
vert0 = loop_ext[i]
vert = loop_ext[i+1]
vert1 = loop_ext[i+2]
# edge vectors
vec0 = (vert0.co - vert.co).normalized()
vec1 = (vert.co - vert1.co).normalized()
# tangent
_vec1 = -vec1
_vec0 = -vec0
ang = (pi - vec0.angle(vec1))/2
normal = face_normals[loop_index][i]
tan0 = normal.cross(vec0)
tan1 = normal.cross(vec1)
tangent = (tan0 + tan1).normalized()/sin(ang)*self.thickness/2
tangents.append(tangent)
# calc correct direction for boundaries
mult = -1
if is_boundary:
dir_val = 0
for i in range(len(loop)):
surf_point = neigh_face_center[loop_index][i]
tangent = tangents[i]
vert = loop_ext[i+1]
dir_val += tangent.dot(vert.co - surf_point)
if dir_val > 0: mult = 1
# add vertices
for i in range(len(loop)):
vert = loop_ext[i+1]
area = 1
new_co = vert.co + tangents[i] * mult * area
# add vertex
new_vert = bm1.verts.new(new_co)
new_loop.append(new_vert)
vert_ids.append(vert.index)
new_loop.append(new_loop[0])
# add faces
#materials += [materials[0]]
for i in range(len(loop)):
v0 = loop_ext[i+1]
v1 = loop_ext[i+2]
v2 = new_loop[i+1]
v3 = new_loop[i]
face_verts = [v1,v0,v3,v2]
if mult == -1: face_verts = [v0,v1,v2,v3]
new_face = bm1.faces.new(face_verts)
# Material by original edges
piece_id = 0
new_face.select = True
new_faces.append(new_face)
wire_length.append((v0.co - v1.co).length)
max_segment = max(wire_length)/self.subdivisions
#for f,l in zip(new_faces,wire_length):
# f.material_index = min(int(l/max_segment), self.subdivisions-1)
bm1.verts.ensure_lookup_table()
push_verts += [v.index for v in loop_ext]
# At this point topology han been build, but not yet thickened
end_time = time.time()
print('Tissue: Polyhedra wireframe, frames in {:.4f} sec'.format(end_time-start_time))
bm1.verts.ensure_lookup_table()
bm1.edges.ensure_lookup_table()
bm1.faces.ensure_lookup_table()
bm1.verts.index_update()
### Displace vertices ###
circle_center = [0]*len(bm1.verts)
circle_normal = [0]*len(bm1.verts)
smooth_corners = [True] * len(bm1.verts)
corners = [[] for i in range(len(bm1.verts))]
normals = [0]*len(bm1.verts)
vertices = [0]*len(bm1.verts)
# Define vectors direction
for f in new_faces:
v0 = f.verts[0]
v1 = f.verts[1]
id = v0.index
corners[id].append((v1.co - v0.co).normalized())
normals[id] = v0.normal.copy()
vertices[id] = v0
smooth_corners[id] = False
# Displace vertices
for i, vecs in enumerate(corners):
if len(vecs) > 0:
v = vertices[i]
nor = normals[i]
ang = 0
for vec in vecs:
ang += nor.angle(vec)
ang /= len(vecs)
div = sin(ang)
if div == 0: div = 1
v.co += nor*self.thickness/2/div
end_time = time.time()
print('Tissue: Polyhedra wireframe, corners displace in {:.4f} sec'.format(end_time-start_time))
# Removing original flat faces
flat_faces = [bm1.faces[i] for i in flat_faces]
for f in flat_faces:
f.material_index = self.subdivisions+1
for v in f.verts:
if smooth_corners[v.index]:
v.co += v.normal*self.thickness/2
smooth_corners[v.index] = False
delete_faces = delete_faces + [f.index for f in original_faces]
delete_faces = list(dict.fromkeys(delete_faces))
delete_faces = [bm1.faces[i] for i in delete_faces]
bmesh.ops.delete(bm1, geom=delete_faces, context='FACES')
bmesh.ops.remove_doubles(bm1, verts=bm1.verts, dist=merge_dist)
bm1.faces.ensure_lookup_table()
bm1.edges.ensure_lookup_table()
bm1.verts.ensure_lookup_table()
if self.dissolve_inners:
bm1.edges.index_update()
dissolve_edges = []
for f in bm1.faces:
e = f.edges[2]
if e not in dissolve_edges:
dissolve_edges.append(e)
bmesh.ops.dissolve_edges(bm1, edges=dissolve_edges, use_verts=True, use_face_split=True)
all_lines = [[] for e in me.edges]
all_end_points = [[] for e in me.edges]
for v in bm1.verts: v.select_set(False)
for f in bm1.faces: f.select_set(False)
_me = me.copy()
bm1.to_mesh(me)
me.update()
new_ob = bpy.data.objects.new("Wireframe", me)
context.collection.objects.link(new_ob)
for o in context.scene.objects: o.select_set(False)
new_ob.select_set(True)
context.view_layer.objects.active = new_ob
me = _me
bm1.free()
bpy.data.meshes.remove(_me)
#new_ob.location = ob.location
new_ob.matrix_world = ob.matrix_world
end_time = time.time()
print('Tissue: Polyhedra wireframe in {:.4f} sec'.format(end_time-start_time))
return {'FINISHED'}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

154
mesh_tissue/utils_pip.py Normal file
View File

@ -0,0 +1,154 @@
# -*- coding:utf-8 -*-
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# <pep8 compliant>
# ----------------------------------------------------------
# Author: Stephen Leger (s-leger)
#
# ----------------------------------------------------------
import bpy
import subprocess
import sys
PYPATH = sys.executable #bpy.app.binary_path_python
class Pip:
def __init__(self):
self._ensurepip()
@staticmethod
def _ensure_user_site_package():
import os
import site
import sys
site_package = site.getusersitepackages()
if not os.path.exists(site_package):
site_package = bpy.utils.user_resource('SCRIPTS', "site_package", create=True)
site.addsitedir(site_package)
if site_package not in sys.path:
sys.path.append(site_package)
'''
@staticmethod
def _ensure_user_site_package():
import os
import site
import sys
site_package = site.getusersitepackages()
if os.path.exists(site_package):
if site_package not in sys.path:
sys.path.append(site_package)
else:
site_package = bpy.utils.user_resource('SCRIPTS', "site_package", create=True)
site.addsitedir(site_package)
'''
def _cmd(self, action, options, module):
if options is not None and "--user" in options:
self._ensure_user_site_package()
cmd = [PYPATH, "-m", "pip", action]
if options is not None:
cmd.extend(options.split(" "))
cmd.append(module)
return self._run(cmd)
def _popen(self, cmd):
popen = subprocess.Popen(cmd, stdout=subprocess.PIPE, universal_newlines=True)
for stdout_line in iter(popen.stdout.readline, ""):
yield stdout_line
popen.stdout.close()
popen.wait()
def _run(self, cmd):
res = False
status = ""
for line in self._popen(cmd):
if "ERROR:" in line:
status = line.strip()
if "Error:" in line:
status = line.strip()
print(line)
if "Successfully" in line:
status = line.strip()
res = True
return res, status
def _ensurepip(self):
pip_not_found = False
try:
import pip
except ImportError:
pip_not_found = True
pass
if pip_not_found:
self._run([PYPATH, "-m", "ensurepip", "--default-pip"])
@staticmethod
def upgrade_pip():
return Pip()._cmd("install", "--upgrade", "pip")
@staticmethod
def uninstall(module, options=None):
"""
:param module: string module name with requirements see:[1]
:param options: string command line options see:[2]
:return: True on uninstall, False if already removed, raise on Error
[1] https://pip.pypa.io/en/stable/reference/pip_install/#id29
[2] https://pip.pypa.io/en/stable/reference/pip_install/#id47
"""
if options is None or options.strip() == "":
# force confirm
options = "-y"
return Pip()._cmd("uninstall", options, module)
@staticmethod
def install(module, options=None):
"""
:param module: string module name with requirements see:[1]
:param options: string command line options see:[2]
:return: True on install, False if already there, raise on Error
[1] https://pip.pypa.io/en/stable/reference/pip_install/#id29
[2] https://pip.pypa.io/en/stable/reference/pip_install/#id47
"""
if options is None or options.strip() == "":
# store in user writable directory, use wheel, without deps
options = "--user --only-binary all --no-deps"
return Pip()._cmd("install", options, module)
@staticmethod
def blender_version():
"""
:return: blender version tuple
"""
return bpy.app.version
@staticmethod
def python_version():
"""
:return: python version object
"""
import sys
# version.major, version.minor, version.micro
return sys.version_info

View File

@ -28,7 +28,7 @@
# #
# ############################################################################ #
import bpy
import bpy, bmesh
import math
from bpy.types import Operator
from bpy.props import BoolProperty
@ -64,48 +64,45 @@ class uv_to_mesh(Operator):
)
def execute(self, context):
if context.mode == 'EDIT_MESH': on_selection = True
else: on_selection = False
bpy.ops.object.mode_set(mode='OBJECT')
for o in bpy.data.objects and bpy.context.view_layer.objects:
o.select_set(False)
bpy.context.object.select_set(True)
ob0 = context.object
for o in bpy.context.view_layer.objects: o.select_set(False)
ob0.select_set(True)
if self.apply_modifiers:
bpy.ops.object.duplicate_move()
bpy.ops.object.convert(target='MESH')
ob0 = bpy.context.object
# me0 = ob0.to_mesh(bpy.context.depsgraph, apply_modifiers=self.apply_modifiers)
#if self.apply_modifiers: me0 = simple_to_mesh(ob0)
#else: me0 = ob0.data.copy()
name0 = ob0.name
ob0 = convert_object_to_mesh(ob0, apply_modifiers=self.apply_modifiers, preserve_status=False)
me0 = ob0.data
area = 0
verts = []
faces = []
face_materials = []
for face in me0.polygons:
if on_selection: polygons = [f for f in me0.polygons if f.select]
else: polygons = me0.polygons
bm = bmesh.new()
for face in polygons:
area += face.area
uv_face = []
store = False
try:
if len(me0.uv_layers) > 0:
verts = []
for loop in face.loop_indices:
uv = me0.uv_layers.active.data[loop].uv
if uv.x != 0 and uv.y != 0:
store = True
new_vert = Vector((uv.x, uv.y, 0))
new_vert = bm.verts.new((uv.x, uv.y, 0))
verts.append(new_vert)
uv_face.append(loop)
if store:
faces.append(uv_face)
face_materials.append(face.material_index)
except:
new_face = bm.faces.new(verts)
new_face.material_index = face.material_index
else:
self.report({'ERROR'}, "Missing UV Map")
return {'CANCELLED'}
name = name0 + 'UV'
name = name0 + '_UV'
# Create mesh and object
me = bpy.data.meshes.new(name + 'Mesh')
ob = bpy.data.objects.new(name, me)
@ -117,9 +114,10 @@ class uv_to_mesh(Operator):
ob.select_set(True)
# Create mesh from given verts, faces.
me.from_pydata(verts, [], faces)
bm.to_mesh(me)
# Update mesh with new data
me.update()
if self.auto_scale:
new_area = 0
for p in me.polygons:
@ -127,7 +125,6 @@ class uv_to_mesh(Operator):
if new_area == 0:
self.report({'ERROR'}, "Impossible to generate mesh from UV")
bpy.data.objects.remove(ob0)
return {'CANCELLED'}
# VERTEX GROUPS
@ -135,7 +132,7 @@ class uv_to_mesh(Operator):
for group in ob0.vertex_groups:
index = group.index
ob.vertex_groups.new(name=group.name)
for p in me0.polygons:
for p in polygons:
for vert, loop in zip(p.vertices, p.loop_indices):
try:
ob.vertex_groups[index].add([loop], group.weight(vert), 'REPLACE')
@ -154,25 +151,12 @@ class uv_to_mesh(Operator):
# MATERIALS
if self.materials:
try:
if len(ob0.material_slots) > 0:
# assign old material
uv_materials = [slot.material for slot in ob0.material_slots]
for i in range(len(uv_materials)):
bpy.ops.object.material_slot_add()
bpy.context.object.material_slots[i].material = uv_materials[i]
for i in range(len(ob.data.polygons)):
ob.data.polygons[i].material_index = face_materials[i]
except:
pass
'''
if self.apply_modifiers:
bpy.ops.object.mode_set(mode='OBJECT')
ob.select_set(False)
ob0.select_set(True)
bpy.ops.object.delete(use_global=False)
ob.select_set(True)
bpy.context.view_layer.objects.active = ob
'''
bpy.data.objects.remove(ob0)
bpy.data.meshes.remove(me0)

4681
mesh_tissue/weight_tools.py Normal file

File diff suppressed because it is too large Load Diff