# ##### 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 import numpy as np from math import sqrt import random, time import bmesh from .utils import * def anim_tessellate_active(self, context): ob = context.object props = ob.tissue_tessellate if not props.bool_hold: try: props.generator.name props.component.name bpy.ops.object.update_tessellate() except: pass def anim_tessellate_object(ob): try: #bpy.context.view_layer.objects.active = ob bpy.ops.object.update_tessellate() except: return None #from bpy.app.handlers import persistent #@persistent def anim_tessellate(scene): # store selected objects #scene = context.scene try: active_object = bpy.context.object except: active_object = None try: selected_objects = bpy.context.selected_objects except: selected_objects = [] if bpy.context.mode in ('OBJECT', 'PAINT_WEIGHT'): old_mode = bpy.context.mode if old_mode == 'PAINT_WEIGHT': old_mode = 'WEIGHT_PAINT' for ob in scene.objects: if ob.tissue_tessellate.bool_run: hidden = ob.hide_viewport ob.hide_viewport = False for o in scene.objects: if not o.hide_viewport: ob.select_set(False) bpy.context.view_layer.objects.active = ob ob.select_set(True) try: bpy.ops.object.update_tessellate() except: pass ob.hide_viewport = hidden # restore selected objects for o in scene.objects: if not o.hide_viewport: o.select_set(False) for o in selected_objects: if not o.hide_viewport: o.select_set(True) bpy.context.view_layer.objects.active = active_object try: bpy.ops.object.mode_set(mode=old_mode) except: pass return def set_tessellate_handler(self, context): old_handlers = [] blender_handlers = bpy.app.handlers.frame_change_post for h in blender_handlers: if "anim_tessellate" in str(h): old_handlers.append(h) for h in old_handlers: blender_handlers.remove(h) for o in context.scene.objects: if o.tissue_tessellate.bool_run: blender_handlers.append(anim_tessellate) break return class tissue_tessellate_prop(PropertyGroup): bool_hold : BoolProperty( name="Hold Update", description="Prevent automatic update while other properties are changed", default=False ) bool_run : BoolProperty( name="Animatable Tessellation", description="Automatically recompute the tessellation when the frame is changed. Currently is not working during Render Animation", default = False, update = set_tessellate_handler ) zscale : FloatProperty( name="Scale", default=1, soft_min=0, soft_max=10, description="Scale factor for the component thickness", update = anim_tessellate_active ) scale_mode : EnumProperty( items=( ('CONSTANT', "Constant", "Uniform thinkness"), ('ADAPTIVE', "Proportional", "Preserve component's proportions") ), default='ADAPTIVE', name="Z-Scale according to faces size", update = anim_tessellate_active ) offset : FloatProperty( name="Surface Offset", default=1, min=-1, max=1, soft_min=-1, soft_max=1, description="Surface offset", update = anim_tessellate_active ) mode : EnumProperty( items=( ('BOUNDS', "Bounds", "The component fits automatically the size of the target face"), ('LOCAL', "Local", "Based on Local coordinates, from 0 to 1"), ('GLOBAL', 'Global', "Based on Global coordinates, from 0 to 1")), default='BOUNDS', name="Component Mode", update = anim_tessellate_active ) rotation_mode : EnumProperty( items=(('RANDOM', "Random", "Random faces rotation"), ('UV', "Active UV", "Rotate according to UV coordinates"), ('DEFAULT', "Default", "Default rotation")), default='DEFAULT', name="Component Rotation", update = anim_tessellate_active ) fill_mode : EnumProperty( items=( ('QUAD', 'Quad', 'Regular quad tessellation. Uses only 3 or 4 vertices'), ('FAN', 'Fan', 'Radial tessellation for polygonal faces'), ('PATCH', 'Patch', 'Curved tessellation according to the last ' + 'Subsurf\n(or Multires) modifiers. Works only with 4 sides ' + 'patches.\nAfter the last Subsurf (or Multires) only ' + 'deformation\nmodifiers can be used')), default='QUAD', name="Fill Mode", update = anim_tessellate_active ) combine_mode : EnumProperty( items=( ('LAST', 'Last', 'Show only the last iteration'), ('UNUSED', 'Unused', 'Combine each iteration with the unused faces of the previous iteration. Used for branching systems'), ('ALL', 'All', 'Combine the result of all iterations')), default='LAST', name="Combine Mode", update = anim_tessellate_active ) gen_modifiers : BoolProperty( name="Generator Modifiers", default=False, description="Apply Modifiers and Shape Keys to the base object", update = anim_tessellate_active ) com_modifiers : BoolProperty( name="Component Modifiers", default=False, description="Apply Modifiers and Shape Keys to the component object", update = anim_tessellate_active ) merge : BoolProperty( name="Merge", default=False, description="Merge vertices in adjacent duplicates", update = anim_tessellate_active ) merge_thres : FloatProperty( name="Distance", default=0.001, soft_min=0, soft_max=10, description="Limit below which to merge vertices", update = anim_tessellate_active ) generator : PointerProperty( type=bpy.types.Object, name="", description="Base object for the tessellation", update = anim_tessellate_active ) component : PointerProperty( type=bpy.types.Object, name="", description="Component object for the tessellation", #default="", update = anim_tessellate_active ) bool_random : BoolProperty( name="Randomize", default=False, description="Randomize component rotation", update = anim_tessellate_active ) random_seed : IntProperty( name="Seed", default=0, soft_min=0, soft_max=10, description="Random seed", update = anim_tessellate_active ) bool_vertex_group : BoolProperty( name="Map Vertex Group", default=False, description="Transfer all Vertex Groups from Base object", update = anim_tessellate_active ) bool_selection : BoolProperty( name="On selected Faces", default=False, description="Create Tessellation only on selected faces", update = anim_tessellate_active ) bool_shapekeys : BoolProperty( name="Use Shape Keys", default=False, description="Transfer Component's Shape Keys. If the name of Vertex " "Groups and Shape Keys are the same, they will be " "automatically combined", update = anim_tessellate_active ) bool_smooth : BoolProperty( name="Smooth Shading", default=False, description="Output faces with smooth shading rather than flat shaded", update = anim_tessellate_active ) bool_materials : BoolProperty( name="Transfer Materials", default=False, description="Preserve component's materials", update = anim_tessellate_active ) bool_material_id : BoolProperty( name="Tessellation on Material ID", default=False, description="Apply the component only on the selected Material", update = anim_tessellate_active ) material_id : IntProperty( name="Material ID", default=0, min=0, description="Material ID", update = anim_tessellate_active ) bool_dissolve_seams : BoolProperty( name="Dissolve Seams", default=False, description="Dissolve all seam edges", update = anim_tessellate_active ) iterations : IntProperty( name="Iterations", default=1, min=1, soft_max=5, description="Automatically repeat the Tessellation using the " + "generated geometry as new base object.\nUseful for " + "for branching systems. Dangerous!", update = anim_tessellate_active ) bool_combine : BoolProperty( name="Combine unused", default=False, description="Combine the generated geometry with unused faces", update = anim_tessellate_active ) bool_advanced : BoolProperty( name="Advanced Settings", default=False, description="Show more settings" ) normals_mode : EnumProperty( items=( ('VERTS', 'Along Normals', 'Consistent direction based on vertices normal'), ('FACES', 'Individual Faces', 'Based on individual faces normal')), default='VERTS', name="Direction", update = anim_tessellate_active ) bool_multi_components : BoolProperty( name="Multi Components", default=False, description="Combine different components according to materials name", update = anim_tessellate_active ) error_message : StringProperty( name="Error Message", default="" ) warning_message : StringProperty( name="Warning Message", default="" ) bounds_x : EnumProperty( items=( ('EXTEND', 'Extend', 'Default X coordinates'), ('CLIP', 'Clip', 'Trim out of bounds in X direction'), ('CYCLIC', 'Cyclic', 'Cyclic components in X direction')), default='EXTEND', name="Bounds X", update = anim_tessellate_active ) bounds_y : EnumProperty( items=( ('EXTEND', 'Extend', 'Default Y coordinates'), ('CLIP', 'Clip', 'Trim out of bounds in Y direction'), ('CYCLIC', 'Cyclic', 'Cyclic components in Y direction')), default='EXTEND', name="Bounds Y", update = anim_tessellate_active ) cap_faces : BoolProperty( name="Cap Holes", default=False, description="Cap open edges loops", update = anim_tessellate_active ) open_edges_crease : FloatProperty( name="Open Edges Crease", default=0, min=0, max=1, description="Automatically set crease for open edges", update = anim_tessellate_active ) def store_parameters(operator, ob): ob.tissue_tessellate.bool_hold = True ob.tissue_tessellate.generator = bpy.data.objects[operator.generator] ob.tissue_tessellate.component = bpy.data.objects[operator.component] ob.tissue_tessellate.zscale = operator.zscale ob.tissue_tessellate.offset = operator.offset ob.tissue_tessellate.gen_modifiers = operator.gen_modifiers ob.tissue_tessellate.com_modifiers = operator.com_modifiers ob.tissue_tessellate.mode = operator.mode ob.tissue_tessellate.rotation_mode = operator.rotation_mode ob.tissue_tessellate.merge = operator.merge ob.tissue_tessellate.merge_thres = operator.merge_thres ob.tissue_tessellate.scale_mode = operator.scale_mode ob.tissue_tessellate.bool_random = operator.bool_random ob.tissue_tessellate.random_seed = operator.random_seed ob.tissue_tessellate.fill_mode = operator.fill_mode ob.tissue_tessellate.bool_vertex_group = operator.bool_vertex_group ob.tissue_tessellate.bool_selection = operator.bool_selection ob.tissue_tessellate.bool_shapekeys = operator.bool_shapekeys ob.tissue_tessellate.bool_smooth = operator.bool_smooth ob.tissue_tessellate.bool_materials = operator.bool_materials ob.tissue_tessellate.bool_material_id = operator.bool_material_id ob.tissue_tessellate.material_id = operator.material_id ob.tissue_tessellate.bool_dissolve_seams = operator.bool_dissolve_seams ob.tissue_tessellate.iterations = operator.iterations ob.tissue_tessellate.bool_advanced = operator.bool_advanced ob.tissue_tessellate.normals_mode = operator.normals_mode ob.tissue_tessellate.bool_combine = operator.bool_combine ob.tissue_tessellate.bool_multi_components = operator.bool_multi_components ob.tissue_tessellate.combine_mode = operator.combine_mode ob.tissue_tessellate.bounds_x = operator.bounds_x ob.tissue_tessellate.bounds_y = operator.bounds_y ob.tissue_tessellate.cap_faces = operator.cap_faces ob.tissue_tessellate.bool_hold = False return ob def tessellate_patch(_ob0, _ob1, offset, zscale, com_modifiers, mode, scale_mode, rotation_mode, rand_seed, bool_vertex_group, bool_selection, bool_shapekeys, bool_material_id, material_id, bounds_x, bounds_y): random.seed(rand_seed) ob0 = convert_object_to_mesh(_ob0) me0 = _ob0.data # Check if zero faces are selected if _ob0.type == 'MESH': bool_cancel = True for p in me0.polygons: check_sel = check_mat = False if not bool_selection or p.select: check_sel = True if not bool_material_id or p.material_index == material_id: check_mat = True if check_sel and check_mat: bool_cancel = False break if bool_cancel: return 0 levels = 0 sculpt_levels = 0 render_levels = 0 bool_multires = False multires_name = "" not_allowed = ['FLUID_SIMULATION', 'ARRAY', 'BEVEL', 'BOOLEAN', 'BUILD', 'DECIMATE', 'EDGE_SPLIT', 'MASK', 'MIRROR', 'REMESH', 'SCREW', 'SOLIDIFY', 'TRIANGULATE', 'WIREFRAME', 'SKIN', 'EXPLODE', 'PARTICLE_INSTANCE', 'PARTICLE_SYSTEM', 'SMOKE'] modifiers0 = list(_ob0.modifiers)#[m for m in ob0.modifiers] show_modifiers = [m.show_viewport for m in _ob0.modifiers] show_modifiers.reverse() modifiers0.reverse() for m in modifiers0: visible = m.show_viewport #m.show_viewport = False if m.type in ('SUBSURF', 'MULTIRES') and visible: levels = m.levels multires_name = m.name if m.type == 'MULTIRES': bool_multires = True multires_name = m.name sculpt_levels = m.sculpt_levels render_levels = m.render_levels else: bool_multires = False break elif m.type in not_allowed: #ob0.data = old_me0 #bpy.data.meshes.remove(me0) return "modifiers_error" before = _ob0.copy() #if ob0.type == 'MESH': before.data = me0 before_mod = list(before.modifiers) before_mod.reverse() for m in before_mod: if m.type in ('SUBSURF', 'MULTIRES') and m.show_viewport: before.modifiers.remove(m) break else: before.modifiers.remove(m) before_subsurf = simple_to_mesh(before) before_bm = bmesh.new() before_bm.from_mesh(before_subsurf) before_bm.faces.ensure_lookup_table() for f in before_bm.faces: if len(f.loops) != 4: return "topology_error" before_bm.edges.ensure_lookup_table() for e in before_bm.edges: if len(e.link_faces) == 0: return "wires_error" before_bm.verts.ensure_lookup_table() for v in before_bm.verts: if len(v.link_faces) == 0: return "verts_error" me0 = ob0.data verts0 = me0.vertices # Collect generator vertices if com_modifiers or _ob1.type != 'MESH': bool_shapekeys = False # set Shape Keys to zero if bool_shapekeys: try: original_key_values = [] for sk in _ob1.data.shape_keys.key_blocks: original_key_values.append(sk.value) sk.value = 0 except: bool_shapekeys = False if not com_modifiers and not bool_shapekeys: mod_visibility = [] for m in _ob1.modifiers: mod_visibility.append(m.show_viewport) m.show_viewport = False com_modifiers = True ob1 = convert_object_to_mesh(_ob1, com_modifiers, False) me1 = ob1.data if mode != 'BOUNDS': bpy.context.object.active_shape_key_index = 0 # Bound X if bounds_x != 'EXTEND': if mode == 'GLOBAL': planes_co = ((0,0,0),(1,1,1)) plane_no = (1,0,0) if mode == 'LOCAL': planes_co = (ob1.matrix_world @ Vector((0,0,0)), ob1.matrix_world @ Vector((1,0,0))) plane_no = planes_co[0]-planes_co[1] bpy.ops.object.mode_set(mode='EDIT') for co in planes_co: bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.bisect(plane_co=co, plane_no=plane_no) bpy.ops.mesh.mark_seam() bpy.ops.object.mode_set(mode='OBJECT') _faces = ob1.data.polygons if mode == 'GLOBAL': for f in [f for f in _faces if (ob1.matrix_world @ f.center).x > 1]: f.select = True for f in [f for f in _faces if (ob1.matrix_world @ f.center).x < 0]: f.select = True else: for f in [f for f in _faces if f.center.x > 1]: f.select = True for f in [f for f in _faces if f.center.x < 0]: f.select = True bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_mode(type='FACE') if bounds_x == 'CLIP': bpy.ops.mesh.delete(type='FACE') bpy.ops.object.mode_set(mode='OBJECT') if bounds_x == 'CYCLIC': bpy.ops.mesh.split() bpy.ops.object.mode_set(mode='OBJECT') # Bound Y if bounds_y != 'EXTEND': if mode == 'GLOBAL': planes_co = ((0,0,0),(1,1,1)) plane_no = (0,1,0) if mode == 'LOCAL': planes_co = (ob1.matrix_world @ Vector((0,0,0)), ob1.matrix_world @ Vector((0,1,0))) plane_no = planes_co[0]-planes_co[1] bpy.ops.object.mode_set(mode='EDIT') for co in planes_co: bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.bisect(plane_co=co, plane_no=plane_no) bpy.ops.mesh.mark_seam() bpy.ops.object.mode_set(mode='OBJECT') _faces = ob1.data.polygons if mode == 'GLOBAL': for f in [f for f in _faces if (ob1.matrix_world @ f.center).y > 1]: f.select = True for f in [f for f in _faces if (ob1.matrix_world @ f.center).y < 0]: f.select = True else: for f in [f for f in _faces if f.center.y > 1]: f.select = True for f in [f for f in _faces if f.center.y < 0]: f.select = True bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_mode(type='FACE') if bounds_y == 'CLIP': bpy.ops.mesh.delete(type='FACE') bpy.ops.object.mode_set(mode='OBJECT') if bounds_y == 'CYCLIC': bpy.ops.mesh.split() bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.mode_set(mode='OBJECT') # Component statistics n_verts = len(me1.vertices) # Create empty lists new_verts = [] new_edges = [] new_faces = [] new_verts_np = np.array(()) # Component bounding box min_c = Vector((0, 0, 0)) max_c = Vector((0, 0, 0)) first = True for v in me1.vertices: vert = v.co if vert[0] < min_c[0] or first: min_c[0] = vert[0] if vert[1] < min_c[1] or first: min_c[1] = vert[1] if vert[2] < min_c[2] or first: min_c[2] = vert[2] if vert[0] > max_c[0] or first: max_c[0] = vert[0] if vert[1] > max_c[1] or first: max_c[1] = vert[1] if vert[2] > max_c[2] or first: max_c[2] = vert[2] first = False bb = max_c - min_c # adaptive XY verts1 = [] for v in me1.vertices: if mode == 'BOUNDS': vert = v.co - min_c # (ob1.matrix_world * v.co) - min_c vert[0] = (vert[0] / bb[0] if bb[0] != 0 else 0.5) vert[1] = (vert[1] / bb[1] if bb[1] != 0 else 0.5) vert[2] = (vert[2] + (-0.5 + offset * 0.5) * bb[2]) * zscale elif mode == 'LOCAL': vert = v.co.xyz vert[2] *= zscale #vert[2] = (vert[2] - min_c[2] + (-0.5 + offset * 0.5) * bb[2]) * zscale elif mode == 'GLOBAL': vert = ob1.matrix_world @ v.co vert[2] *= zscale try: for sk in me1.shape_keys.key_blocks: sk.data[v.index].co = ob1.matrix_world @ sk.data[v.index].co except: pass #verts1.append(vert) v.co = vert # Bounds X, Y if mode != 'BOUNDS': if bounds_x == 'CYCLIC': move_verts = [] for f in [f for f in me1.polygons if (f.center).x > 1]: for v in f.vertices: if v not in move_verts: move_verts.append(v) for v in move_verts: me1.vertices[v].co.x -= 1 try: _ob1.active_shape_key_index = 0 for sk in me1.shape_keys.key_blocks: sk.data[v].co.x -= 1 except: pass move_verts = [] for f in [f for f in me1.polygons if (f.center).x < 0]: for v in f.vertices: if v not in move_verts: move_verts.append(v) for v in move_verts: me1.vertices[v].co.x += 1 try: _ob1.active_shape_key_index = 0 for sk in me1.shape_keys.key_blocks: sk.data[v].co.x += 1 except: pass if bounds_y == 'CYCLIC': move_verts = [] for f in [f for f in me1.polygons if (f.center).y > 1]: for v in f.vertices: if v not in move_verts: move_verts.append(v) for v in move_verts: me1.vertices[v].co.y -= 1 try: _ob1.active_shape_key_index = 0 for sk in me1.shape_keys.key_blocks: sk.data[v].co.y -= 1 except: pass move_verts = [] for f in [f for f in me1.polygons if (f.center).y < 0]: for v in f.vertices: if v not in move_verts: move_verts.append(v) for v in move_verts: me1.vertices[v].co.y += 1 try: _ob1.active_shape_key_index = 0 for sk in me1.shape_keys.key_blocks: sk.data[v].co.y += 1 except: pass verts1 = [v.co for v in me1.vertices] patch_faces = 4**levels sides = int(sqrt(patch_faces)) sides0 = sides-2 patch_faces0 = int((sides-2)**2) n_patches = int(len(me0.polygons)/patch_faces) if len(me0.polygons)%patch_faces != 0: #ob0.data = old_me0 return "topology_error" new_verts = [] new_edges = [] new_faces = [] for o in bpy.context.view_layer.objects: o.select_set(False) new_patch = None # All vertex group if bool_vertex_group: try: weight = [] for vg in ob0.vertex_groups: _weight = [] for v in me0.vertices: try: _weight.append(vg.weight(v.index)) except: _weight.append(0) weight.append(_weight) except: bool_vertex_group = False # Adaptive Z if scale_mode == 'ADAPTIVE': if mode == 'BOUNDS': com_area = (bb[0]*bb[1]) else: com_area = 1 mult = 1/com_area*patch_faces verts_area = [] bm = bmesh.new() bm.from_mesh(me0) bm.verts.ensure_lookup_table() for v in bm.verts: area = 0 faces = v.link_faces for f in faces: area += f.calc_area() area/=len(faces) area*=mult verts_area.append(sqrt(area)) random.seed(rand_seed) bool_correct = False _faces = [[[0] for ii in range(sides)] for jj in range(sides)] _verts = [[[0] for ii in range(sides+1)] for jj in range(sides+1)] for i in range(n_patches): poly = me0.polygons[i*patch_faces] if bool_selection and not poly.select: continue if bool_material_id and not poly.material_index == material_id: continue bool_correct = True new_patch = bpy.data.objects.new("patch", me1.copy()) bpy.context.collection.objects.link(new_patch) new_patch.select_set(True) bpy.context.view_layer.objects.active = new_patch for area in bpy.context.screen.areas: for space in area.spaces: try: new_patch.local_view_set(space, True) except: pass # Vertex Group if bool_vertex_group: for vg in ob0.vertex_groups: new_patch.vertex_groups.new(name=vg.name) # find patch faces faces = _faces.copy() verts = _verts.copy() shift1 = sides shift2 = sides*2-1 shift3 = sides*3-2 for j in range(patch_faces): if j < patch_faces0: if levels == 0: u = j%sides0 v = j//sides0 else: u = j%sides0+1 v = j//sides0+1 elif j < patch_faces0 + shift1: u = j-patch_faces0 v = 0 elif j < patch_faces0 + shift2: u = sides-1 v = j-(patch_faces0 + sides)+1 elif j < patch_faces0 + shift3: jj = j-(patch_faces0 + shift2) u = sides-jj-2 v = sides-1 else: jj = j-(patch_faces0 + shift3) u = 0 v = sides-jj-2 face = me0.polygons[j+i*patch_faces] faces[u][v] = face verts[u][v] = verts0[face.vertices[0]] if u == sides-1: verts[sides][v] = verts0[face.vertices[1]] if v == sides-1: verts[u][sides] = verts0[face.vertices[3]] if u == v == sides-1: verts[sides][sides] = verts0[face.vertices[2]] # Random rotation if rotation_mode == 'RANDOM': rand = random.randint(0, 3) if rand == 1: verts = [[verts[k][w] for w in range(sides,-1,-1)] for k in range(sides,-1,-1)] elif rand == 2: verts = [[verts[w][k] for w in range(sides,-1,-1)] for k in range(sides+1)] elif rand == 3: verts = [[verts[w][k] for w in range(sides+1)] for k in range(sides,-1,-1)] # UV rotation elif rotation_mode == 'UV' and ob0.type == 'MESH': if len(ob0.data.uv_layers) > 0: uv0 = me0.uv_layers.active.data[faces[0][0].index*4].uv uv1 = me0.uv_layers.active.data[faces[0][-1].index*4 + 3].uv uv2 = me0.uv_layers.active.data[faces[-1][-1].index*4 + 2].uv uv3 = me0.uv_layers.active.data[faces[-1][0].index*4 + 1].uv v01 = (uv0 + uv1) v32 = (uv3 + uv2) v0132 = v32 - v01 v0132.normalize() v12 = (uv1 + uv2) v03 = (uv0 + uv3) v1203 = v03 - v12 v1203.normalize() vertUV = [] dot1203 = v1203.x dot0132 = v0132.x if(abs(dot1203) < abs(dot0132)): if (dot0132 > 0): pass else: verts = [[verts[k][w] for w in range(sides,-1,-1)] for k in range(sides,-1,-1)] else: if(dot1203 < 0): verts = [[verts[w][k] for w in range(sides,-1,-1)] for k in range(sides+1)] else: verts = [[verts[w][k] for w in range(sides+1)] for k in range(sides,-1,-1)] step = 1/sides for vert, patch_vert in zip(verts1, new_patch.data.vertices): # grid coordinates u = int(vert[0]//step) v = int(vert[1]//step) u1 = min(u+1, sides) v1 = min(v+1, sides) if mode != 'BOUNDS': if u > sides-1: u = sides-1 u1 = sides if u < 0: u = 0 u1 = 1 if v > sides-1: v = sides-1 v1 = sides if v < 0: v = 0 v1 = 1 v00 = verts[u][v] v10 = verts[u1][v] v01 = verts[u][v1] v11 = verts[u1][v1] # factor coordinates fu = (vert[0]-u*step)/step fv = (vert[1]-v*step)/step fw = vert.z # interpolate Z scaling factor fvec2d = Vector((fu,fv,0)) if scale_mode == 'ADAPTIVE': a00 = verts_area[v00.index] a10 = verts_area[v10.index] a01 = verts_area[v01.index] a11 = verts_area[v11.index] fw*=lerp2(a00,a10,a01,a11,fvec2d) # build factor vector fvec = Vector((fu,fv,fw)) # interpolate vertex on patch patch_vert.co = lerp3(v00, v10, v01, v11, fvec) # Vertex Group if bool_vertex_group: for _weight, vg in zip(weight, new_patch.vertex_groups): w00 = _weight[v00.index] w10 = _weight[v10.index] w01 = _weight[v01.index] w11 = _weight[v11.index] wuv = lerp2(w00,w10,w01,w11, fvec2d) vg.add([patch_vert.index], wuv, "ADD") if bool_shapekeys: for sk in ob1.data.shape_keys.key_blocks: source = sk.data for sk_v, _v in zip(source, me1.vertices): if mode == 'BOUNDS': sk_vert = sk_v.co - min_c # (ob1.matrix_world * v.co) - min_c sk_vert[0] = (sk_vert[0] / bb[0] if bb[0] != 0 else 0.5) sk_vert[1] = (sk_vert[1] / bb[1] if bb[1] != 0 else 0.5) sk_vert[2] = (sk_vert[2] + (-0.5 + offset * 0.5) * bb[2]) * zscale elif mode == 'LOCAL': sk_vert = sk_v.co#.xyzco #sk_vert[2] *= zscale #sk_vert[2] = (sk_vert[2] - min_c[2] + (-0.5 + offset * 0.5) * bb[2]) * zscale elif mode == 'GLOBAL': #sk_vert = ob1.matrix_world @ sk_v.co sk_vert = sk_v.co #sk_vert[2] *= zscale # grid coordinates u = int(sk_vert[0]//step) v = int(sk_vert[1]//step) u1 = min(u+1, sides) v1 = min(v+1, sides) if mode != 'BOUNDS': if u > sides-1: u = sides-1 u1 = sides if u < 0: u = 0 u1 = 1 if v > sides-1: v = sides-1 v1 = sides if v < 0: v = 0 v1 = 1 v00 = verts[u][v] v10 = verts[u1][v] v01 = verts[u][v1] v11 = verts[u1][v1] # factor coordinates fu = (sk_vert[0]-u*step)/step fv = (sk_vert[1]-v*step)/step fw = sk_vert.z if scale_mode == 'ADAPTIVE': a00 = verts_area[v00.index] a10 = verts_area[v10.index] a01 = verts_area[v01.index] a11 = verts_area[v11.index] fw*=lerp2(a00,a10,a01,a11,Vector((fu,fv,0))) fvec = Vector((fu,fv,fw)) sk_co = lerp3(v00, v10, v01, v11, fvec) new_patch.data.shape_keys.key_blocks[sk.name].data[_v.index].co = sk_co #if ob0.type == 'MESH': ob0.data = old_me0 if not bool_correct: return 0 bpy.ops.object.join() if bool_shapekeys: # set original values and combine Shape Keys and Vertex Groups for sk, val in zip(_ob1.data.shape_keys.key_blocks, original_key_values): sk.value = val new_patch.data.shape_keys.key_blocks[sk.name].value = val if bool_vertex_group: for sk in new_patch.data.shape_keys.key_blocks: for vg in new_patch.vertex_groups: if sk.name == vg.name: sk.vertex_group = vg.name new_name = ob0.name + "_" + ob1.name new_patch.name = "tessellate_temp" if bool_multires: for m in ob0.modifiers: if m.type == 'MULTIRES' and m.name == multires_name: m.levels = levels m.sculpt_levels = sculpt_levels m.render_levels = render_levels # restore original modifiers visibility for component object try: for m, vis in zip(_ob1.modifiers, mod_visibility): m.show_viewport = vis except: pass bpy.data.objects.remove(before) bpy.data.objects.remove(ob0) bpy.data.objects.remove(ob1) return new_patch def tessellate_original(_ob0, _ob1, offset, zscale, gen_modifiers, com_modifiers, mode, scale_mode, rotation_mode, rand_seed, fill_mode, bool_vertex_group, bool_selection, bool_shapekeys, bool_material_id, material_id, normals_mode, bounds_x, bounds_y): if com_modifiers or _ob1.type != 'MESH': bool_shapekeys = False random.seed(rand_seed) if bool_shapekeys: try: original_key_values = [] for sk in _ob1.data.shape_keys.key_blocks: original_key_values.append(sk.value) sk.value = 0 except: bool_shapekeys = False ob0 = convert_object_to_mesh(_ob0, gen_modifiers, True) me0 = ob0.data ob1 = convert_object_to_mesh(_ob1, com_modifiers, True) me1 = ob1.data base_polygons = [] base_face_normals = [] n_faces0 = len(me0.polygons) # Check if zero faces are selected if (bool_selection and ob0.type == 'MESH') or bool_material_id: for p in me0.polygons: if (bool_selection and ob0.type == 'MESH'): is_sel = p.select else: is_sel = True if bool_material_id: is_mat = p.material_index == material_id else: is_mat = True if is_sel and is_mat: base_polygons.append(p) base_face_normals.append(p.normal) else: base_polygons = me0.polygons base_face_normals = [p.normal for p in me0.polygons] # numpy test: slower #base_face_normals = np.zeros(n_faces0*3) #me0.polygons.foreach_get("normal", base_face_normals) #base_face_normals = base_face_normals.reshape((n_faces0,3)) if len(base_polygons) == 0: return 0 if mode != 'BOUNDS': bpy.ops.object.select_all(action='DESELECT') for o in bpy.context.view_layer.objects: o.select_set(False) bpy.context.view_layer.objects.active = ob1 ob1.select_set(True) bpy.context.object.active_shape_key_index = 0 # Bound X if bounds_x != 'EXTEND': if mode == 'GLOBAL': planes_co = ((0,0,0),(1,1,1)) plane_no = (1,0,0) if mode == 'LOCAL': planes_co = (ob1.matrix_world @ Vector((0,0,0)), ob1.matrix_world @ Vector((1,0,0))) plane_no = planes_co[0]-planes_co[1] bpy.ops.object.mode_set(mode='EDIT') for co in planes_co: bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.bisect(plane_co=co, plane_no=plane_no) bpy.ops.mesh.mark_seam() bpy.ops.object.mode_set(mode='OBJECT') _faces = ob1.data.polygons if mode == 'GLOBAL': for f in [f for f in _faces if (ob1.matrix_world @ f.center).x > 1]: f.select = True for f in [f for f in _faces if (ob1.matrix_world @ f.center).x < 0]: f.select = True else: for f in [f for f in _faces if f.center.x > 1]: f.select = True for f in [f for f in _faces if f.center.x < 0]: f.select = True bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_mode(type='FACE') if bounds_x == 'CLIP': bpy.ops.mesh.delete(type='FACE') bpy.ops.object.mode_set(mode='OBJECT') if bounds_x == 'CYCLIC': bpy.ops.mesh.split() bpy.ops.object.mode_set(mode='OBJECT') # Bound Y if bounds_y != 'EXTEND': if mode == 'GLOBAL': planes_co = ((0,0,0),(1,1,1)) plane_no = (0,1,0) if mode == 'LOCAL': planes_co = (ob1.matrix_world @ Vector((0,0,0)), ob1.matrix_world @ Vector((0,1,0))) plane_no = planes_co[0]-planes_co[1] bpy.ops.object.mode_set(mode='EDIT') for co in planes_co: bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.bisect(plane_co=co, plane_no=plane_no) bpy.ops.mesh.mark_seam() bpy.ops.object.mode_set(mode='OBJECT') _faces = ob1.data.polygons if mode == 'GLOBAL': for f in [f for f in _faces if (ob1.matrix_world @ f.center).y > 1]: f.select = True for f in [f for f in _faces if (ob1.matrix_world @ f.center).y < 0]: f.select = True else: for f in [f for f in _faces if f.center.y > 1]: f.select = True for f in [f for f in _faces if f.center.y < 0]: f.select = True bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_mode(type='FACE') if bounds_y == 'CLIP': bpy.ops.mesh.delete(type='FACE') bpy.ops.object.mode_set(mode='OBJECT') if bounds_y == 'CYCLIC': bpy.ops.mesh.split() bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.mode_set(mode='OBJECT') #ob1 = new_ob1 me1 = ob1.data verts0 = me0.vertices # Collect generator vertices # Component statistics n_verts1 = len(me1.vertices) n_edges1 = len(me1.edges) n_faces1 = len(me1.polygons) # Create empty lists new_verts = [] new_edges = [] new_faces = [] new_verts_np = np.array(()) # Component Coordinates co1 = [0]*n_verts1*3 if mode == 'GLOBAL': for v in me1.vertices: v.co = ob1.matrix_world @ v.co try: for sk in me1.shape_keys.key_blocks: sk.data[v.index].co = ob1.matrix_world @ sk.data[v.index].co except: pass if mode != 'BOUNDS': if bounds_x == 'CYCLIC': move_verts = [] for f in [f for f in me1.polygons if (f.center).x > 1]: for v in f.vertices: if v not in move_verts: move_verts.append(v) for v in move_verts: me1.vertices[v].co.x -= 1 try: _ob1.active_shape_key_index = 0 for sk in me1.shape_keys.key_blocks: sk.data[v].co.x -= 1 except: pass move_verts = [] for f in [f for f in me1.polygons if (f.center).x < 0]: for v in f.vertices: if v not in move_verts: move_verts.append(v) for v in move_verts: me1.vertices[v].co.x += 1 try: _ob1.active_shape_key_index = 0 for sk in me1.shape_keys.key_blocks: sk.data[v].co.x += 1 except: pass if bounds_y == 'CYCLIC': move_verts = [] for f in [f for f in me1.polygons if (f.center).y > 1]: for v in f.vertices: if v not in move_verts: move_verts.append(v) for v in move_verts: me1.vertices[v].co.y -= 1 try: #new_ob1.active_shape_key_index = 0 for sk in me1.shape_keys.key_blocks: sk.data[v].co.y -= 1 except: pass move_verts = [] for f in [f for f in me1.polygons if (f.center).y < 0]: for v in f.vertices: if v not in move_verts: move_verts.append(v) for v in move_verts: me1.vertices[v].co.y += 1 try: #new_ob1.active_shape_key_index = 0 for sk in me1.shape_keys.key_blocks: sk.data[v].co.y += 1 except: pass me1.vertices.foreach_get("co", co1) co1 = np.array(co1) vx = co1[0::3].reshape((n_verts1,1)) vy = co1[1::3].reshape((n_verts1,1)) vz = co1[2::3].reshape((n_verts1,1)) min_c = Vector((vx.min(), vy.min(), vz.min())) # Min BB Corner max_c = Vector((vx.max(), vy.max(), vz.max())) # Max BB Corner bb = max_c - min_c # Bounding Box # Component Coordinates if mode == 'BOUNDS': vx = (vx - min_c[0]) / bb[0] if bb[0] != 0 else 0.5 vy = (vy - min_c[1]) / bb[1] if bb[1] != 0 else 0.5 vz = ((vz - min_c[2]) + (-0.5 + offset * 0.5) * bb[2]) * zscale else: vz *= zscale # Component polygons fs1 = [[i for i in p.vertices] for p in me1.polygons] new_faces = fs1[:] # Component edges es1 = np.array([[i for i in e.vertices] for e in me1.edges]) #es1 = [[i for i in e.vertices] for e in me1.edges if e.is_loose] new_edges = es1[:] # SHAPE KEYS if bool_shapekeys: basis = True #com_modifiers vx_key = [] vy_key = [] vz_key = [] sk_np = [] for sk in ob1.data.shape_keys.key_blocks: do_shapekeys = True # set all keys to 0 for _sk in ob1.data.shape_keys.key_blocks: _sk.value = 0 sk.value = 1 if basis: basis = False continue # Apply component modifiers if com_modifiers: sk_ob = convert_object_to_mesh(_ob1) sk_data = sk_ob.data source = sk_data.vertices else: source = sk.data shapekeys = [] for v in source: if mode == 'BOUNDS': vert = v.co - min_c vert[0] = vert[0] / bb[0] vert[1] = vert[1] / bb[1] vert[2] = (vert[2] + (-0.5 + offset * 0.5) * bb[2]) * zscale elif mode == 'LOCAL': vert = v.co.xyz vert[2] *= zscale #vert[2] = (vert[2] - min_c[2] + (-0.5 + offset * 0.5) * bb[2]) * \ # zscale elif mode == 'GLOBAL': vert = v.co.xyz #vert = ob1.matrix_world @ v.co vert[2] *= zscale shapekeys.append(vert) # Component vertices key1 = np.array([v for v in shapekeys]).reshape(len(shapekeys), 3, 1) vx_key.append(key1[:, 0]) vy_key.append(key1[:, 1]) vz_key.append(key1[:, 2]) #sk_np.append([]) # All vertex group if bool_vertex_group: try: weight = [] vertex_groups = ob0.vertex_groups for vg in vertex_groups: _weight = [] for v in me0.vertices: try: _weight.append(vg.weight(v.index)) except: _weight.append(0) weight.append(_weight) except: bool_vertex_group = False # Adaptive Z if scale_mode == 'ADAPTIVE': if mode == 'BOUNDS': com_area = (bb[0]*bb[1]) else: com_area = 1 if com_area == 0: mult = 1 else: mult = 1/com_area verts_area = [] bm = bmesh.new() bm.from_mesh(me0) bm.verts.ensure_lookup_table() for v in bm.verts: area = 0 faces = v.link_faces for f in faces: area += f.calc_area() try: area/=len(faces) area*=mult verts_area.append(sqrt(area)) except: verts_area.append(1) # FAN tessellation mode if fill_mode == 'FAN': fan_verts = [v.co.to_tuple() for v in me0.vertices] fan_polygons = [] fan_select = [] fan_material = [] fan_normals = [] # selected_faces = [] for p in base_polygons: fan_center = Vector((0, 0, 0)) center_area = 0 for v in p.vertices: fan_center += me0.vertices[v].co if scale_mode == 'ADAPTIVE': center_area += verts_area[v] fan_center /= len(p.vertices) center_area /= len(p.vertices) last_vert = len(fan_verts) fan_verts.append(fan_center.to_tuple()) #fan_verts.append(fan_center) if scale_mode == 'ADAPTIVE': verts_area.append(center_area) # Vertex Group if bool_vertex_group: for w in weight: center_weight = sum([w[i] for i in p.vertices]) / len(p.vertices) w.append(center_weight) for i in range(len(p.vertices)): fan_polygons.append((p.vertices[i], p.vertices[(i + 1) % len(p.vertices)], last_vert, last_vert)) if bool_material_id: fan_material.append(p.material_index) if bool_selection: fan_select.append(p.select) if normals_mode == 'FACES': fan_normals.append(p.normal) fan_me = bpy.data.meshes.new('Fan.Mesh') fan_me.from_pydata(tuple(fan_verts), [], tuple(fan_polygons)) me0 = fan_me.copy() bpy.data.meshes.remove(fan_me) verts0 = me0.vertices base_polygons = me0.polygons if normals_mode == 'FACES': base_face_normals = fan_normals count = 0 # necessary for UV calculation # TESSELLATION j = 0 jj = -1 bool_correct = False # optimization test n_faces = len(base_polygons) _vs0 = [0]*n_faces _nvs0 = [0]*n_faces _sz = [0]*n_faces _w0 = [[0]*n_faces]*len(ob0.vertex_groups) np_faces = [np.array(p) for p in fs1] new_faces = [0]*n_faces*n_faces1 face1_count = 0 for p in base_polygons: bool_correct = True if rotation_mode == 'UV' and ob0.type != 'MESH': rotation_mode = 'DEFAULT' # Random rotation if rotation_mode == 'RANDOM': shifted_vertices = [] n_poly_verts = len(p.vertices) rand = random.randint(0, n_poly_verts) for i in range(n_poly_verts): shifted_vertices.append(p.vertices[(i + rand) % n_poly_verts]) if scale_mode == 'ADAPTIVE': verts_area0 = np.array([verts_area[i] for i in shifted_vertices]) vs0 = np.array([verts0[i].co for i in shifted_vertices]) nvs0 = np.array([verts0[i].normal for i in shifted_vertices]) if normals_mode == 'VERTS': nvs0 = np.array([verts0[i].normal for i in shifted_vertices]) # vertex weight if bool_vertex_group: ws0 = [] for w in weight: _ws0 = [] for i in shifted_vertices: try: _ws0.append(w[i]) except: _ws0.append(0) ws0.append(np.array(_ws0)) # UV rotation elif rotation_mode == 'UV': if len(ob0.data.uv_layers) > 0 and fill_mode != 'FAN': i = p.index if bool_material_id: count = sum([len(p.vertices) for p in me0.polygons[:i]]) #if i == 0: count = 0 v01 = (me0.uv_layers.active.data[count].uv + me0.uv_layers.active.data[count + 1].uv) if len(p.vertices) > 3: v32 = (me0.uv_layers.active.data[count + 3].uv + me0.uv_layers.active.data[count + 2].uv) else: v32 = (me0.uv_layers.active.data[count].uv + me0.uv_layers.active.data[count + 2].uv) v0132 = v32 - v01 v0132.normalize() v12 = (me0.uv_layers.active.data[count + 1].uv + me0.uv_layers.active.data[count + 2].uv) if len(p.vertices) > 3: v03 = (me0.uv_layers.active.data[count].uv + me0.uv_layers.active.data[count + 3].uv) else: v03 = (me0.uv_layers.active.data[count].uv + me0.uv_layers.active.data[count].uv) v1203 = v03 - v12 v1203.normalize() vertUV = [] dot1203 = v1203.x dot0132 = v0132.x if(abs(dot1203) < abs(dot0132)): if (dot0132 > 0): vertUV = p.vertices[1:] + p.vertices[:1] else: vertUV = p.vertices[3:] + p.vertices[:3] else: if(dot1203 < 0): vertUV = p.vertices[:] else: vertUV = p.vertices[2:] + p.vertices[:2] vs0 = np.array([verts0[i].co for i in vertUV]) nvs0 = np.array([verts0[i].normal for i in vertUV]) # Vertex weight if bool_vertex_group: ws0 = [] for w in weight: _ws0 = [] for i in vertUV: try: _ws0.append(w[i]) except: _ws0.append(0) ws0.append(np.array(_ws0)) count += len(p.vertices) else: rotation_mode = 'DEFAULT' # Default rotation if rotation_mode == 'DEFAULT': vs0 = np.array([verts0[i].co for i in p.vertices]) nvs0 = np.array([verts0[i].normal for i in p.vertices]) # Vertex weight if bool_vertex_group: ws0 = [] for w in weight: _ws0 = [] for i in p.vertices: try: _ws0.append(w[i]) except: _ws0.append(0) ws0.append(np.array(_ws0)) # optimization test _vs0[j] = (vs0[0], vs0[1], vs0[2], vs0[-1]) if normals_mode == 'VERTS': _nvs0[j] = (nvs0[0], nvs0[1], nvs0[2], nvs0[-1]) #else: # _nvs0[j] = base_face_normals[j] # vertex z to normal if scale_mode == 'ADAPTIVE': poly_faces = (p.vertices[0], p.vertices[1], p.vertices[2], p.vertices[-1]) if rotation_mode == 'RANDOM': sz = verts_area0 else: sz = np.array([verts_area[i] for i in poly_faces]) _sz[j] = sz if bool_vertex_group: vg_count = 0 for _ws0 in ws0: _w0[vg_count][j] = (_ws0[0], _ws0[1], _ws0[2], _ws0[-1]) vg_count += 1 for p in fs1: new_faces[face1_count] = [i + n_verts1 * j for i in p] face1_count += 1 j += 1 # build edges list n_edges1 = new_edges.shape[0] new_edges = new_edges.reshape((1, n_edges1, 2)) new_edges = new_edges.repeat(n_faces,axis=0) new_edges = new_edges.reshape((n_edges1*n_faces, 2)) increment = np.arange(n_faces)*n_verts1 increment = increment.repeat(n_edges1, axis=0) increment = increment.reshape((n_faces*n_edges1,1)) new_edges = new_edges + increment # optimization test _vs0 = np.array(_vs0) _sz = np.array(_sz) _vs0_0 = _vs0[:,0].reshape((n_faces,1,3)) _vs0_1 = _vs0[:,1].reshape((n_faces,1,3)) _vs0_2 = _vs0[:,2].reshape((n_faces,1,3)) _vs0_3 = _vs0[:,3].reshape((n_faces,1,3)) # remapped vertex coordinates v0 = _vs0_0 + (_vs0_1 - _vs0_0) * vx v1 = _vs0_3 + (_vs0_2 - _vs0_3) * vx v2 = v0 + (v1 - v0) * vy # remapped vertex normal if normals_mode == 'VERTS': _nvs0 = np.array(_nvs0) _nvs0_0 = _nvs0[:,0].reshape((n_faces,1,3)) _nvs0_1 = _nvs0[:,1].reshape((n_faces,1,3)) _nvs0_2 = _nvs0[:,2].reshape((n_faces,1,3)) _nvs0_3 = _nvs0[:,3].reshape((n_faces,1,3)) nv0 = _nvs0_0 + (_nvs0_1 - _nvs0_0) * vx nv1 = _nvs0_3 + (_nvs0_2 - _nvs0_3) * vx nv2 = nv0 + (nv1 - nv0) * vy else: nv2 = np.array(base_face_normals).reshape((n_faces,1,3)) if bool_vertex_group: n_vg = len(_w0) w = np.array(_w0) #for w in _w0: #w = np.array(w) w_0 = w[:,:,0].reshape((n_vg, n_faces,1,1)) w_1 = w[:,:,1].reshape((n_vg, n_faces,1,1)) w_2 = w[:,:,2].reshape((n_vg, n_faces,1,1)) w_3 = w[:,:,3].reshape((n_vg, n_faces,1,1)) # remapped weight w0 = w_0 + (w_1 - w_0) * vx w1 = w_3 + (w_2 - w_3) * vx w = w0 + (w1 - w0) * vy w = w.reshape((n_vg, n_faces*n_verts1)) #w = w2.tolist() if scale_mode == 'ADAPTIVE': _sz_0 = _sz[:,0].reshape((n_faces,1,1)) _sz_1 = _sz[:,1].reshape((n_faces,1,1)) _sz_2 = _sz[:,2].reshape((n_faces,1,1)) _sz_3 = _sz[:,3].reshape((n_faces,1,1)) # remapped z scale sz0 = _sz_0 + (_sz_1 - _sz_0) * vx sz1 = _sz_3 + (_sz_2 - _sz_3) * vx sz2 = sz0 + (sz1 - sz0) * vy v3 = v2 + nv2 * vz * sz2 else: v3 = v2 + nv2 * vz new_verts_np = v3.reshape((n_faces*n_verts1,3)) if bool_shapekeys: n_sk = len(vx_key) sk_np = [0]*n_sk for i in range(n_sk): vx = np.array(vx_key) vy = np.array(vy_key) vz = np.array(vz_key) # remapped vertex coordinates v0 = _vs0_0 + (_vs0_1 - _vs0_0) * vx v1 = _vs0_3 + (_vs0_2 - _vs0_3) * vx v2 = v0 + (v1 - v0) * vy # remapped vertex normal if normals_mode == 'VERTS': nv0 = _nvs0_0 + (_nvs0_1 - _nvs0_0) * vx nv1 = _nvs0_3 + (_nvs0_2 - _nvs0_3) * vx nv2 = nv0 + (nv1 - nv0) * vy else: nv2 = np.array(base_face_normals).reshape((n_faces,1,3)) if scale_mode == 'ADAPTIVE': # remapped z scale sz0 = _sz_0 + (_sz_1 - _sz_0) * vx sz1 = _sz_3 + (_sz_2 - _sz_3) * vx sz2 = sz0 + (sz1 - sz0) * vy v3 = v2 + nv2 * vz * sz2 else: v3 = v2 + nv2 * vz sk_np[i] = v3.reshape((n_faces*n_verts1,3)) #if ob0.type == 'MESH': ob0.data = old_me0 if not bool_correct: return 0 new_verts = new_verts_np.tolist() new_name = ob0.name + "_" + ob1.name new_me = bpy.data.meshes.new(new_name) new_me.from_pydata(new_verts, new_edges.tolist(), new_faces) new_me.update(calc_edges=True) new_ob = bpy.data.objects.new("tessellate_temp", new_me) # vertex group if bool_vertex_group and False: for vg in ob0.vertex_groups: new_ob.vertex_groups.new(name=vg.name) for i in range(len(vg_np[vg.index])): new_ob.vertex_groups[vg.name].add([i], vg_np[vg.index][i],"ADD") # vertex group if bool_vertex_group: for vg in ob0.vertex_groups: new_ob.vertex_groups.new(name=vg.name) for i in range(len(w[vg.index])): new_ob.vertex_groups[vg.name].add([i], w[vg.index,i],"ADD") if bool_shapekeys: basis = com_modifiers sk_count = 0 for sk, val in zip(_ob1.data.shape_keys.key_blocks, original_key_values): sk.value = val new_ob.shape_key_add(name=sk.name) new_ob.data.shape_keys.key_blocks[sk.name].value = val # set shape keys vertices sk_data = new_ob.data.shape_keys.key_blocks[sk.name].data if sk_count == 0: sk_count += 1 continue for id in range(len(sk_data)): sk_data[id].co = sk_np[sk_count-1][id] sk_count += 1 if bool_vertex_group: for sk in new_ob.data.shape_keys.key_blocks: for vg in new_ob.vertex_groups: if sk.name == vg.name: sk.vertex_group = vg.name # EDGES SEAMS edge_data = [0]*n_edges1 me1.edges.foreach_get("use_seam",edge_data) if any(edge_data): edge_data = edge_data*n_faces new_ob.data.edges.foreach_set("use_seam",edge_data) # EDGES SHARP edge_data = [0]*n_edges1 me1.edges.foreach_get("use_edge_sharp",edge_data) if any(edge_data): edge_data = edge_data*n_faces new_ob.data.edges.foreach_set("use_edge_sharp",edge_data) bpy.ops.object.select_all(action='DESELECT') bpy.context.collection.objects.link(new_ob) new_ob.select_set(True) bpy.context.view_layer.objects.active = new_ob # EDGES BEVEL edge_data = [0]*n_edges1 me1.edges.foreach_get("bevel_weight",edge_data) if any(edge_data): bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.transform.edge_bevelweight(value=1) bpy.ops.object.mode_set(mode='OBJECT') edge_data = edge_data*n_faces new_ob.data.edges.foreach_set("bevel_weight",edge_data) # EDGE CREASES edge_data = [0]*n_edges1 me1.edges.foreach_get("crease",edge_data) if any(edge_data): bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.transform.edge_crease(value=1) bpy.ops.object.mode_set(mode='OBJECT') edge_data = edge_data*n_faces new_ob.data.edges.foreach_set('crease', edge_data) # MATERIALS for slot in ob1.material_slots: new_ob.data.materials.append(slot.material) polygon_materials = [0]*n_faces1 me1.polygons.foreach_get("material_index", polygon_materials) polygon_materials *= n_faces new_ob.data.polygons.foreach_set("material_index", polygon_materials) new_ob.data.update() ### try: bpy.data.objects.remove(new_ob1) except: pass bpy.data.objects.remove(ob0) bpy.data.meshes.remove(me0) bpy.data.objects.remove(ob1) bpy.data.meshes.remove(me1) return new_ob class tessellate(Operator): bl_idname = "object.tessellate" bl_label = "Tessellate" bl_description = ("Create a copy of selected object on the active object's " "faces, adapting the shape to the different faces") bl_options = {'REGISTER', 'UNDO'} object_name : StringProperty( name="", description="Name of the generated object" ) zscale : FloatProperty( name="Scale", default=1, soft_min=0, soft_max=10, description="Scale factor for the component thickness" ) scale_mode : EnumProperty( items=( ('CONSTANT', "Constant", "Uniform thickness"), ('ADAPTIVE', "Proportional", "Preserve component's proportions") ), default='ADAPTIVE', name="Z-Scale according to faces size" ) offset : FloatProperty( name="Surface Offset", default=1, min=-1, max=1, soft_min=-1, soft_max=1, description="Surface offset" ) mode : EnumProperty( items=( ('BOUNDS', "Bounds", "The component fits automatically the size of the target face"), ('LOCAL', "Local", "Based on Local coordinates, from 0 to 1"), ('GLOBAL', 'Global', "Based on Global coordinates, from 0 to 1")), default='BOUNDS', name="Component Mode" ) rotation_mode : EnumProperty( items=(('RANDOM', "Random", "Random faces rotation"), ('UV', "Active UV", "Face rotation is based on UV coordinates"), ('DEFAULT', "Default", "Default rotation")), default='DEFAULT', name="Component Rotation" ) fill_mode : EnumProperty( items=( ('QUAD', 'Quad', 'Regular quad tessellation. Uses only 3 or 4 vertices'), ('FAN', 'Fan', 'Radial tessellation for polygonal faces'), ('PATCH', 'Patch', 'Curved tessellation according to the last ' + 'Subsurf\n(or Multires) modifiers. Works only with 4 sides ' + 'patches.\nAfter the last Subsurf (or Multires) only ' + 'deformation\nmodifiers can be used')), default='QUAD', name="Fill Mode" ) combine_mode : EnumProperty( items=( ('LAST', 'Last', 'Show only the last iteration'), ('UNUSED', 'Unused', 'Combine each iteration with the unused faces of the previous iteration. Used for branching systems'), ('ALL', 'All', 'Combine the result of all iterations')), default='LAST', name="Combine Mode", ) gen_modifiers : BoolProperty( name="Generator Modifiers", default=False, description="Apply Modifiers and Shape Keys to the base object" ) com_modifiers : BoolProperty( name="Component Modifiers", default=False, description="Apply Modifiers and Shape Keys to the component object" ) merge : BoolProperty( name="Merge", default=False, description="Merge vertices in adjacent duplicates" ) merge_thres : FloatProperty( name="Distance", default=0.001, soft_min=0, soft_max=10, description="Limit below which to merge vertices" ) bool_random : BoolProperty( name="Randomize", default=False, description="Randomize component rotation" ) random_seed : IntProperty( name="Seed", default=0, soft_min=0, soft_max=10, description="Random seed" ) bool_vertex_group : BoolProperty( name="Map Vertex Groups", default=False, description="Transfer all Vertex Groups from Base object" ) bool_selection : BoolProperty( name="On selected Faces", default=False, description="Create Tessellation only on selected faces" ) bool_shapekeys : BoolProperty( name="Use Shape Keys", default=False, description="Transfer Component's Shape Keys. If the name of Vertex " "Groups and Shape Keys are the same, they will be " "automatically combined" ) bool_smooth : BoolProperty( name="Smooth Shading", default=False, description="Output faces with smooth shading rather than flat shaded" ) bool_materials : BoolProperty( name="Transfer Materials", default=True, description="Preserve component's materials" ) generator : StringProperty( name="", description="Base object for the tessellation", default = "" ) component : StringProperty( name="", description="Component object for the tessellation", default = "" ) bool_material_id : BoolProperty( name="Tessellation on Material ID", default=False, description="Apply the component only on the selected Material" ) bool_dissolve_seams : BoolProperty( name="Dissolve Seams", default=False, description="Dissolve all seam edges" ) material_id : IntProperty( name="Material ID", default=0, min=0, description="Material ID" ) iterations : IntProperty( name="Iterations", default=1, min=1, soft_max=5, description="Automatically repeat the Tessellation using the " + "generated geometry as new base object.\nUseful for " + "for branching systems. Dangerous!" ) bool_combine : BoolProperty( name="Combine unused", default=False, description="Combine the generated geometry with unused faces" ) bool_advanced : BoolProperty( name="Advanced Settings", default=False, description="Show more settings" ) normals_mode : EnumProperty( items=( ('VERTS', 'Along Normals', 'Consistent direction based on vertices normal'), ('FACES', 'Individual Faces', 'Based on individual faces normal')), default='VERTS', name="Direction" ) bool_multi_components : BoolProperty( name="Multi Components", default=False, description="Combine different components according to materials name" ) bounds_x : EnumProperty( items=( ('EXTEND', 'Extend', 'Default X coordinates'), ('CLIP', 'Clip', 'Trim out of bounds in X direction'), ('CYCLIC', 'Cyclic', 'Cyclic components in X direction')), default='EXTEND', name="Bounds X", ) bounds_y : EnumProperty( items=( ('EXTEND', 'Extend', 'Default Y coordinates'), ('CLIP', 'Clip', 'Trim out of bounds in Y direction'), ('CYCLIC', 'Cyclic', 'Cyclic components in Y direction')), default='EXTEND', name="Bounds Y", ) cap_faces : BoolProperty( name="Cap Holes", default=False, description="Cap open edges loops" ) open_edges_crease : FloatProperty( name="Open Edges Crease", default=0, min=0, max=1, description="Automatically set crease for open edges" ) # NOTE: this was made into an annotation for 2.8x, but should be a class variable. # working_on = "" def draw(self, context): allowed_obj = ('MESH', 'CURVE', 'SURFACE', 'FONT', 'META') ''' try: bool_working = self.working_on == self.object_name and \ self.working_on != "" except: bool_working = False ''' bool_working = False bool_allowed = False ob0 = None ob1 = None sel = bpy.context.selected_objects if len(sel) == 1: try: ob0 = sel[0].tissue_tessellate.generator ob1 = sel[0].tissue_tessellate.component self.generator = ob0.name self.component = ob1.name bool_working = True bool_allowed = True except: pass if len(sel) == 2: bool_allowed = True for o in sel: if o.type not in allowed_obj: bool_allowed = False if len(sel) != 2 and not bool_working: layout = self.layout layout.label(icon='INFO') layout.label(text="Please, select two different objects") layout.label(text="Select first the Component object, then select") layout.label(text="the Base object.") elif not bool_allowed and not bool_working: layout = self.layout layout.label(icon='INFO') layout.label(text="Only Mesh, Curve, Surface or Text objects are allowed") else: if ob0 == ob1 == None: ob0 = bpy.context.active_object self.generator = ob0.name for o in sel: if o != ob0: ob1 = o self.component = o.name self.no_component = False break # new object name if self.object_name == "": if self.generator == "": self.object_name = "Tessellation" else: #self.object_name = self.generator + "_Tessellation" self.object_name = "Tessellation" layout = self.layout # Base and Component col = layout.column(align=True) row = col.row(align=True) row.label(text="BASE : " + self.generator) row.label(text="COMPONENT : " + self.component) # Base Modifiers row = col.row(align=True) col2 = row.column(align=True) col2.prop(self, "gen_modifiers", text="Use Modifiers", icon='MODIFIER') base = bpy.data.objects[self.generator] try: if not (base.modifiers or base.data.shape_keys): col2.enabled = False self.gen_modifiers = False except: col2.enabled = False self.gen_modifiers = False # Component Modifiers row.separator() col3 = row.column(align=True) col3.prop(self, "com_modifiers", text="Use Modifiers", icon='MODIFIER') component = bpy.data.objects[self.component] try: if not (component.modifiers or component.data.shape_keys): col3.enabled = False self.com_modifiers = False except: col3.enabled = False self.com_modifiers = False col.separator() # Fill and Rotation row = col.row(align=True) row.label(text="Fill Mode:") row.label(text="Rotation:") row = col.row(align=True) #col2 = row.column(align=True) row.prop( self, "fill_mode", text="", icon='NONE', expand=False, slider=True, toggle=False, icon_only=False, event=False, full_event=False, emboss=True, index=-1) # Rotation row.separator() col2 = row.column(align=True) col2.prop( self, "rotation_mode", text="", icon='NONE', expand=False, slider=True, toggle=False, icon_only=False, event=False, full_event=False, emboss=True, index=-1) if self.rotation_mode == 'RANDOM': col2.prop(self, "random_seed") if self.rotation_mode == 'UV': uv_error = False if self.fill_mode == 'FAN': row = col.row(align=True) row.label(text="UV rotation doesn't work in FAN mode", icon='ERROR') uv_error = True if ob0.type != 'MESH': row = col.row(align=True) row.label( text="UV rotation supported only for Mesh objects", icon='ERROR') uv_error = True else: if len(ob0.data.uv_layers) == 0: row = col.row(align=True) check_name = self.generator row.label(text="'" + check_name + "' doesn't have UV Maps", icon='ERROR') uv_error = True if uv_error: row = col.row(align=True) row.label(text="Default rotation will be used instead", icon='INFO') # Component XY row = col.row(align=True) row.label(text="Component Coordinates:") row = col.row(align=True) row.prop( self, "mode", text="Component XY", icon='NONE', expand=True, slider=False, toggle=False, icon_only=False, event=False, full_event=False, emboss=True, index=-1) if self.mode != 'BOUNDS': col.separator() row = col.row(align=True) row.label(text="X:") row.prop( self, "bounds_x", text="Bounds X", icon='NONE', expand=True, slider=False, toggle=False, icon_only=False, event=False, full_event=False, emboss=True, index=-1) row = col.row(align=True) row.label(text="Y:") row.prop( self, "bounds_y", text="Bounds X", icon='NONE', expand=True, slider=False, toggle=False, icon_only=False, event=False, full_event=False, emboss=True, index=-1) # Component Z col.label(text="Thickness:") row = col.row(align=True) row.prop( self, "scale_mode", text="Scale Mode", icon='NONE', expand=True, slider=False, toggle=False, icon_only=False, event=False, full_event=False, emboss=True, index=-1) col.prop( self, "zscale", text="Scale", icon='NONE', expand=False, slider=True, toggle=False, icon_only=False, event=False, full_event=False, emboss=True, index=-1) if self.mode == 'BOUNDS': col.prop( self, "offset", text="Offset", icon='NONE', expand=False, slider=True, toggle=False, icon_only=False, event=False, full_event=False, emboss=True, index=-1) # Direction row = col.row(align=True) row.label(text="Direction:") row = col.row(align=True) row.prop( self, "normals_mode", text="Direction", icon='NONE', expand=True, slider=False, toggle=False, icon_only=False, event=False, full_event=False, emboss=True, index=-1) row.enabled = self.fill_mode != 'PATCH' # Merge col = layout.column(align=True) row = col.row(align=True) row.prop(self, "merge") if self.merge: row.prop(self, "merge_thres") row = col.row(align=True) row = col.row(align=True) row.prop(self, "bool_smooth") if self.merge: col2 = row.column(align=True) col2.prop(self, "bool_dissolve_seams") #if ob1.type != 'MESH': col2.enabled = False row = col.row(align=True) row.prop(self, "cap_faces") if self.cap_faces: col2 = row.column(align=True) col2.prop(self, "open_edges_crease", text="Crease") # Advanced Settings col = layout.column(align=True) col.separator() col.separator() row = col.row(align=True) row.prop(self, "bool_advanced", icon='SETTINGS') if self.bool_advanced: allow_multi = False allow_shapekeys = not self.com_modifiers for m in ob0.data.materials: try: o = bpy.data.objects[m.name] allow_multi = True try: if o.data.shape_keys is None: continue elif len(o.data.shape_keys.key_blocks) < 2: continue else: allow_shapekeys = not self.com_modifiers except: pass except: pass # DATA # col = layout.column(align=True) col.label(text="Morphing:") # vertex group + shape keys row = col.row(align=True) col2 = row.column(align=True) col2.prop(self, "bool_vertex_group", icon='GROUP_VERTEX') #col2.prop_search(props, "vertex_group", props.generator, "vertex_groups") try: if len(ob0.vertex_groups) == 0: col2.enabled = False except: col2.enabled = False row.separator() col2 = row.column(align=True) row2 = col2.row(align=True) row2.prop(self, "bool_shapekeys", text="Use Shape Keys", icon='SHAPEKEY_DATA') row2.enabled = allow_shapekeys # LIMITED TESSELLATION col = layout.column(align=True) col.label(text="Limited Tessellation:") row = col.row(align=True) col2 = row.column(align=True) col2.prop(self, "bool_multi_components", icon='MOD_TINT') if not allow_multi: col2.enabled = False self.bool_multi_components = False col.separator() row = col.row(align=True) col2 = row.column(align=True) col2.prop(self, "bool_selection", text="On selected Faces", icon='RESTRICT_SELECT_OFF') #if self.bool_material_id or self.bool_selection or self.bool_multi_components: #col2 = row.column(align=True) # col2.prop(self, "bool_combine") row.separator() if ob0.type != 'MESH': col2.enabled = False col2 = row.column(align=True) col2.prop(self, "bool_material_id", icon='MATERIAL_DATA', text="Material ID") if self.bool_material_id and not self.bool_multi_components: #col2 = row.column(align=True) col2.prop(self, "material_id") col2.enabled = not self.bool_multi_components col.separator() row = col.row(align=True) row.label(text='Reiterate Tessellation:', icon='FILE_REFRESH') row.prop(self, 'iterations', text='Repeat', icon='SETTINGS') col.separator() row = col.row(align=True) row.label(text='Combine Iterations:') row = col.row(align=True) row.prop( self, "combine_mode", icon='NONE', expand=True, slider=False, toggle=False, icon_only=False, event=False, full_event=False, emboss=True, index=-1) def execute(self, context): allowed_obj = ('MESH', 'CURVE', 'META', 'SURFACE', 'FONT') try: ob0 = bpy.data.objects[self.generator] ob1 = bpy.data.objects[self.component] except: return {'CANCELLED'} self.object_name = "Tessellation" # Check if existing object with same name names = [o.name for o in bpy.data.objects] if self.object_name in names: count_name = 1 while True: test_name = self.object_name + '.{:03d}'.format(count_name) if not (test_name in names): self.object_name = test_name break count_name += 1 if ob1.type not in allowed_obj: message = "Component must be Mesh, Curve, Surface, Text or Meta object!" self.report({'ERROR'}, message) self.component = None if ob0.type not in allowed_obj: message = "Generator must be Mesh, Curve, Surface, Text or Meta object!" self.report({'ERROR'}, message) self.generator = "" if True:#self.component not in ("",None) and self.generator not in ("",None): if bpy.ops.object.select_all.poll(): bpy.ops.object.select_all(action='TOGGLE') bpy.ops.object.mode_set(mode='OBJECT') #data0 = ob0.to_mesh(False) #data0 = ob0.data.copy() bool_update = False if bpy.context.object == ob0: auto_layer_collection() #new_ob = bpy.data.objects.new(self.object_name, data0) new_ob = convert_object_to_mesh(ob0,False,False) new_ob.data.name = self.object_name #bpy.context.collection.objects.link(new_ob) #bpy.context.view_layer.objects.active = new_ob new_ob.name = self.object_name #new_ob.select_set(True) else: new_ob = bpy.context.object bool_update = True new_ob = store_parameters(self, new_ob) try: bpy.ops.object.update_tessellate() except RuntimeError as e: bpy.data.objects.remove(new_ob) self.report({'ERROR'}, str(e)) return {'CANCELLED'} if not bool_update: self.object_name = new_ob.name #self.working_on = self.object_name new_ob.location = ob0.location new_ob.matrix_world = ob0.matrix_world return {'FINISHED'} def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self) class update_tessellate(Operator): bl_idname = "object.update_tessellate" bl_label = "Refresh" bl_description = ("Fast update the tessellated mesh according to base and " "component changes") bl_options = {'REGISTER', 'UNDO'} go = False @classmethod def poll(cls, context): #try: try: #context.object == None: return False return context.object.tissue_tessellate.generator != None and \ context.object.tissue_tessellate.component != None except: return False @staticmethod def check_gen_comp(checking): # note pass the stored name key in here to check it out return checking in bpy.data.objects.keys() def execute(self, context): start_time = time.time() ob = bpy.context.object if not self.go: generator = ob.tissue_tessellate.generator component = ob.tissue_tessellate.component zscale = ob.tissue_tessellate.zscale scale_mode = ob.tissue_tessellate.scale_mode rotation_mode = ob.tissue_tessellate.rotation_mode offset = ob.tissue_tessellate.offset merge = ob.tissue_tessellate.merge merge_thres = ob.tissue_tessellate.merge_thres gen_modifiers = ob.tissue_tessellate.gen_modifiers com_modifiers = ob.tissue_tessellate.com_modifiers bool_random = ob.tissue_tessellate.bool_random random_seed = ob.tissue_tessellate.random_seed fill_mode = ob.tissue_tessellate.fill_mode bool_vertex_group = ob.tissue_tessellate.bool_vertex_group bool_selection = ob.tissue_tessellate.bool_selection bool_shapekeys = ob.tissue_tessellate.bool_shapekeys mode = ob.tissue_tessellate.mode bool_smooth = ob.tissue_tessellate.bool_smooth bool_materials = ob.tissue_tessellate.bool_materials bool_dissolve_seams = ob.tissue_tessellate.bool_dissolve_seams bool_material_id = ob.tissue_tessellate.bool_material_id material_id = ob.tissue_tessellate.material_id iterations = ob.tissue_tessellate.iterations bool_combine = ob.tissue_tessellate.bool_combine normals_mode = ob.tissue_tessellate.normals_mode bool_advanced = ob.tissue_tessellate.bool_advanced bool_multi_components = ob.tissue_tessellate.bool_multi_components combine_mode = ob.tissue_tessellate.combine_mode bounds_x = ob.tissue_tessellate.bounds_x bounds_y = ob.tissue_tessellate.bounds_y cap_faces = ob.tissue_tessellate.cap_faces open_edges_crease = ob.tissue_tessellate.open_edges_crease try: generator.name component.name except: self.report({'ERROR'}, "Active object must be Tessellate before Update") return {'CANCELLED'} # Solve Local View issues local_spaces = [] local_ob0 = [] local_ob1 = [] for area in bpy.context.screen.areas: for space in area.spaces: try: if ob.local_view_get(space): local_spaces.append(space) local_ob0 = ob0.local_view_get(space) ob0.local_view_set(space, True) local_ob1 = ob1.local_view_get(space) ob1.local_view_set(space, True) except: pass starting_mode = bpy.context.object.mode #if starting_mode == 'PAINT_WEIGHT': starting_mode = 'WEIGHT_PAINT' bpy.ops.object.mode_set(mode='OBJECT') ob0 = generator ob1 = component auto_layer_collection() ob0_hide = ob0.hide_get() ob0_hidev = ob0.hide_viewport ob0_hider = ob0.hide_render ob1_hide = ob1.hide_get() ob1_hidev = ob1.hide_viewport ob1_hider = ob1.hide_render ob0.hide_set(False) ob0.hide_viewport = False ob0.hide_render = False ob1.hide_set(False) ob1.hide_viewport = False ob1.hide_render = False if ob0.type == 'META': base_ob = convert_object_to_mesh(ob0, False, True) else: base_ob = ob0.copy() base_ob.data = ob0.data.copy() bpy.context.collection.objects.link(base_ob) # In Blender 2.80 cache of copied objects is lost, must be re-baked bool_update_cloth = False for m in base_ob.modifiers: if m.type == 'CLOTH': m.point_cache.frame_end = bpy.context.scene.frame_current bool_update_cloth = True if bool_update_cloth: bpy.ops.ptcache.free_bake_all() bpy.ops.ptcache.bake_all() #new_ob.location = ob.location #new_ob.matrix_world = ob.matrix_world base_ob.modifiers.update() bpy.ops.object.select_all(action='DESELECT') iter_objects = [base_ob] #base_ob = new_ob#.copy() for iter in range(iterations): same_iteration = [] matched_materials = [] if bool_multi_components: mat_iter = len(base_ob.material_slots) else: mat_iter = 1 for m_id in range(mat_iter): if bool_multi_components: try: mat = base_ob.material_slots[m_id].material ob1 = bpy.data.objects[mat.name] material_id = m_id matched_materials.append(m_id) bool_material_id = True except: continue if com_modifiers: data1 = simple_to_mesh(ob1) else: data1 = ob1.data.copy() n_edges1 = len(data1.edges) if iter != 0: gen_modifiers = True if fill_mode == 'PATCH': new_ob = tessellate_patch( base_ob, ob1, offset, zscale, com_modifiers, mode, scale_mode, rotation_mode, random_seed, bool_vertex_group, bool_selection, bool_shapekeys, bool_material_id, material_id, bounds_x, bounds_y ) else: new_ob = tessellate_original( base_ob, ob1, offset, zscale, gen_modifiers, com_modifiers, mode, scale_mode, rotation_mode, random_seed, fill_mode, bool_vertex_group, bool_selection, bool_shapekeys, bool_material_id, material_id, normals_mode, bounds_x, bounds_y ) if type(new_ob) is bpy.types.Object: bpy.context.view_layer.objects.active = new_ob else: continue n_components = int(len(new_ob.data.edges) / n_edges1) # SELECTION if bool_selection: try: # create selection list polygon_selection = [p.select for p in ob1.data.polygons] * int( len(new_ob.data.polygons) / len(ob1.data.polygons)) new_ob.data.polygons.foreach_set("select", polygon_selection) except: pass if type(new_ob) == str: break if bool_multi_components and type(new_ob) not in (int,str): same_iteration.append(new_ob) new_ob.select_set(True) bpy.context.view_layer.objects.active = new_ob if type(new_ob) == str: break #bpy.data.objects.remove(base_ob) if bool_multi_components: bpy.context.view_layer.update() bpy.context.view_layer.objects.active.select_set(True) for o in bpy.data.objects: if o in same_iteration: o.select_set(True) o.location = ob.location else: try: o.select_set(False) except: pass bpy.ops.object.join() new_ob = bpy.context.view_layer.objects.active new_ob.select_set(True) new_ob.data.update() #try: # combine object if (bool_selection or bool_material_id) and combine_mode == 'UNUSED': # remove faces from last mesh bm = bmesh.new() last_mesh = iter_objects[-1].data.copy() bm.from_mesh(last_mesh) bm.faces.ensure_lookup_table() if bool_multi_components: remove_materials = matched_materials elif bool_material_id: remove_materials = [material_id] else: remove_materials = [] if bool_selection: remove_faces = [f for f in bm.faces if f.material_index in remove_materials and f.select] else: remove_faces = [f for f in bm.faces if f.material_index in remove_materials] bmesh.ops.delete(bm, geom=remove_faces, context='FACES') bm.to_mesh(last_mesh) last_mesh.update() if len(last_mesh.vertices) > 0: iter_objects[-1].data = last_mesh.copy() iter_objects[-1].data.update() else: bpy.data.objects.remove(iter_objects[-1]) iter_objects = iter_objects[:-1] base_ob = convert_object_to_mesh(new_ob,True,True) #bpy.context.collection.objects.unlink(base_ob) if iter < iterations-1: new_ob.data = base_ob.data iter_objects.append(new_ob) new_ob.location = ob.location new_ob.matrix_world = ob.matrix_world try: bpy.data.objects.remove(bpy.data.objects['_Tessellation_Base']) except: pass base_ob.name = "_Tessellation_Base" elif combine_mode == 'ALL': base_ob = new_ob.copy() iter_objects.append(new_ob) new_ob.location = ob.location new_ob.matrix_world = ob.matrix_world else: if base_ob != new_ob: bpy.data.objects.remove(base_ob) base_ob = new_ob iter_objects = [new_ob] if new_ob == 0: #for m, vis in zip(ob.modifiers, mod_visibility): m.show_viewport = vis message = "Zero faces selected in the Base mesh!" bpy.ops.object.mode_set(mode=starting_mode) self.report({'ERROR'}, message) return {'CANCELLED'} errors = {} errors["modifiers_error"] = "Modifiers that change the topology of the mesh \n" \ "after the last Subsurf (or Multires) are not allowed." errors["topology_error"] = "Make sure that the topology of the mesh before \n" \ "the last Subsurf (or Multires) is quads only." errors["wires_error"] = "Please remove all wire edges in the base object." errors["verts_error"] = "Please remove all floating vertices in the base object" if new_ob in errors: for o in iter_objects: bpy.data.objects.remove(o) bpy.context.view_layer.objects.active = ob ob.select_set(True) message = errors[new_ob] ob.tissue_tessellate.error_message = message bpy.ops.object.mode_set(mode=starting_mode) self.report({'ERROR'}, message) return {'CANCELLED'} new_ob.location = ob.location new_ob.matrix_world = ob.matrix_world ### REPEAT if combine_mode != 'LAST' and len(iter_objects)>0: if base_ob not in iter_objects: bpy.data.objects.remove(base_ob) for o in iter_objects: o.location = ob.location o.select_set(True) bpy.ops.object.join() new_ob.data.update() # update data and preserve name if ob.type != 'MESH': loc, matr = ob.location, ob.matrix_world ob = convert_object_to_mesh(ob,False,True) ob.location, ob.matrix_world = loc, matr data_name = ob.data.name old_data = ob.data ob.data = new_ob.data bpy.data.meshes.remove(old_data) ob.data.name = data_name # copy vertex group if bool_vertex_group: for vg in new_ob.vertex_groups: if not vg.name in ob.vertex_groups.keys(): ob.vertex_groups.new(name=vg.name) new_vg = ob.vertex_groups[vg.name] for i in range(len(ob.data.vertices)): try: weight = vg.weight(i) except: weight = 0 new_vg.add([i], weight, 'REPLACE') selected_objects = [o for o in bpy.context.selected_objects] for o in selected_objects: o.select_set(False) ob.select_set(True) bpy.context.view_layer.objects.active = ob bpy.data.objects.remove(new_ob) if merge: bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_mode( use_extend=False, use_expand=False, type='VERT') bpy.ops.mesh.select_non_manifold( extend=False, use_wire=False, use_boundary=True, use_multi_face=False, use_non_contiguous=False, use_verts=False) bpy.ops.mesh.remove_doubles( threshold=merge_thres, use_unselected=False) bpy.ops.object.mode_set(mode='OBJECT') if bool_dissolve_seams: bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_mode(type='EDGE') bpy.ops.mesh.select_all(action='DESELECT') bpy.ops.object.mode_set(mode='OBJECT') for e in ob.data.edges: e.select = e.use_seam bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.dissolve_edges() if cap_faces: bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_mode( use_extend=False, use_expand=False, type='EDGE') bpy.ops.mesh.select_non_manifold( extend=False, use_wire=False, use_boundary=True, use_multi_face=False, use_non_contiguous=False, use_verts=False) bpy.ops.mesh.edge_face_add() if open_edges_crease != 0: bpy.ops.transform.edge_crease(value=open_edges_crease) bpy.ops.object.mode_set(mode='EDIT') bpy.ops.object.mode_set(mode='OBJECT') if bool_smooth: bpy.ops.object.shade_smooth() ####values = [True] * len(ob.data.polygons) ####ob.data.polygons.foreach_set("use_smooth", values) #for m, vis in zip(ob.modifiers, mod_visibility): m.show_viewport = vis end_time = time.time() print('Tissue: object "{}" tessellated in {:.4f} sec'.format(ob.name, end_time-start_time)) for mesh in bpy.data.meshes: if not mesh.users: bpy.data.meshes.remove(mesh) for o in selected_objects: try: o.select_set(True) except: pass bpy.ops.object.mode_set(mode=starting_mode) # clean objects for o in bpy.data.objects: if o.name not in context.view_layer.objects and "temp" in o.name: bpy.data.objects.remove(o) ob.tissue_tessellate.error_message = "" # Restore Base visibility ob0.hide_set(ob0_hide) ob0.hide_viewport = ob0_hidev ob0.hide_render = ob0_hider # Restore Component visibility ob1.hide_set(ob1_hide) ob1.hide_viewport = ob1_hidev ob1.hide_render = ob1_hider # Restore Local visibility for space, local0, local1 in zip(local_spaces, local_ob0, local_ob1): ob0.local_view_set(space, local0) ob1.local_view_set(space, local1) return {'FINISHED'} def check(self, context): return True class TISSUE_PT_tessellate(Panel): bl_label = "Tissue Tools" bl_category = "Edit" bl_space_type = "VIEW_3D" bl_region_type = "UI" bl_options = {'DEFAULT_CLOSED'} @classmethod def poll(cls, context): return context.mode in {'OBJECT', 'EDIT_MESH'} def draw(self, context): layout = self.layout col = layout.column(align=True) col.label(text="Tessellate:") col.operator("object.tessellate") col.operator("object.dual_mesh_tessellated") col.separator() #col = layout.column(align=True) #col.label(text="Tessellate Edit:") #col.operator("object.settings_tessellate") col.operator("object.update_tessellate", icon='FILE_REFRESH') #col = layout.column(align=True) col.operator("mesh.rotate_face", icon='NDOF_TURN') col.separator() col.label(text="Other:") col.operator("object.dual_mesh") col.operator("object.lattice_along_surface", icon="OUTLINER_OB_LATTICE") act = context.active_object if act and act.type == 'MESH': col.operator("object.uv_to_mesh", icon="UV") class TISSUE_PT_tessellate_object(Panel): bl_space_type = 'PROPERTIES' bl_region_type = 'WINDOW' bl_context = "data" bl_label = "Tissue - Tessellate" bl_options = {'DEFAULT_CLOSED'} @classmethod def poll(cls, context): try: return context.object.type == 'MESH' except: return False def draw(self, context): ob = context.object props = ob.tissue_tessellate allowed_obj = ('MESH','CURVE','SURFACE','FONT', 'META') try: bool_tessellated = props.generator or props.component != None ob0 = props.generator ob1 = props.component except: bool_tessellated = False layout = self.layout if not bool_tessellated: layout.label(text="The selected object is not a Tessellated object", icon='INFO') else: if props.error_message != "": layout.label(text=props.error_message, icon='ERROR') col = layout.column(align=True) row = col.row(align=True) set_tessellate_handler(self,context) set_animatable_fix_handler(self,context) row.prop(props, "bool_run", text="Animatable") row.operator("object.update_tessellate", icon='FILE_REFRESH') col = layout.column(align=True) row = col.row(align=True) row.label(text="BASE :") row.label(text="COMPONENT :") row = col.row(align=True) col2 = row.column(align=True) col2.prop_search(props, "generator", context.scene, "objects") row.separator() col2 = row.column(align=True) col2.prop_search(props, "component", context.scene, "objects") row = col.row(align=True) col2 = row.column(align=True) col2.prop(props, "gen_modifiers", text="Use Modifiers", icon='MODIFIER') row.separator() try: if not (ob0.modifiers or ob0.data.shape_keys) or props.fill_mode == 'PATCH': col2.enabled = False except: col2.enabled = False col2 = row.column(align=True) col2.prop(props, "com_modifiers", text="Use Modifiers", icon='MODIFIER') try: if not (props.component.modifiers or props.component.data.shape_keys): col2.enabled = False except: col2.enabled = False col.separator() # Fill and Rotation row = col.row(align=True) row.label(text="Fill Mode:") row.separator() row.label(text="Rotation:") row = col.row(align=True) # fill row.prop(props, "fill_mode", text="", icon='NONE', expand=False, slider=True, toggle=False, icon_only=False, event=False, full_event=False, emboss=True, index=-1) row.separator() # rotation col2 = row.column(align=True) col2.prop(props, "rotation_mode", text="", icon='NONE', expand=False, slider=True, toggle=False, icon_only=False, event=False, full_event=False, emboss=True, index=-1) if props.rotation_mode == 'RANDOM': #row = col.row(align=True) col2.prop(props, "random_seed") if props.rotation_mode == 'UV': uv_error = False if props.fill_mode == 'FAN': row = col.row(align=True) row.label(text="UV rotation doesn't work in FAN mode", icon='ERROR') uv_error = True if props.generator.type != 'MESH': row = col.row(align=True) row.label( text="UV rotation supported only for Mesh objects", icon='ERROR') uv_error = True else: if len(props.generator.data.uv_layers) == 0: row = col.row(align=True) row.label(text="'" + props.generator.name + " doesn't have UV Maps", icon='ERROR') uv_error = True if uv_error: row = col.row(align=True) row.label(text="Default rotation will be used instead", icon='INFO') # component XY row = col.row(align=True) row.label(text="Component Coordinates:") row = col.row(align=True) row.prop(props, "mode", expand=True) if props.mode != 'BOUNDS': col.separator() row = col.row(align=True) row.label(text="X:") row.prop( props, "bounds_x", text="Bounds X", icon='NONE', expand=True, slider=False, toggle=False, icon_only=False, event=False, full_event=False, emboss=True, index=-1) row = col.row(align=True) row.label(text="Y:") row.prop( props, "bounds_y", text="Bounds X", icon='NONE', expand=True, slider=False, toggle=False, icon_only=False, event=False, full_event=False, emboss=True, index=-1) # component Z col.label(text="Thickness:") row = col.row(align=True) row.prop(props, "scale_mode", expand=True) col.prop(props, "zscale", text="Scale", icon='NONE', expand=False, slider=True, toggle=False, icon_only=False, event=False, full_event=False, emboss=True, index=-1) if props.mode == 'BOUNDS': col.prop(props, "offset", text="Offset", icon='NONE', expand=False, slider=True, toggle=False, icon_only=False, event=False, full_event=False, emboss=True, index=-1) # Direction row = col.row(align=True) row.label(text="Direction:") row = col.row(align=True) row.prop( props, "normals_mode", text="Direction", icon='NONE', expand=True, slider=False, toggle=False, icon_only=False, event=False, full_event=False, emboss=True, index=-1) row.enabled = props.fill_mode != 'PATCH' # merge col = layout.column(align=True) row = col.row(align=True) row.prop(props, "merge") if props.merge: row.prop(props, "merge_thres") row = col.row(align=True) row.prop(props, "bool_smooth") if props.merge: col2 = row.column(align=True) col2.prop(props, "bool_dissolve_seams") #if props.component.type != 'MESH': col2.enabled = False row = col.row(align=True) row.prop(props, "cap_faces") if props.cap_faces: col2 = row.column(align=True) col2.prop(props, "open_edges_crease", text="Crease") # Advanced Settings col = layout.column(align=True) col.separator() col.separator() row = col.row(align=True) row.prop(props, "bool_advanced", icon='SETTINGS') if props.bool_advanced: allow_multi = False allow_shapekeys = not props.com_modifiers for m in ob0.data.materials: try: o = bpy.data.objects[m.name] allow_multi = True try: if o.data.shape_keys is None: continue elif len(o.data.shape_keys.key_blocks) < 2: continue else: allow_shapekeys = not props.com_modifiers except: pass except: pass # DATA # col = layout.column(align=True) col.label(text="Morphing:") row = col.row(align=True) col2 = row.column(align=True) col2.prop(props, "bool_vertex_group", icon='GROUP_VERTEX') #col2.prop_search(props, "vertex_group", props.generator, "vertex_groups") try: if len(props.generator.vertex_groups) == 0: col2.enabled = False except: col2.enabled = False row.separator() col2 = row.column(align=True) row2 = col2.row(align=True) row2.prop(props, "bool_shapekeys", text="Use Shape Keys", icon='SHAPEKEY_DATA') row2.enabled = allow_shapekeys # LIMITED TESSELLATION col = layout.column(align=True) col.label(text="Limited Tessellation:") row = col.row(align=True) col2 = row.column(align=True) col2.prop(props, "bool_multi_components", icon='MOD_TINT') if not allow_multi: col2.enabled = False col.separator() row = col.row(align=True) col2 = row.column(align=True) col2.prop(props, "bool_selection", text="On selected Faces", icon='RESTRICT_SELECT_OFF') #if props.bool_material_id or props.bool_selection or props.bool_multi_components: #col2 = row.column(align=True) # col2.prop(props, "bool_combine") row.separator() if props.generator.type != 'MESH': col2.enabled = False col2 = row.column(align=True) col2.prop(props, "bool_material_id", icon='MATERIAL_DATA', text="Material ID") if props.bool_material_id and not props.bool_multi_components: #col2 = row.column(align=True) col2.prop(props, "material_id") if props.bool_multi_components: col2.enabled = False # TRANSFER DATA ### OFF if props.fill_mode != 'PATCH' and False: col = layout.column(align=True) col.label(text="Component Data:") row = col.row(align=True) col2 = row.column(align=True) col2.prop(props, "bool_materials", icon='MATERIAL_DATA') row.separator() col2 = row.column(align=True) if props.fill_mode == 'PATCH': col.enabled = False col.label(text='Not needed in Patch mode', icon='INFO') col.separator() row = col.row(align=True) row.label(text='Reiterate Tessellation:', icon='FILE_REFRESH') row.prop(props, 'iterations', text='Repeat', icon='SETTINGS') col.separator() row = col.row(align=True) row.label(text='Combine Iterations:') row = col.row(align=True) row.prop( props, "combine_mode", text="Combine:",icon='NONE', expand=True, slider=False, toggle=False, icon_only=False, event=False, full_event=False, emboss=True, index=-1) class rotate_face(Operator): bl_idname = "mesh.rotate_face" bl_label = "Rotate Faces" bl_description = "Rotate selected faces and update tessellated meshes" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return context.mode == 'EDIT_MESH' def execute(self, context): ob = bpy.context.active_object me = ob.data bm = bmesh.from_edit_mesh(me) mesh_select_mode = [sm for sm in context.tool_settings.mesh_select_mode] for face in bm.faces: if (face.select): vs = face.verts[:] vs2 = vs[1:]+vs[:1] material_index = face.material_index bm.faces.remove(face) f2 = bm.faces.new(vs2) f2.select = True f2.material_index = material_index bm.normal_update() # trigger UI update bmesh.update_edit_mesh(me) ob.select_set(False) # update tessellated meshes bpy.ops.object.mode_set(mode='OBJECT') for o in [obj for obj in bpy.data.objects if obj.tissue_tessellate.generator == ob and obj.visible_get()]: bpy.context.view_layer.objects.active = o bpy.ops.object.update_tessellate() o.select_set(False) ob.select_set(True) bpy.context.view_layer.objects.active = ob bpy.ops.object.mode_set(mode='EDIT') context.tool_settings.mesh_select_mode = mesh_select_mode return {'FINISHED'}