# SPDX-License-Identifier: GPL-2.0-or-later import bpy, bmesh import threading import numpy as np import multiprocessing from multiprocessing import Process, Pool from mathutils import Vector, Matrix from math import * try: from .numba_functions import * except: pass from . import config def use_numba_tess(): tissue_addon = bpy.context.preferences.addons[__package__] if 'use_numba_tess' in tissue_addon.preferences.keys(): return tissue_addon.preferences['use_numba_tess'] else: return True def tissue_time(start_time, name, levels=0): tissue_addon = bpy.context.preferences.addons[__package__] end_time = time.time() if 'print_stats' in tissue_addon.preferences.keys(): ps = tissue_addon.preferences['print_stats'] else: ps = 1 if levels < ps: if "Tissue: " in name: head = "" else: head = " " if start_time: print('{}{}{} in {:.4f} sec'.format(head, "| "*levels, name, end_time - start_time)) else: print('{}{}{}'.format(head, "| "*levels, name)) return end_time # ------------------------------------------------------------------ # MATH # ------------------------------------------------------------------ def _np_broadcast(arrays): shapes = [arr.shape for arr in arrays] for i in range(len(shapes[0])): ish = [sh[i] for sh in shapes] max_len = max(ish) for j in range(len(arrays)): leng = ish[j] if leng == 1: arrays[j] = np.repeat(arrays[j], max_len, axis=i) for arr in arrays: arr = arr.flatten() #vt = v0 + (v1 - v0) * t return arrays def lerp(a, b, t): return a + (b - a) * t def _lerp2(v1, v2, v3, v4, v): v12 = v1.lerp(v2,v.x) # + (v2 - v1) * v.x v34 = v3.lerp(v4,v.x) # + (v4 - v3) * v.x return v12.lerp(v34, v.y)# + (v34 - v12) * v.y def lerp2(v1, v2, v3, v4, v): v12 = v1 + (v2 - v1) * v.x v34 = v3 + (v4 - v3) * v.x v = v12 + (v34 - v12) * v.y return v def lerp3(v1, v2, v3, v4, v): loc = lerp2(v1.co, v2.co, v3.co, v4.co, v) nor = lerp2(v1.normal, v2.normal, v3.normal, v4.normal, v) nor.normalize() return loc + nor * v.z import sys def np_lerp2(v00, v10, v01, v11, vx, vy, mode=''): if 'numba' in sys.modules and use_numba_tess(): if mode == 'verts': co2 = numba_interp_points(v00, v10, v01, v11, vx, vy) elif mode == 'shapekeys': co2 = numba_interp_points_sk(v00, v10, v01, v11, vx, vy) else: co2 = numba_lerp2(v00, v10, v01, v11, vx, vy) else: co0 = v00 + (v10 - v00) * vx co1 = v01 + (v11 - v01) * vx co2 = co0 + (co1 - co0) * vy return co2 def calc_thickness(co2,n2,vz,a,weight): if 'numba' in sys.modules and use_numba_tess(): if len(co2.shape) == 3: if type(a) != np.ndarray: a = np.ones(len(co2)).reshape((-1,1,1)) if type(weight) != np.ndarray: weight = np.ones(len(co2)).reshape((-1,1,1)) co3 = numba_calc_thickness_area_weight(co2,n2,vz,a,weight) elif len(co2.shape) == 4: n_patches = co2.shape[0] n_sk = co2.shape[1] n_verts = co2.shape[2] if type(a) != np.ndarray: a = np.ones(n_patches).reshape((n_patches,1,1,1)) if type(weight) != np.ndarray: weight = np.ones(n_patches).reshape((n_patches,1,1,1)) na = a.shape[1]-1 nw = weight.shape[1]-1 co3 = np.empty((n_sk,n_patches,n_verts,3)) for i in range(n_sk): co3[i] = numba_calc_thickness_area_weight(co2[:,i],n2[:,i],vz[:,i],a[:,min(i,na)],weight[:,min(i,nw)]) co3 = co3.swapaxes(0,1) else: use_area = type(a) == np.ndarray use_weight = type(weight) == np.ndarray if use_area: if use_weight: co3 = co2 + n2 * vz * a * weight else: co3 = co2 + n2 * vz * a else: if use_weight: co3 = co2 + n2 * vz * weight else: co3 = co2 + n2 * vz return co3 def combine_and_flatten(arrays): if 'numba' in sys.modules: new_list = numba_combine_and_flatten(arrays) else: new_list = np.concatenate(arrays, axis=0) new_list = new_list.flatten().tolist() return new_list def np_interp2(grid, vx, vy): grid_shape = grid.shape[-2:] levels = len(grid.shape)-2 nu = grid_shape[0] nv = grid_shape[1] u = np.arange(nu)/(nu-1) v = np.arange(nv)/(nv-1) u_shape = [1]*levels + [nu] v_shape = [1]*levels + [nv] co0 = np.interp() co1 = np.interp() co2 = np.interp() return co2 def flatten_vector(vec, x, y): """ Find planar vector according to two axis. :arg vec: Input vector. :type vec: :class:'mathutils.Vector' :arg x: First axis. :type x: :class:'mathutils.Vector' :arg y: Second axis. :type y: :class:'mathutils.Vector' :return: Projected 2D Vector. :rtype: :class:'mathutils.Vector' """ vx = vec.project(x) vy = vec.project(y) mult = 1 if vx.dot(x) > 0 else -1 vx = mult*vx.length mult = 1 if vy.dot(y) > 0 else -1 vy = mult*vy.length return Vector((vx, vy)) def vector_rotation(vec): """ Find vector rotation according to X axis. :arg vec: Input vector. :type vec: :class:'mathutils.Vector' :return: Angle in radians. :rtype: float """ v0 = Vector((1,0)) ang = Vector.angle_signed(vec, v0) if ang < 0: ang = 2*pi + ang return ang # ------------------------------------------------------------------ # SCENE # ------------------------------------------------------------------ def set_animatable_fix_handler(self, context): ''' Prevent Blender Crashes with handlers ''' old_handlers = [] blender_handlers = bpy.app.handlers.render_init for h in blender_handlers: if "turn_off_animatable" in str(h): old_handlers.append(h) for h in old_handlers: blender_handlers.remove(h) blender_handlers.append(turn_off_animatable) return def turn_off_animatable(scene): ''' Prevent Blender Crashes with handlers ''' for o in [o for o in bpy.data.objects if o.type == 'MESH']: o.tissue_tessellate.bool_run = False #if not o.reaction_diffusion_settings.bool_cache: # o.reaction_diffusion_settings.run = False #except: pass return # ------------------------------------------------------------------ # OBJECTS # ------------------------------------------------------------------ def convert_object_to_mesh(ob, apply_modifiers=True, preserve_status=True): try: ob.name except: return None if ob.type != 'MESH': if not apply_modifiers: mod_visibility = [m.show_viewport for m in ob.modifiers] for m in ob.modifiers: m.show_viewport = False #ob.modifiers.update() #dg = bpy.context.evaluated_depsgraph_get() #ob_eval = ob.evaluated_get(dg) #me = bpy.data.meshes.new_from_object(ob_eval, preserve_all_data_layers=True, depsgraph=dg) me = simple_to_mesh(ob) new_ob = bpy.data.objects.new(ob.data.name, me) new_ob.location, new_ob.matrix_world = ob.location, ob.matrix_world if not apply_modifiers: for m,vis in zip(ob.modifiers,mod_visibility): m.show_viewport = vis else: if apply_modifiers: new_ob = ob.copy() new_me = simple_to_mesh(ob) new_ob.modifiers.clear() new_ob.data = new_me else: new_ob = ob.copy() new_ob.data = ob.data.copy() new_ob.modifiers.clear() bpy.context.collection.objects.link(new_ob) if preserve_status: new_ob.select_set(False) else: for o in bpy.context.view_layer.objects: o.select_set(False) new_ob.select_set(True) bpy.context.view_layer.objects.active = new_ob return new_ob def simple_to_mesh(ob, depsgraph=None): ''' Convert object to mesh applying Modifiers and Shape Keys ''' #global evaluatedDepsgraph if depsgraph == None: if config.evaluatedDepsgraph == None: dg = bpy.context.evaluated_depsgraph_get() else: dg = config.evaluatedDepsgraph else: dg = depsgraph ob_eval = ob.evaluated_get(dg) me = bpy.data.meshes.new_from_object(ob_eval, preserve_all_data_layers=True, depsgraph=dg) me.calc_normals() return me def _join_objects(context, objects, link_to_scene=True, make_active=True): C = context bm = bmesh.new() materials = {} faces_materials = [] if config.evaluatedDepsgraph == None: dg = C.evaluated_depsgraph_get() else: dg = config.evaluatedDepsgraph for o in objects: bm.from_object(o, dg) # add object's material to the dictionary for m in o.data.materials: if m not in materials: materials[m] = len(materials) for f in o.data.polygons: index = f.material_index mat = o.material_slots[index].material new_index = materials[mat] faces_materials.append(new_index) bm.verts.ensure_lookup_table() bm.edges.ensure_lookup_table() bm.faces.ensure_lookup_table() # assign new indexes for index, f in zip(faces_materials, bm.faces): f.material_index = index # create object me = bpy.data.meshes.new('joined') bm.to_mesh(me) me.update() ob = bpy.data.objects.new('joined', me) if link_to_scene: C.collection.objects.link(ob) # make active if make_active: for o in C.view_layer.objects: o.select_set(False) ob.select_set(True) C.view_layer.objects.active = ob # add materials for m in materials.keys(): ob.data.materials.append(m) return ob def join_objects(context, objects): generated_data = [o.data for o in objects] context.view_layer.update() for o in context.view_layer.objects: o.select_set(o in objects) bpy.ops.object.join() new_ob = context.view_layer.objects.active new_ob.select_set(True) for me in generated_data: if me != new_ob.data: bpy.data.meshes.remove(me) return new_ob def join_objects(objects): override = bpy.context.copy() new_ob = objects[0] override['active_object'] = new_ob override['selected_editable_objects'] = objects bpy.ops.object.join(override) return new_ob def repeat_mesh(me, n): ''' Return Mesh data adding and applying an array without offset (Slower) ''' bm = bmesh.new() for i in range(n): bm.from_mesh(me) new_me = me.copy() bm.to_mesh(new_me) bm.free() return new_me def array_mesh(ob, n): ''' Return Mesh data adding and applying an array without offset ''' arr = ob.modifiers.new('Repeat','ARRAY') arr.relative_offset_displace[0] = 0 arr.count = n #bpy.ops.object.modifier_apply({'active_object':ob},modifier='Repeat') #me = ob.data ob.modifiers.update() dg = bpy.context.evaluated_depsgraph_get() me = simple_to_mesh(ob, depsgraph=dg) ob.modifiers.remove(arr) return me def array_mesh_object(ob, n): ''' Return Mesh data adding and applying an array without offset ''' arr = ob.modifiers.new('Repeat','ARRAY') arr.relative_offset_displace[0] = 0 arr.count = n ob.modifiers.update() override = bpy.context.copy() override['active_object'] = ob override = {'active_object': ob} bpy.ops.object.modifier_apply(override, modifier=arr.name) return ob def get_mesh_before_subs(ob): 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') subs = 0 hide_mods = [] mods_visibility = [] for m in ob.modifiers: hide_mods.append(m) mods_visibility.append(m.show_viewport) if m.type in ('SUBSURF','MULTIRES'): hide_mods = [m] subs = m.levels elif m.type in not_allowed: subs = 0 hide_mods = [] mods_visibility = [] for m in hide_mods: m.show_viewport = False me = simple_to_mesh(ob) for m, vis in zip(hide_mods,mods_visibility): m.show_viewport = vis return me, subs # ------------------------------------------------------------------ # MESH FUNCTIONS # ------------------------------------------------------------------ def calc_verts_area(me): n_verts = len(me.vertices) n_faces = len(me.polygons) vareas = np.zeros(n_verts) vcount = np.zeros(n_verts) parea = [0]*n_faces pverts = [0]*n_faces*4 me.polygons.foreach_get('area', parea) me.polygons.foreach_get('vertices', pverts) parea = np.array(parea) pverts = np.array(pverts).reshape((n_faces, 4)) for a, verts in zip(parea,pverts): vareas[verts] += a vcount[verts] += 1 return vareas / vcount def calc_verts_area_bmesh(me): bm = bmesh.new() bm.from_mesh(me) bm.verts.ensure_lookup_table() verts_area = np.zeros(len(me.vertices)) for v in bm.verts: area = 0 faces = v.link_faces for f in faces: area += f.calc_area() verts_area[v.index] = area if area == 0 else area/len(faces) bm.free() return verts_area import time def get_patches____(me_low, me_high, sides, subs, bool_selection, bool_material_id, material_id): nv = len(me_low.vertices) # number of vertices ne = len(me_low.edges) # number of edges nf = len(me_low.polygons) # number of polygons n = 2**subs + 1 # number of vertices along each patch edge nev = ne * n # number of vertices along the subdivided edges nevi = nev - 2*ne # internal vertices along subdividede edges n0 = 2**(subs-1) - 1 # filtered polygonal faces poly_sides = np.array([len(p.vertices) for p in me_low.polygons]) mask = poly_sides == sides if bool_material_id: mask_material = [1]*nf me_low.polygons.foreach_get('material_index',mask_material) mask_material = np.array(mask_material) == material_id mask = np.logical_and(mask,mask_material) if bool_selection: mask_selection = [True]*nf me_low.polygons.foreach_get('select',mask_selection) mask_selection = np.array(mask_selection) mask = np.logical_and(mask,mask_selection) polys = np.array(me_low.polygons)[mask] mult = n0**2 + n0 ps = poly_sides * mult + 1 ps = np.insert(ps,0,nv + nevi, axis=0)[:-1] ips = ps.cumsum()[mask] # incremental polygon sides nf = len(polys) # when subdivided quad faces follows a different pattern if sides == 4: n_patches = nf else: n_patches = nf*sides if sides == 4: patches = np.zeros((nf,n,n),dtype='int') verts = [[vv for vv in p.vertices] for p in polys if len(p.vertices) == sides] verts = np.array(verts).reshape((-1,sides)) # filling corners patches[:,0,0] = verts[:,0] patches[:,n-1,0] = verts[:,1] patches[:,n-1,n-1] = verts[:,2] patches[:,0,n-1] = verts[:,3] if subs != 0: shift_verts = np.roll(verts, -1, axis=1)[:,:,None] edge_keys = np.concatenate((shift_verts, verts[:,:,None]), axis=2) edge_keys.sort() edge_verts = np.array(me_low.edge_keys) # edges keys edges_index = np.zeros((ne,ne),dtype='int') edges_index[edge_verts[:,0],edge_verts[:,1]] = np.arange(ne) evi = np.arange(nevi) + nv evi = evi.reshape(ne,n-2) # edges inner verts straight = np.arange(n-2)+1 inverted = np.flip(straight) inners = np.array([[j*(n-2)+i for j in range(n-2)] for i in range(n-2)]) ek1 = np.array(me_high.edge_keys) # edges keys ids0 = np.arange(ne)*(n-1) # edge keys highres keys0 = ek1[ids0] # first inner edge keys1 = ek1[ids0 + n-2] # last inner edge keys = np.concatenate((keys0,keys1)) pick_verts = np.array((inverted,straight)) patch_index = np.arange(nf)[:,None,None] # edge 0 e0 = edge_keys[:,0] # get edge key (faces, 2) edge_id = edges_index[e0[:,0],e0[:,1]] # edge index edge_verts = evi[edge_id] # indexes of inner vertices test = np.concatenate((verts[:,0,None], edge_verts[:,0,None]),axis=1) dir = (test[:,None] == keys).all(2).any(1).astype('int8') #dir = np.full(verts[:,0].shape, 0, dtype='int8') ids = pick_verts[dir][:,None,:] # indexes order along the side patches[patch_index,ids,0] = edge_verts[:,None,:] # assign indexes #patches[:,msk] = inverted # np.flip(patches[msk]) # edge 1 e0 = edge_keys[:,1] # get edge key (faces, 2) edge_id = edges_index[e0[:,0],e0[:,1]] # edge index edge_verts = evi[edge_id] # indexes of inner vertices test = np.concatenate((verts[:,1,None], edge_verts[:,0,None]),axis=1) dir = (test[:,None] == keys).all(2).any(1).astype('int8') ids = pick_verts[dir][:,:,None] # indexes order along the side patches[patch_index,n-1,ids] = edge_verts[:,:,None] # assign indexes # edge 2 e0 = edge_keys[:,2] # get edge key (faces, 2) edge_id = edges_index[e0[:,0],e0[:,1]] # edge index edge_verts = evi[edge_id] # indexes of inner vertices test = np.concatenate((verts[:,3,None], edge_verts[:,0,None]),axis=1) dir = (test[:,None] == keys).all(2).any(1).astype('int8') ids = pick_verts[dir][:,None,:] # indexes order along the side patches[patch_index,ids,n-1] = edge_verts[:,None,:] # assign indexes # edge 3 e0 = edge_keys[:,3] # get edge key (faces, 2) edge_id = edges_index[e0[:,0],e0[:,1]] # edge index edge_verts = evi[edge_id] # indexes of inner vertices test = np.concatenate((verts[:,0,None], edge_verts[:,0,None]),axis=1) dir = (test[:,None] == keys).all(2).any(1).astype('int8') ids = pick_verts[dir][:,:,None] # indexes order along the side patches[patch_index,0,ids] = edge_verts[:,:,None] # assign indexes # fill inners patches[:,1:-1,1:-1] = inners[None,:,:] + ips[:,None,None] #end_time = time.time() #print('Tissue: Got Patches in {:.4f} sec'.format(end_time-start_time)) return patches, mask def tessellate_prepare_component(ob1, props): mode = props['mode'] bounds_x = props['bounds_x'] bounds_y = props['bounds_y'] scale_mode = props['scale_mode'] normals_mode = props['normals_mode'] zscale = props['zscale'] offset = props['offset'] use_origin_offset = props['use_origin_offset'] bool_shapekeys = props['bool_shapekeys'] thres = 0.005 me1 = ob1.data # Component statistics n_verts = len(me1.vertices) # 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 if use_origin_offset: vert[2] = v.co[2] 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 if scale_mode == 'CONSTANT' or normals_mode in ('OBJECT', 'SHAPEKEYS'): if not use_origin_offset: vert[2] = vert[2] / bb[2] if bb[2] != 0 else 0 vert[2] = vert[2] - 0.5 + offset * 0.5 else: if not use_origin_offset: vert[2] = vert[2] + (-0.5 + offset * 0.5) * bb[2] vert[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 v.co = vert # ShapeKeys if bool_shapekeys and ob1.data.shape_keys: for sk in ob1.data.shape_keys.key_blocks: source = sk.data _sk_uv_quads = [0]*len(verts1) _sk_uv = [0]*len(verts1) for i, sk_v in enumerate(source): if mode == 'BOUNDS': sk_vert = sk_v.co - min_c if use_origin_offset: sk_vert[2] = sk_v.co[2] 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) if scale_mode == 'CONSTANT' or normals_mode in ('OBJECT', 'SHAPEKEYS'): if not use_origin_offset: sk_vert[2] = (sk_vert[2] / bb[2] if bb[2] != 0 else sk_vert[2]) sk_vert[2] = sk_vert[2] - 0.5 + offset * 0.5 else: if not use_origin_offset: sk_vert[2] = sk_vert[2] + (- 0.5 + offset * 0.5) * bb[2] sk_vert[2] *= zscale elif mode == 'LOCAL': sk_vert = sk_v.co sk_vert[2] *= zscale elif mode == 'GLOBAL': sk_vert = sk_v.co sk_vert[2] *= zscale sk_v.co = sk_vert if mode != 'BOUNDS' and (bounds_x != 'EXTEND' or bounds_y != 'EXTEND'): ob1.active_shape_key_index = 0 bm = bmesh.new() bm.from_mesh(me1) # Bound X planes_co = [] planes_no = [] bounds = [] if bounds_x != 'EXTEND': planes_co += [(0,0,0), (1,0,0)] planes_no += [(-1,0,0), (1,0,0)] bounds += [bounds_x, bounds_x] if bounds_y != 'EXTEND': planes_co += [(0,0,0), (0,1,0)] planes_no += [(0,-1,0), (0,1,0)] bounds += [bounds_y, bounds_y] for co, norm, bound in zip(planes_co, planes_no, bounds): count = 0 while True: moved = 0 original_edges = list(bm.edges) geom = list(bm.verts) + list(bm.edges) + list(bm.faces) bisect = bmesh.ops.bisect_plane(bm, geom=geom, dist=0, plane_co=co, plane_no=norm, use_snap_center=False, clear_outer=bound=='CLIP', clear_inner=False ) geom = bisect['geom'] cut_edges = [g for g in bisect['geom_cut'] if type(g)==bmesh.types.BMEdge] cut_verts = [g for g in bisect['geom_cut'] if type(g)==bmesh.types.BMVert] if bound!='CLIP': for e in cut_edges: seam = True # Prevent glitches for e1 in original_edges: match_00 = (e.verts[0].co-e1.verts[0].co).length < thres match_11 = (e.verts[1].co-e1.verts[1].co).length < thres match_01 = (e.verts[0].co-e1.verts[1].co).length < thres match_10 = (e.verts[1].co-e1.verts[0].co).length < thres if (match_00 and match_11) or (match_01 and match_10): seam = False break e.seam = seam if bound == 'CYCLIC': geom_verts = [] if norm == (-1,0,0): geom_verts = [v for v in bm.verts if v.co.x < 0] if norm == (1,0,0): geom_verts = [v for v in bm.verts if v.co.x > 1] if norm == (0,-1,0): geom_verts = [v for v in bm.verts if v.co.y < 0] if norm == (0,1,0): geom_verts = [v for v in bm.verts if v.co.y > 1] if len(geom_verts) > 0: geom = bmesh.ops.region_extend(bm, geom=geom_verts, use_contract=False, use_faces=False, use_face_step=True ) geom = bmesh.ops.split(bm, geom=geom['geom'], use_only_faces=False) vec = Vector(norm) move_verts = [g for g in geom['geom'] if type(g)==bmesh.types.BMVert] bmesh.ops.translate(bm, vec=-vec, verts=move_verts) for key in bm.verts.layers.shape.keys(): sk = bm.verts.layers.shape.get(key) for v in move_verts: v[sk] -= vec moved += len(move_verts) count += 1 if moved == 0 or count > 1000: break bm.to_mesh(me1) com_area = bb[0]*bb[1] return ob1, com_area def get_quads(me, bool_selection): nf = len(me.polygons) verts = [] materials = [] mask = [] for poly in me.polygons: p = list(poly.vertices) sides = len(p) if sides == 3: verts.append([[p[0], p[-1]], [p[1], p[2]]]) materials.append(poly.material_index) mask.append(poly.select if bool_selection else True) elif sides == 4: verts.append([[p[0], p[3]], [p[1], p[2]]]) materials.append(poly.material_index) mask.append(poly.select if bool_selection else True) else: while True: new_poly = [[p[-2], p.pop(-1)], [p[1], p.pop(0)]] verts.append(new_poly) materials.append(poly.material_index) mask.append(poly.select if bool_selection else True) if len(p) < 3: break mask = np.array(mask) materials = np.array(materials)[mask] verts = np.array(verts)[mask] return verts, mask, materials def get_patches(me_low, me_high, sides, subs, bool_selection): #, bool_material_id, material_id): nv = len(me_low.vertices) # number of vertices ne = len(me_low.edges) # number of edges nf = len(me_low.polygons) # number of polygons n = 2**subs + 1 nev = ne * n # number of vertices along the subdivided edges nevi = nev - 2*ne # internal vertices along subdividede edges n0 = 2**(subs-1) - 1 # filtered polygonal faces poly_sides = [0]*nf me_low.polygons.foreach_get('loop_total',poly_sides) poly_sides = np.array(poly_sides) mask = poly_sides == sides if bool_selection: mask_selection = [True]*nf me_low.polygons.foreach_get('select',mask_selection) mask = np.array(mask_selection) materials = [1]*nf me_low.polygons.foreach_get('material_index',materials) materials = np.array(materials)[mask] polys = np.array(me_low.polygons)[mask] mult = n0**2 + n0 ps = poly_sides * mult + 1 ps = np.insert(ps,0,nv + nevi, axis=0)[:-1] ips = ps.cumsum()[mask] # incremental polygon sides nf = len(polys) # when subdivided quad faces follows a different pattern if sides == 4: n_patches = nf else: n_patches = nf*sides if sides == 4: patches = np.empty((nf,n,n),dtype='int') verts = [list(p.vertices) for p in polys if len(p.vertices) == sides] verts = np.array(verts).reshape((-1,sides)) # filling corners patches[:,0,0] = verts[:,0] patches[:,n-1,0] = verts[:,1] patches[:,n-1,n-1] = verts[:,2] patches[:,0,n-1] = verts[:,3] if subs != 0: shift_verts = np.roll(verts, -1, axis=1)[:,:,None] edge_keys = np.concatenate((shift_verts, verts[:,:,None]), axis=2) edge_keys.sort() edge_verts = np.array(me_low.edge_keys) # edges keys edges_index = np.empty((ne,ne),dtype='int') edges_index[edge_verts[:,0],edge_verts[:,1]] = np.arange(ne) evi = np.arange(nevi) + nv evi = evi.reshape(ne,n-2) # edges inner verts straight = np.arange(n-2)+1 inverted = np.flip(straight) inners = np.array([[j*(n-2)+i for j in range(n-2)] for i in range(n-2)]) ek1 = me_high.edge_keys # edges keys ek1 = np.array(ek1) # edge keys highres keys0 = ek1[np.arange(ne)*(n-1)] # first inner edge keys1 = ek1[np.arange(ne)*(n-1)+n-2] # last inner edge edges_dir = np.zeros((nev,nev),dtype='bool') # Better memory usage #edges_dir = np.zeros((nev,nev),dtype='int8') ### Memory usage not efficient, dictionary as alternative? edges_dir[keys0[:,0], keys0[:,1]] = 1 edges_dir[keys1[:,0], keys1[:,1]] = 1 pick_verts = np.array((inverted,straight)) patch_index = np.arange(nf)[:,None,None] # edge 0 e0 = edge_keys[:,0] # get edge key (faces, 2) edge_id = edges_index[e0[:,0],e0[:,1]] # edge index edge_verts = evi[edge_id] # indexes of inner vertices dir = edges_dir[verts[:,0], edge_verts[:,0]] # check correct direction ids = pick_verts[dir.astype('int8')][:,None,:] # indexes order along the side patches[patch_index,ids,0] = edge_verts[:,None,:] # assign indexes # edge 1 e0 = edge_keys[:,1] # get edge key (faces, 2) edge_id = edges_index[e0[:,0],e0[:,1]] # edge index edge_verts = evi[edge_id] # indexes of inner vertices dir = edges_dir[verts[:,1], edge_verts[:,0]] # check correct direction ids = pick_verts[dir.astype('int8')][:,:,None] # indexes order along the side patches[patch_index,n-1,ids] = edge_verts[:,:,None] # assign indexes # edge 2 e0 = edge_keys[:,2] # get edge key (faces, 2) edge_id = edges_index[e0[:,0],e0[:,1]] # edge index edge_verts = evi[edge_id] # indexes of inner vertices dir = edges_dir[verts[:,3], edge_verts[:,0]] # check correct direction ids = pick_verts[dir.astype('int8')][:,None,:] # indexes order along the side patches[patch_index,ids,n-1] = edge_verts[:,None,:] # assign indexes # edge 3 e0 = edge_keys[:,3] # get edge key (faces, 2) edge_id = edges_index[e0[:,0],e0[:,1]] # edge index edge_verts = evi[edge_id] # indexes of inner vertices dir = edges_dir[verts[:,0], edge_verts[:,0]] # check correct direction ids = pick_verts[dir.astype('int8')][:,:,None] # indexes order along the side patches[patch_index,0,ids] = edge_verts[:,:,None] # assign indexes # fill inners patches[:,1:-1,1:-1] = inners[None,:,:] + ips[:,None,None] return patches, mask, materials def get_vertices_numpy(mesh): ''' Create a numpy array with the vertices of a given mesh ''' n_verts = len(mesh.vertices) verts = [0]*n_verts*3 mesh.vertices.foreach_get('co', verts) verts = np.array(verts).reshape((n_verts,3)) return verts def get_vertices_and_normals_numpy(mesh): ''' Create two numpy arrays with the vertices and the normals of a given mesh ''' n_verts = len(mesh.vertices) verts = [0]*n_verts*3 normals = [0]*n_verts*3 mesh.vertices.foreach_get('co', verts) mesh.vertices.foreach_get('normal', normals) verts = np.array(verts).reshape((n_verts,3)) normals = np.array(normals).reshape((n_verts,3)) return verts, normals def get_normals_numpy(mesh): ''' Create a numpy array with the normals of a given mesh ''' n_verts = len(mesh.vertices) normals = [0]*n_verts*3 mesh.vertices.foreach_get('normal', normals) normals = np.array(normals).reshape((n_verts,3)) return normals def get_edges_numpy(mesh): ''' Create a numpy array with the edges of a given mesh ''' n_edges = len(mesh.edges) edges = [0]*n_edges*2 mesh.edges.foreach_get('vertices', edges) edges = np.array(edges).reshape((n_edges,2)).astype('int') return edges def get_edges_id_numpy(mesh): n_edges = len(mesh.edges) edges = [0]*n_edges*2 mesh.edges.foreach_get('vertices', edges) edges = np.array(edges).reshape((n_edges,2)) indexes = np.arange(n_edges).reshape((n_edges,1)) edges = np.concatenate((edges,indexes), axis=1) return edges def get_polygons_select_numpy(mesh): n_polys = len(mesh.polygons) selections = [0]*n_polys*2 mesh.polygons.foreach_get('select', selections) selections = np.array(selections) return selections def get_attribute_numpy(elements_list, attribute='select', mult=1): ''' Generate a numpy array getting attribute from a list of element using the foreach_get() function. ''' n_elements = len(elements_list) values = [0]*n_elements*mult elements_list.foreach_get(attribute, values) values = np.array(values) if mult > 1: values = values.reshape((n_elements,mult)) return values def get_vertices(mesh): n_verts = len(mesh.vertices) verts = [0]*n_verts*3 mesh.vertices.foreach_get('co', verts) verts = np.array(verts).reshape((n_verts,3)) verts = [Vector(v) for v in verts] return verts def get_faces(mesh): faces = [[v for v in f.vertices] for f in mesh.polygons] return faces def get_faces_numpy(mesh): faces = [[v for v in f.vertices] for f in mesh.polygons] return np.array(faces) def get_faces_edges_numpy(mesh): faces = [v.edge_keys for f in mesh.polygons] return np.array(faces) def find_curves(edges, n_verts): verts_dict = {key:[] for key in range(n_verts)} for e in edges: verts_dict[e[0]].append(e[1]) verts_dict[e[1]].append(e[0]) curves = [] while True: if len(verts_dict) == 0: break # next starting point v = list(verts_dict.keys())[0] # neighbors v01 = verts_dict[v] if len(v01) == 0: verts_dict.pop(v) continue curve = [] if len(v01) > 1: curve.append(v01[1]) # add neighbors curve.append(v) # add starting point curve.append(v01[0]) # add neighbors verts_dict.pop(v) # start building curve while True: #last_point = curve[-1] #if last_point not in verts_dict: break # try to change direction if needed if curve[-1] in verts_dict: pass elif curve[0] in verts_dict: curve.reverse() else: break # neighbors points last_point = curve[-1] v01 = verts_dict[last_point] # curve end if len(v01) == 1: verts_dict.pop(last_point) if curve[0] in verts_dict: continue else: break # chose next point new_point = None if v01[0] == curve[-2]: new_point = v01[1] elif v01[1] == curve[-2]: new_point = v01[0] #else: break #if new_point != curve[1]: curve.append(new_point) verts_dict.pop(last_point) if curve[0] == curve[-1]: verts_dict.pop(new_point) break curves.append(curve) return curves def curve_from_points(points, name='Curve'): curve = bpy.data.curves.new(name,'CURVE') for c in points: s = curve.splines.new('POLY') s.points.add(len(c)) for i,p in enumerate(c): s.points[i].co = p.xyz + [1] ob_curve = bpy.data.objects.new(name,curve) return ob_curve def curve_from_pydata(points, radii, indexes, name='Curve', skip_open=False, merge_distance=1, set_active=True, only_data=False): curve = bpy.data.curves.new(name,'CURVE') curve.dimensions = '3D' use_rad = True for c in indexes: bool_cyclic = c[0] == c[-1] if bool_cyclic: c.pop(-1) # cleanup pts = np.array([points[i] for i in c]) try: rad = np.array([radii[i] for i in c]) except: use_rad = False rad = 1 if merge_distance > 0: pts1 = np.roll(pts,1,axis=0) dist = np.linalg.norm(pts1-pts, axis=1) count = 0 n = len(dist) mask = np.ones(n).astype('bool') for i in range(n): count += dist[i] if count > merge_distance: count = 0 else: mask[i] = False pts = pts[mask] if use_rad: rad = rad[mask] if skip_open and not bool_cyclic: continue s = curve.splines.new('POLY') n_pts = len(pts) s.points.add(n_pts-1) w = np.ones(n_pts).reshape((n_pts,1)) co = np.concatenate((pts,w),axis=1).reshape((n_pts*4)) s.points.foreach_set('co',co) if use_rad: s.points.foreach_set('radius',rad) s.use_cyclic_u = bool_cyclic if only_data: return curve else: ob_curve = bpy.data.objects.new(name,curve) bpy.context.collection.objects.link(ob_curve) if set_active: bpy.context.view_layer.objects.active = ob_curve return ob_curve def update_curve_from_pydata(curve, points, normals, radii, indexes, merge_distance=1, pattern=[1,0], depth=0.1, offset=0): curve.splines.clear() use_rad = True for ic, c in enumerate(indexes): bool_cyclic = c[0] == c[-1] if bool_cyclic: c.pop(-1) # cleanup pts = np.array([points[i] for i in c if i != None]) nor = np.array([normals[i] for i in c if i != None]) try: rad = np.array([radii[i] for i in c if i != None]) except: use_rad = False rad = 1 if merge_distance > 0: pts1 = np.roll(pts,1,axis=0) dist = np.linalg.norm(pts1-pts, axis=1) count = 0 n = len(dist) mask = np.ones(n).astype('bool') for i in range(n): count += dist[i] if count > merge_distance: count = 0 else: mask[i] = False pts = pts[mask] nor = nor[mask] if use_rad: rad = rad[mask] #if skip_open and not bool_cyclic: continue n_pts = len(pts) series = np.arange(n_pts) patt1 = series + (series-series%pattern[1])/pattern[1]*pattern[0]+pattern[0] patt1 = patt1[patt1 0: edge = todo_edges[0] vert_loop, edge_loop = run_edge_loop(edge) for e in edge_loop: try: todo_edges.remove(e) except: pass edge_loops.append(edge_loop) vert_loops.append(vert_loop) #if len(todo_edges) == 0: break return vert_loops, edge_loops def run_edge_loop_direction(edge,vert): """ Return vertices and edges along a loop in a specific direction. :arg edge: Edges used as seed. :type edges: :class:'bmesh.types.BMEdge' :arg edge: Vertex of the Edge used for the direction. :type vert: :class:'bmesh.types.BMVert' :return: Elements in the loop (Verts, Edges), where: - Verts - List of :class:'bmesh.types.BMVert' - Edges - List of :class:'bmesh.types.BMEdge' :rtype: tuple """ edge0 = edge edge_loop = [edge] vert_loop = [vert] while True: link_edges = list(vert.link_edges) link_edges.remove(edge) n_edges = len(link_edges) if n_edges == 1: edge = link_edges[0] elif n_edges < 4: link_faces = edge.link_faces if len(link_faces) == 0: break edge = None for e in link_edges: link_faces1 = e.link_faces if len(link_faces) == len(link_faces1): common_faces = [f for f in link_faces1 if f in link_faces] if len(common_faces) == 0: edge = e break else: break if edge == None: break edge_loop.append(edge) vert = edge.other_vert(vert) vert_loop.append(vert) if edge == edge0: break return vert_loop, edge_loop def run_edge_loop(edge): """ Return vertices and edges along a loop in both directions. :arg edge: Edges used as seed. :type edges: :class:'bmesh.types.BMEdge' :return: Elements in the loop (Verts, Edges), where: - Verts - List of :class:'bmesh.types.BMVert' - Edges - List of :class:'bmesh.types.BMEdge' :rtype: tuple """ vert0 = edge.verts[0] vert_loop0, edge_loop0 = run_edge_loop_direction(edge, vert0) if len(edge_loop0) == 1 or edge_loop0[0] != edge_loop0[-1]: vert1 = edge.verts[1] vert_loop1, edge_loop1 = run_edge_loop_direction(edge, vert1) edge_loop0.reverse() vert_loop0.reverse() edge_loop = edge_loop0[:-1] + edge_loop1 vert_loop = vert_loop0 + vert_loop1 else: edge_loop = edge_loop0[1:] vert_loop = vert_loop0 return vert_loop, edge_loop def curve_from_vertices(indexes, verts, name='Curve'): """ Curve data from given vertices. :arg indexes: List of Lists of indexes of the vertices. :type indexes: List of Lists of int :arg verts: List of vertices. :type verts: List of :class:'bpy.types.MeshVertex' :arg name: Name of the Curve data. :type name: str :return: Generated Curve data :rtype: :class:'bpy.types.Curve' """ curve = bpy.data.curves.new(name,'CURVE') for c in indexes: s = curve.splines.new('POLY') s.points.add(len(c)) for i,p in enumerate(c): s.points[i].co = verts[p].co.xyz + [1] #s.points[i].tilt = degrees(asin(verts[p].co.z)) ob_curve = bpy.data.objects.new(name,curve) return ob_curve def nurbs_from_vertices(indexes, co, radii=[], name='Curve', set_active=True, interpolation='POLY'): curve = bpy.data.curves.new(name,'CURVE') curve.dimensions = '3D' curve.resolution_u = 2 curve.bevel_depth = 0.01 curve.bevel_resolution = 0 for pts in indexes: s = curve.splines.new(interpolation) n_pts = len(pts) s.points.add(n_pts-1) w = np.ones(n_pts).reshape((n_pts,1)) curve_co = np.concatenate((co[pts],w),axis=1).reshape((n_pts*4)) s.points.foreach_set('co',curve_co) try: s.points.foreach_set('radius',radii[pts]) except: pass s.use_endpoint_u = True ob_curve = bpy.data.objects.new(name,curve) bpy.context.collection.objects.link(ob_curve) if set_active: bpy.context.view_layer.objects.active = ob_curve ob_curve.select_set(True) return ob_curve # ------------------------------------------------------------------ # VERTEX GROUPS AND WEIGHT # ------------------------------------------------------------------ def get_weight(vertex_group, n_verts): """ Read weight values from given Vertex Group. :arg vertex_group: Vertex Group. :type vertex_group: :class:'bpy.types.VertexGroup' :arg n_verts: Number of Vertices (output list size). :type n_verts: int :return: Read weight values. :rtype: list """ weight = [0]*n_verts for i in range(n_verts): try: weight[i] = vertex_group.weight(i) except: pass return weight def get_weight_numpy(vertex_group, n_verts): """ Read weight values from given Vertex Group. :arg vertex_group: Vertex Group. :type vertex_group: :class:'bpy.types.VertexGroup' :arg n_verts: Number of Vertices (output list size). :type n_verts: int :return: Read weight values as numpy array. :rtype: :class:'numpy.ndarray' """ weight = [0]*n_verts for i in range(n_verts): try: weight[i] = vertex_group.weight(i) except: pass return np.array(weight) def bmesh_get_weight_numpy(group_index, layer, verts): weight = np.zeros(len(verts)) for i, v in enumerate(verts): dvert = v[layer] if group_index in dvert: weight[i] = dvert[group_index] #dvert[group_index] = 0.5 return weight def bmesh_set_weight_numpy(group_index, layer, verts, weight): for i, v in enumerate(verts): dvert = v[layer] if group_index in dvert: dvert[group_index] = weight[i] return verts def bmesh_set_weight_numpy(bm, group_index, weight): layer = bm.verts.layers.deform.verify() for i, v in enumerate(bm.verts): dvert = v[layer] #if group_index in dvert: dvert[group_index] = weight[i] return bm def set_weight_numpy(vg, weight): for i, w in enumerate(weight): vg.add([i], w, 'REPLACE') return vg def uv_from_bmesh(bm, uv_index=None): if uv_index: uv_lay = bm.loops.layers.uv[uv_index] else: uv_lay = bm.loops.layers.uv.active uv_co = [0]*len(bm.verts) for face in bm.faces: for vert,loop in zip(face.verts, face.loops): uv_co[vert.index] = loop[uv_lay].uv return uv_co def get_uv_edge_vectors(me, uv_map = 0, only_positive=False): count = 0 uv_vectors = {} for i, f in enumerate(me.polygons): f_verts = len(f.vertices) for j0 in range(f_verts): j1 = (j0+1)%f_verts uv0 = me.uv_layers[uv_map].data[count+j0].uv uv1 = me.uv_layers[uv_map].data[count+j1].uv delta_uv = (uv1-uv0).normalized() if only_positive: delta_uv.x = abs(delta_uv.x) delta_uv.y = abs(delta_uv.y) edge_key = tuple(sorted([f.vertices[j0], f.vertices[j1]])) uv_vectors[edge_key] = delta_uv count += f_verts uv_vectors = [uv_vectors[tuple(sorted(e.vertices))] for e in me.edges] return uv_vectors def mesh_diffusion(me, values, iter, diff=0.2, uv_dir=0): values = np.array(values) n_verts = len(me.vertices) n_edges = len(me.edges) edge_verts = [0]*n_edges*2 #me.edges.foreach_get("vertices", edge_verts) count = 0 edge_verts = [] uv_factor = {} uv_ang = (0.5 + uv_dir*0.5)*pi/2 uv_vec = Vector((cos(uv_ang), sin(uv_ang))) for i, f in enumerate(me.polygons): f_verts = len(f.vertices) for j0 in range(f_verts): j1 = (j0+1)%f_verts if uv_dir != 0: uv0 = me.uv_layers[0].data[count+j0].uv uv1 = me.uv_layers[0].data[count+j1].uv delta_uv = (uv1-uv0).normalized() delta_uv.x = abs(delta_uv.x) delta_uv.y = abs(delta_uv.y) dir = uv_vec.dot(delta_uv) else: dir = 1 #dir = abs(dir) #uv_factor.append(dir) edge_key = [f.vertices[j0], f.vertices[j1]] edge_key.sort() uv_factor[tuple(edge_key)] = dir count += f_verts id0 = [] id1 = [] uv_mult = [] for ek, val in uv_factor.items(): id0.append(ek[0]) id1.append(ek[1]) uv_mult.append(val) id0 = np.array(id0) id1 = np.array(id1) uv_mult = np.array(uv_mult) #edge_verts = np.array(edge_verts) #arr = np.arange(n_edges)*2 #id0 = edge_verts[arr] # first vertex indices for each edge #id1 = edge_verts[arr+1] # second vertex indices for each edge for ii in range(iter): lap = np.zeros(n_verts) if uv_dir != 0: lap0 = (values[id1] - values[id0])*uv_mult # laplacian increment for first vertex of each edge else: lap0 = (values[id1] - values[id0]) np.add.at(lap, id0, lap0) np.add.at(lap, id1, -lap0) values += diff*lap return values def mesh_diffusion_vector(me, vectors, iter, diff, uv_dir=0): vectors = np.array(vectors) x = vectors[:,0] y = vectors[:,1] z = vectors[:,2] x = mesh_diffusion(me, x, iter, diff, uv_dir) y = mesh_diffusion(me, y, iter, diff, uv_dir) z = mesh_diffusion(me, z, iter, diff, uv_dir) vectors[:,0] = x vectors[:,1] = y vectors[:,2] = z return vectors # ------------------------------------------------------------------ # MODIFIERS # ------------------------------------------------------------------ def mod_preserve_topology(mod): same_topology_modifiers = ('DATA_TRANSFER','NORMAL_EDIT','WEIGHTED_NORMAL', 'UV_PROJECT','UV_WARP','VERTEX_WEIGHT_EDIT','VERTEX_WEIGHT_MIX', 'VERTEX_WEIGHT_PROXIMITY','ARMATURE','CAST','CURVE','DISPLACE','HOOK', 'LAPLACIANDEFORM','LATTICE','MESH_DEFORM','SHRINKWRAP','SIMPLE_DEFORM', 'SMOOTH','CORRECTIVE_SMOOTH','LAPLACIANSMOOTH','SURFACE_DEFORM','WARP', 'WAVE','CLOTH','COLLISION','DYNAMIC_PAINT','SOFT_BODY' ) return mod.type in same_topology_modifiers def mod_preserve_shape(mod): same_shape_modifiers = ('DATA_TRANSFER','NORMAL_EDIT','WEIGHTED_NORMAL', 'UV_PROJECT','UV_WARP','VERTEX_WEIGHT_EDIT','VERTEX_WEIGHT_MIX', 'VERTEX_WEIGHT_PROXIMITY','DYNAMIC_PAINT' ) return mod.type in same_shape_modifiers def recurLayerCollection(layerColl, collName): ''' Recursively transverse layer_collection for a particular name. ''' found = None if (layerColl.name == collName): return layerColl for layer in layerColl.children: found = recurLayerCollection(layer, collName) if found: return found def auto_layer_collection(): ''' Automatically change active layer collection. ''' layer = bpy.context.view_layer.active_layer_collection layer_collection = bpy.context.view_layer.layer_collection if layer.hide_viewport or layer.collection.hide_viewport: collections = bpy.context.object.users_collection for c in collections: lc = recurLayerCollection(layer_collection, c.name) if not c.hide_viewport and not lc.hide_viewport: bpy.context.view_layer.active_layer_collection = lc