def __init__(self, location, normal, face_index, distance, field): self.co = location face = field.bm.faces[face_index] verts = [vert.co for vert in face.verts] mask = [ Vector((0, 0, field.mask.get(vert.index, 0))) for vert in field.bm.faces[face_index].verts ] normals = [vert.normal for vert in field.bm.faces[face_index].verts] self.normal = geometry.barycentric_transform(location, *verts, *normals) self.mask = geometry.barycentric_transform(location, *verts, *mask).z try: co = face.verts[0] vec = field.vert_field[co.index].u vecs = [ field.vert_field[vert.index].get_nearest_vec(vec) for vert in face.verts ] vec = geometry.barycentric_transform(location, *verts, *vecs) except KeyError: vec = Vector((random(), random(), random())).cross(self.normal).normalized() self.frame = CrossFrame(vec, self.normal) self.face = face_index
def sample_point(self, point, ref_dir=None): location, normal, index, distance = self.bvh.find_nearest(point) if location: face = self.bm.faces[index] face_verts_co = [vert.co for vert in face.verts] if not ref_dir: ref_dir = self.field[face.verts[0].index] field = [ nmv.physics.best_matching_vector( nmv.physics.symmetry_space(self.field[vert.index], vert.normal) if not self.hex_mode else nmv.physics.hex_symmetry_space( self.field[vert.index], vert.normal), reference=ref_dir) for vert in face.verts ] dir = barycentric_transform(point, *face_verts_co, *field) scale_curv = [ Vector((self.scale[vert.index], self.curvature[vert.index], 0)) for vert in face.verts ] scale_curv = barycentric_transform(point, *face_verts_co, *scale_curv) scale = scale_curv[0] curv = scale_curv[1] dir -= normal * normal.dot(dir) dir.normalize() return location, normal, dir, scale, curv else: return None, None, None, None
def compute_intersect_circle_circle(params, result, gates): '''Compute two circles intersection(s) Result has to be [[],[],[]] to host the solutions Gates are as follow: 0: Is there a intersection? 1: First Intersection 2: Second intersection 3: The working plane is defined by its normal (True) or by a third point (False)''' center_a, radius_a, center_b, radius_b, plane_pt, plane_normal = params local_result = [] sphere_loc_a = V(center_a) sphere_loc_b = V(center_b) if gates[3]: norm = V(plane_normal).normalized() else: v_in_plane = V(plane_pt) norm = normal([sphere_loc_a, sphere_loc_b, v_in_plane]) if norm.length == 0: if gates[3]: print("Circle Intersection Error: the Normal can't be (0,0,0)") else: print("Circle Intersection Error: the point in plane is aligned with origins") is_2d = norm.x == 0 and norm.y == 0 if is_2d and False: z_coord = sphere_loc_a.z inter_p = intersect_sphere_sphere_2d(sphere_loc_a.to_2d(), radius_a, sphere_loc_b.to_2d(), radius_b) if inter_p[0]: intersect = True vec1 = list(inter_p[1]) + [z_coord] vec0 = list(inter_p[0]) + [z_coord] local_result = [intersect, vec0, vec1] else: dist = (sphere_loc_a - sphere_loc_b).length new_a = V([0, 0, 0]) new_b = V([dist, 0, 0]) inter_p = intersect_sphere_sphere_2d(new_a.to_2d(), radius_a, new_b.to_2d(), radius_b) if inter_p[0]: intersect_num = True trird_point = sphere_loc_a + norm new_c = V([0, 0, 1]) vec0 = barycentric_transform(inter_p[0].to_3d(), new_a, new_b, new_c, sphere_loc_a, sphere_loc_b, trird_point) vec1 = barycentric_transform(inter_p[1].to_3d(), new_a, new_b, new_c, sphere_loc_a, sphere_loc_b, trird_point) local_result = [intersect_num, list(vec0), list(vec1)] if not local_result: direc = (sphere_loc_b - sphere_loc_a).normalized() intersect_num = False vec0 = sphere_loc_a + direc * radius_a vec1 = sphere_loc_b - direc * radius_b local_result = [intersect_num, list(vec0), list(vec1)] for i, res in enumerate(result): if gates[i]: res.append(local_result[i])
def process(self): Points = self.inputs[1] Colors = self.outputs[0] if Colors.is_linked: dps = get_sv_depsgraph() obj = self.inputs[0].sv_get()[0] # triangulate faces bvh = BVHTree.FromObject(obj, dps) point = Points.sv_get()[0] outc = [] ran = range(3) image = bpy.data.images[self.image] width, height = image.size # Must be exactly the same vertically and horizontally, square image. pixels = np.array(image.pixels[:]).reshape(width, height, 4) uvMap = obj.data.uv_layers[0].data for P in point: loc, norm, ind, dist = bvh.find_nearest(P) found_poly = obj.data.polygons[ind] verticesIndices = found_poly.vertices p1, p2, p3 = [ obj.data.vertices[verticesIndices[i]].co for i in ran ] uvMapIndices = found_poly.loop_indices uv1, uv2, uv3 = [ uvMap[uvMapIndices[i]].uv.to_3d() for i in ran ] V = barycentric_transform(loc, p1, p2, p3, uv1, uv2, uv3) outc.append(pixels[int(V.y * (width - 1)), int(V.x * (height - 1))].tolist()) Colors.sv_set([outc])
def process(self): Points = self.inputs[0] Colors = self.outputs[0] if Colors.is_linked: obj = bpy.data.objects[self.object_ref] # triangulate faces bvh = BVHTree.FromObject(obj, bpy.context.scene, deform=True, render=False, cage=False, epsilon=0.0) point = Points.sv_get()[0] outc = [] ran = range(3) image = bpy.data.images[self.image] width, height = image.size pixels = np.array(image.pixels[:]).reshape(width, height, 4) uvMap = obj.data.uv_layers[0].data for P in point: loc, norm, ind, dist = bvh.find_nearest(P) found_poly = obj.data.polygons[ind] verticesIndices = found_poly.vertices p1, p2, p3 = [ obj.data.vertices[verticesIndices[i]].co for i in ran ] uvMapIndices = found_poly.loop_indices uv1, uv2, uv3 = [ uvMap[uvMapIndices[i]].uv.to_3d() for i in ran ] V = barycentric_transform(loc, p1, p2, p3, uv1, uv2, uv3) outc.append(pixels[int(V.x * (width - 1)), int(V.y * (height - 1))].tolist()) Colors.sv_set([outc])
def process(self): Object, PointsUV = self.inputs Pom, uvV, uvP = self.outputs obj = Object.sv_get()[0] # triangulate faces UVMAPV, UVMAPP = UV(self, obj) if Pom.is_linked: pointuv = PointsUV.sv_get()[0] bvh = BVHTree.FromPolygons(UVMAPV, UVMAPP, all_triangles=False, epsilon=0.0) ran = range(3) out = [] uvMap = obj.data.uv_layers[0].data for Puv in pointuv: loc, norm, ind, dist = bvh.find_nearest(Puv) found_poly = obj.data.polygons[ind] verticesIndices = found_poly.vertices p1, p2, p3 = [ obj.data.vertices[verticesIndices[i]].co for i in ran ] uvMapIndices = found_poly.loop_indices uv1, uv2, uv3 = [ uvMap[uvMapIndices[i]].uv.to_3d() for i in ran ] V = barycentric_transform(Puv, uv1, uv2, uv3, p1, p2, p3) out.append(V[:]) Pom.sv_set([out]) if uvV.is_linked: uvV.sv_set([UVMAPV]) uvP.sv_set([UVMAPP])
def process(self): Points, Colors = self.inputs obj = bpy.data.objects[self.object_ref] # triangulate faces bvh = BVHTree.FromObject(obj, bpy.context.scene, deform=True, render=False, cage=False, epsilon=0.0) point = Points.sv_get()[0] color = Colors.sv_get()[0] ran = range(3) image = bpy.data.images[self.image] width, height = image.size uvMap = obj.data.uv_layers[0].data pixels = np.array(image.pixels[:]).reshape(width, height, 4) for P, C in zip(point, safc(point, color)): loc, norm, ind, dist = bvh.find_nearest(P) found_poly = obj.data.polygons[ind] verticesIndices = found_poly.vertices p1, p2, p3 = [ obj.data.vertices[verticesIndices[i]].co for i in ran ] uvMapIndices = found_poly.loop_indices uv1, uv2, uv3 = [uvMap[uvMapIndices[i]].uv.to_3d() for i in ran] V = barycentric_transform(loc, p1, p2, p3, uv1, uv2, uv3) Vx, Vy = int(V.x * (width - 1)), int(V.y * (height - 1)) pixels[Vy, Vx] = C image.pixels = pixels.flatten().tolist()
def __init__(self, location, normal, face_index, distance, field): self.co = location face = field.bm.faces[face_index] verts = [vert.co for vert in face.verts] normals = [vert.normal for vert in field.bm.faces[face_index].verts] curvature = [Vector((0, 0, field.mesh_curvature[vert.index])) for vert in field.bm.faces[face_index].verts] self.normal = geometry.barycentric_transform(location, *verts, *normals).normalized() self.curvature = geometry.barycentric_transform(location, *verts, *curvature).z co = face.verts[0] vec = field.vert_field[co.index].u vecs = [field.vert_field[vert.index].get_nearest_vec(vec) for vert in face.verts] vec = geometry.barycentric_transform(location, *verts, *vecs) self.frame = CrossFrame(vec, self.normal) self.face = face_index
def __init__(self, location, normal, face_index, distance, field): self.co = location face = field.bm.faces[face_index] verts = [vert.co for vert in face.verts] curvature = [Vector((0, 0, field.mesh_curvature[vert.index])) for vert in field.bm.faces[face_index].verts] self.curvature_signed = geometry.barycentric_transform(location, *verts, *curvature).z self.curvature = max(self.curvature_signed, -self.curvature_signed) self.normal = normal try: co = face.verts[0] vec = field.vert_field[co.index].u vecs = [field.vert_field[vert.index].get_nearest_vec(vec) for vert in face.verts] vec = geometry.barycentric_transform(location, *verts, *vecs) except KeyError: vec = Vector((random(), random(), random())).cross(normal).normalized() self.frame = CrossFrame(vec, self.normal) self.face = face_index
def interpolate_tri_2d(self, dst_vert_1, dst_vert_2, dst_vert_3, src_vert_1, src_vert_2, src_vert_3, v): """ Map the provided `v` vertex, considering only two of it's coordinates, from the source triangle (defined by `src_vert_n` vertices) to the face defined by three `dst_vert_n` vertices. """ X, Y = self.get_other_axes() v = self.from2d(v[X], v[Y]) return barycentric_transform(v, src_vert_1, src_vert_2, src_vert_3, dst_vert_1, dst_vert_2, dst_vert_3)
def compute_barycentric_transform_mu(params, result): '''Port to MathUtils barycentric transform function''' tri_src = [V(v) for v in params[2]] tri_dest = [V(v) for v in params[3]] sub_result = [] for vert in params[0]: point = V(vert) new_vert = barycentric_transform(point, tri_src[0], tri_src[1], tri_src[2], tri_dest[0], tri_dest[1], tri_dest[2]) sub_result.append(list(new_vert)) result.append(sub_result)
def get_color_from_geometry(obj, ray_origin, ray_direction, orig_scene=None, location=None, polygon_index=-1): global image_tuples #raycast, or use polygon_index and location if already available if not location or polygon_index == -1: if not orig_scene: dg = bpy.context.evaluated_depsgraph_get() orig_scene = bpy.context.scene.evaluated_get(dg) success, location, normal, polygon_index, object, matrix = orig_scene.ray_cast(bpy.context.view_layer, ray_origin, ray_direction, distance=0.002) if not success: return None # Find the UV map part corresponding to polygon_index slots = obj.material_slots material_index = obj.data.polygons[polygon_index].material_index # if no material exists if material_index >= len(slots) or material_index == -1: return [0.8, 0.8, 0.8] material = slots[obj.data.polygons[polygon_index].material_index].material image = get_material_image(material) # if no texture exists if not image: color = get_material_color(material) return [color[0], color[1], color[2]] # get UV map vertices indices verticesIndices = obj.data.polygons[polygon_index].vertices p1, p2, p3 = [obj.data.vertices[verticesIndices[i]].co for i in range(3)] uvMap = obj.data.uv_layers[obj.data.uv_layers.keys()[0]] uv1, uv2, uv3 = [uvMap.data[obj.data.polygons[polygon_index].loop_indices[i]].uv for i in range(3)] uv1 = Vector((uv1[0], uv1[1], 0)) uv2 = Vector((uv2[0], uv2[1], 0)) uv3 = Vector((uv3[0], uv3[1], 0)) transformed_point = barycentric_transform( location, p1, p2, p3, uv1, uv2, uv3 ) width = image.size[0] height = image.size[1] uv = Vector((transformed_point.x % 1.0, transformed_point.y % 1.0)) coord = ( round((uv[0] % 1.0) * width-1), round((uv[1] % 1.0) * height-1), ) pindex = int(((width * int(coord[1])) + int(coord[0])) * 4) # store images as tuples to avoid recreating the object each loop if image.name not in image_tuples: print('Adding image', image.name) image_tuples[image.name] = tuple(image.pixels) color = image_tuples[image.name][pindex:pindex+4] return [color[0], color[1], color[2]]
def point_to_uv(obj, triangle_index, point, map_name, texture_size): uv_map = obj.data.uv_layers[map_name] vertices_indices = obj.data.polygons[triangle_index].vertices uv_map_indices = obj.data.polygons[triangle_index].loop_indices p1, p2, p3 = [obj.data.vertices[vertices_indices[i]].co for i in range(3)] uv1, uv2, uv3 = [uv_map.data[uv_map_indices[i]].uv for i in range(3)] uv_point = barycentric_transform(point, p1, p2, p3, uv1.to_3d(), uv2.to_3d(), uv3.to_3d()) return uv_point.to_2d() * texture_size
def process(self): Points, Colors = self.inputs obj = bpy.data.objects[self.object_ref] # triangulate faces bvh = BVHTree.FromObject(obj, bpy.context.scene, deform=True, render=False, cage=False, epsilon=0.0) point = Points.sv_get()[0] color = Colors.sv_get()[0] ran = range(3) image = bpy.data.images[self.image] width, height = image.size uvMap = obj.data.uv_layers[0].data pixels = np.array(image.pixels[:]).reshape(width,height,4) for P, C in zip(point, safc(point, color)): loc, norm, ind, dist = bvh.find_nearest(P) found_poly = obj.data.polygons[ind] verticesIndices = found_poly.vertices p1, p2, p3 = [obj.data.vertices[verticesIndices[i]].co for i in ran] uvMapIndices = found_poly.loop_indices uv1, uv2, uv3 = [uvMap[uvMapIndices[i]].uv.to_3d() for i in ran] V = barycentric_transform(loc, p1, p2, p3, uv1, uv2, uv3) Vx, Vy = int(V.x*(width-1)), int(V.y*(height-1)) pixels[Vy, Vx] = C image.pixels = pixels.flatten().tolist()
def process(self): Points = self.inputs[0] Colors = self.outputs[0] if Colors.is_linked: obj = bpy.data.objects[self.object_ref] # triangulate faces bvh = BVHTree.FromObject(obj, bpy.context.scene, deform=True, render=False, cage=False, epsilon=0.0) point = Points.sv_get()[0] outc = [] ran = range(3) image = bpy.data.images[self.image] width, height = image.size pixels = np.array(image.pixels[:]).reshape(width, height, 4) uvMap = obj.data.uv_layers[0].data for P in point: loc, norm, ind, dist = bvh.find_nearest(P) found_poly = obj.data.polygons[ind] verticesIndices = found_poly.vertices p1, p2, p3 = [obj.data.vertices[verticesIndices[i]].co for i in ran] uvMapIndices = found_poly.loop_indices uv1, uv2, uv3 = [uvMap[uvMapIndices[i]].uv.to_3d() for i in ran] V = barycentric_transform(loc, p1, p2, p3, uv1, uv2, uv3) outc.append(pixels[int(V.x*(width-1)), int(V.y*(height-1))].tolist()) Colors.sv_set([outc])
def process(self): PointsUV = self.inputs[0] Pom, uvV, uvP = self.outputs obj = bpy.data.objects[self.object_ref] # triangulate faces UVMAPV, UVMAPP = UV(self,obj) if Pom.is_linked: pointuv = PointsUV.sv_get()[0] bvh = BVHTree.FromPolygons(UVMAPV, UVMAPP, all_triangles=False, epsilon=0.0) ran = range(3) out = [] uvMap = obj.data.uv_layers[0].data for Puv in pointuv: loc, norm, ind, dist = bvh.find_nearest(Puv) found_poly = obj.data.polygons[ind] verticesIndices = found_poly.vertices p1, p2, p3 = [obj.data.vertices[verticesIndices[i]].co for i in ran] uvMapIndices = found_poly.loop_indices uv1, uv2, uv3 = [uvMap[uvMapIndices[i]].uv.to_3d() for i in ran] V = barycentric_transform(Puv, uv1, uv2, uv3, p1, p2, p3) out.append(V[:]) Pom.sv_set([out]) if uvV.is_linked: uvV.sv_set([UVMAPV]) uvP.sv_set([UVMAPP])
def __stroke_apply(self, context, _): sc = context.scene objs = common.get_uv_editable_objects(context) for obj in objs: world_mat = obj.matrix_world bm = bmesh.from_edit_mesh(obj.data) uv_layer = bm.loops.layers.uv.verify() mco = self.current_mco if sc.muv_uv_sculpt_tools == 'GRAB': for info in self.__loop_info[obj]: diff_uv = (mco - self.__initial_mco) * info["strength"] l = bm.faces[info["face_idx"]].loops[info["loop_idx"]] l[uv_layer].uv = info["initial_uv"] + diff_uv / 100.0 elif sc.muv_uv_sculpt_tools == 'PINCH': _, region, space = common.get_space('VIEW_3D', 'WINDOW', 'VIEW_3D') loop_info = [] for f in bm.faces: if not f.select: continue for i, l in enumerate(f.loops): loc_2d = location_3d_to_region_2d_extra( region, space.region_3d, compat.matmul(world_mat, l.vert.co)) diff = loc_2d - self.__initial_mco if diff.length < sc.muv_uv_sculpt_radius: info = { "face_idx": f.index, "loop_idx": i, "initial_vco": l.vert.co.copy(), "initial_vco_2d": loc_2d, "initial_uv": l[uv_layer].uv.copy(), "strength": _get_strength(diff.length, sc.muv_uv_sculpt_radius, sc.muv_uv_sculpt_strength) } loop_info.append(info) # mouse coordinate to UV coordinate ray_vec = view3d_utils.region_2d_to_vector_3d( region, space.region_3d, mco) ray_vec.normalize() ray_orig = view3d_utils.region_2d_to_origin_3d( region, space.region_3d, mco) ray_tgt = ray_orig + ray_vec * 1000000.0 mwi = world_mat.inverted() ray_orig_obj = compat.matmul(mwi, ray_orig) ray_tgt_obj = compat.matmul(mwi, ray_tgt) ray_dir_obj = ray_tgt_obj - ray_orig_obj ray_dir_obj.normalize() tree = BVHTree.FromBMesh(bm) loc, _, fidx, _ = tree.ray_cast(ray_orig_obj, ray_dir_obj) if not loc: return loops = [l for l in bm.faces[fidx].loops] uvs = [ Vector((l[uv_layer].uv.x, l[uv_layer].uv.y, 0.0)) for l in loops ] target_uv = barycentric_transform(loc, loops[0].vert.co, loops[1].vert.co, loops[2].vert.co, uvs[0], uvs[1], uvs[2]) target_uv = Vector((target_uv.x, target_uv.y)) # move to target UV coordinate for info in loop_info: l = bm.faces[info["face_idx"]].loops[info["loop_idx"]] if sc.muv_uv_sculpt_pinch_invert: diff_uv = \ (l[uv_layer].uv - target_uv) * info["strength"] else: diff_uv = \ (target_uv - l[uv_layer].uv) * info["strength"] l[uv_layer].uv = l[uv_layer].uv + diff_uv / 10.0 elif sc.muv_uv_sculpt_tools == 'RELAX': _, region, space = common.get_space('VIEW_3D', 'WINDOW', 'VIEW_3D') # get vertex and loop relation vert_db = {} for f in bm.faces: for l in f.loops: if l.vert in vert_db: vert_db[l.vert]["loops"].append(l) else: vert_db[l.vert] = {"loops": [l]} # get relaxation information for k in vert_db.keys(): d = vert_db[k] d["uv_sum"] = Vector((0.0, 0.0)) d["uv_count"] = 0 for l in d["loops"]: ln = l.link_loop_next lp = l.link_loop_prev d["uv_sum"] = d["uv_sum"] + ln[uv_layer].uv d["uv_sum"] = d["uv_sum"] + lp[uv_layer].uv d["uv_count"] = d["uv_count"] + 2 d["uv_p"] = d["uv_sum"] / d["uv_count"] d["uv_b"] = d["uv_p"] - d["loops"][0][uv_layer].uv for k in vert_db.keys(): d = vert_db[k] d["uv_sum_b"] = Vector((0.0, 0.0)) for l in d["loops"]: ln = l.link_loop_next lp = l.link_loop_prev dn = vert_db[ln.vert] dp = vert_db[lp.vert] d["uv_sum_b"] = d["uv_sum_b"] + dn["uv_b"] + dp["uv_b"] # apply for f in bm.faces: if not f.select: continue for i, l in enumerate(f.loops): loc_2d = location_3d_to_region_2d_extra( region, space.region_3d, compat.matmul(world_mat, l.vert.co)) diff = loc_2d - self.__initial_mco if diff.length >= sc.muv_uv_sculpt_radius: continue db = vert_db[l.vert] strength = _get_strength(diff.length, sc.muv_uv_sculpt_radius, sc.muv_uv_sculpt_strength) base = (1.0 - strength) * l[uv_layer].uv if sc.muv_uv_sculpt_relax_method == 'HC': t = 0.5 * \ (db["uv_b"] + db["uv_sum_b"] / d["uv_count"]) diff = strength * (db["uv_p"] - t) target_uv = base + diff elif sc.muv_uv_sculpt_relax_method == 'LAPLACIAN': diff = strength * db["uv_p"] target_uv = base + diff else: continue l[uv_layer].uv = target_uv bmesh.update_edit_mesh(obj.data)
def _main(self, ob_act, objects, mode='OFFSET', use_clamp=False): def me_nos(verts): return [v.normal.copy() for v in verts] def me_cos(verts): return [v.co.copy() for v in verts] def ob_add_shape(ob, name): me = ob.data key = ob.shape_key_add(from_mix=False) if len(me.shape_keys.key_blocks) == 1: key.name = "Basis" key = ob.shape_key_add(from_mix=False) # we need a rest key.name = name ob.active_shape_key_index = len(me.shape_keys.key_blocks) - 1 ob.show_only_shape_key = True from mathutils.geometry import barycentric_transform from mathutils import Vector if use_clamp and mode == 'OFFSET': use_clamp = False me = ob_act.data orig_key_name = ob_act.active_shape_key.name orig_shape_coords = me_cos(ob_act.active_shape_key.data) orig_normals = me_nos(me.vertices) # actual mesh vertex location isn't as reliable as the base shape :S # orig_coords = me_cos(me.vertices) orig_coords = me_cos(me.shape_keys.key_blocks[0].data) for ob_other in objects: if ob_other.type != 'MESH': self.report({'WARNING'}, ("Skipping '%s', " "not a mesh") % ob_other.name) continue me_other = ob_other.data if len(me_other.vertices) != len(me.vertices): self.report({'WARNING'}, ("Skipping '%s', " "vertex count differs") % ob_other.name) continue target_normals = me_nos(me_other.vertices) if me_other.shape_keys: target_coords = me_cos(me_other.shape_keys.key_blocks[0].data) else: target_coords = me_cos(me_other.vertices) ob_add_shape(ob_other, orig_key_name) # editing the final coords, only list that stores wrapped coords target_shape_coords = [v.co for v in ob_other.active_shape_key.data] median_coords = [[] for i in range(len(me.vertices))] # Method 1, edge if mode == 'OFFSET': for i, vert_cos in enumerate(median_coords): vert_cos.append(target_coords[i] + (orig_shape_coords[i] - orig_coords[i])) elif mode == 'RELATIVE_FACE': for poly in me.polygons: idxs = poly.vertices[:] v_before = idxs[-2] v = idxs[-1] for v_after in idxs: pt = barycentric_transform(orig_shape_coords[v], orig_coords[v_before], orig_coords[v], orig_coords[v_after], target_coords[v_before], target_coords[v], target_coords[v_after], ) median_coords[v].append(pt) v_before = v v = v_after elif mode == 'RELATIVE_EDGE': for ed in me.edges: i1, i2 = ed.vertices v1, v2 = orig_coords[i1], orig_coords[i2] edge_length = (v1 - v2).length n1loc = v1 + orig_normals[i1] * edge_length n2loc = v2 + orig_normals[i2] * edge_length # now get the target nloc's v1_to, v2_to = target_coords[i1], target_coords[i2] edlen_to = (v1_to - v2_to).length n1loc_to = v1_to + target_normals[i1] * edlen_to n2loc_to = v2_to + target_normals[i2] * edlen_to pt = barycentric_transform(orig_shape_coords[i1], v2, v1, n1loc, v2_to, v1_to, n1loc_to) median_coords[i1].append(pt) pt = barycentric_transform(orig_shape_coords[i2], v1, v2, n2loc, v1_to, v2_to, n2loc_to) median_coords[i2].append(pt) # apply the offsets to the new shape from functools import reduce VectorAdd = Vector.__add__ for i, vert_cos in enumerate(median_coords): if vert_cos: co = reduce(VectorAdd, vert_cos) / len(vert_cos) if use_clamp: # clamp to the same movement as the original # breaks copy between different scaled meshes. len_from = (orig_shape_coords[i] - orig_coords[i]).length ofs = co - target_coords[i] ofs.length = len_from co = target_coords[i] + ofs target_shape_coords[i][:] = co return {'FINISHED'}
def _main(self, ob_act, objects, mode='OFFSET', use_clamp=False): def me_nos(verts): return [v.normal.copy() for v in verts] def me_cos(verts): return [v.co.copy() for v in verts] def ob_add_shape(ob, name): me = ob.data key = ob.shape_key_add(from_mix=False) if len(me.shape_keys.key_blocks) == 1: key.name = "Basis" key = ob.shape_key_add(from_mix=False) # we need a rest key.name = name ob.active_shape_key_index = len(me.shape_keys.key_blocks) - 1 ob.show_only_shape_key = True from mathutils.geometry import barycentric_transform from mathutils import Vector if use_clamp and mode == 'OFFSET': use_clamp = False me = ob_act.data orig_key_name = ob_act.active_shape_key.name orig_shape_coords = me_cos(ob_act.active_shape_key.data) orig_normals = me_nos(me.vertices) # actual mesh vertex location isn't as reliable as the base shape :S #~ orig_coords = me_cos(me.vertices) orig_coords = me_cos(me.shape_keys.key_blocks[0].data) for ob_other in objects: me_other = ob_other.data if len(me_other.vertices) != len(me.vertices): self.report({'WARNING'}, ("Skipping '%s', " "vertex count differs") % ob_other.name) continue target_normals = me_nos(me_other.vertices) if me_other.shape_keys: target_coords = me_cos(me_other.shape_keys.key_blocks[0].data) else: target_coords = me_cos(me_other.vertices) ob_add_shape(ob_other, orig_key_name) # editing the final coords, only list that stores wrapped coords target_shape_coords = [v.co for v in ob_other.active_shape_key.data] median_coords = [[] for i in range(len(me.vertices))] # Method 1, edge if mode == 'OFFSET': for i, vert_cos in enumerate(median_coords): vert_cos.append(target_coords[i] + (orig_shape_coords[i] - orig_coords[i])) elif mode == 'RELATIVE_FACE': for poly in me.polygons: idxs = poly.vertices[:] v_before = idxs[-2] v = idxs[-1] for v_after in idxs: pt = barycentric_transform(orig_shape_coords[v], orig_coords[v_before], orig_coords[v], orig_coords[v_after], target_coords[v_before], target_coords[v], target_coords[v_after], ) median_coords[v].append(pt) v_before = v v = v_after elif mode == 'RELATIVE_EDGE': for ed in me.edges: i1, i2 = ed.vertices v1, v2 = orig_coords[i1], orig_coords[i2] edge_length = (v1 - v2).length n1loc = v1 + orig_normals[i1] * edge_length n2loc = v2 + orig_normals[i2] * edge_length # now get the target nloc's v1_to, v2_to = target_coords[i1], target_coords[i2] edlen_to = (v1_to - v2_to).length n1loc_to = v1_to + target_normals[i1] * edlen_to n2loc_to = v2_to + target_normals[i2] * edlen_to pt = barycentric_transform(orig_shape_coords[i1], v2, v1, n1loc, v2_to, v1_to, n1loc_to) median_coords[i1].append(pt) pt = barycentric_transform(orig_shape_coords[i2], v1, v2, n2loc, v1_to, v2_to, n2loc_to) median_coords[i2].append(pt) # apply the offsets to the new shape from functools import reduce VectorAdd = Vector.__add__ for i, vert_cos in enumerate(median_coords): if vert_cos: co = reduce(VectorAdd, vert_cos) / len(vert_cos) if use_clamp: # clamp to the same movement as the original # breaks copy between different scaled meshes. len_from = (orig_shape_coords[i] - orig_coords[i]).length ofs = co - target_coords[i] ofs.length = len_from co = target_coords[i] + ofs target_shape_coords[i][:] = co return {'FINISHED'}
def particle_hair_to_points_with_children(self, context): partsysMod = self.particleObj.particle_systems.active # use last particle_id = self.particleObj.particle_systems.active_index # use last particle_modal_settings = context.active_object.modal_hair.particle_system_modal_settings[ particle_id] # cos using self.particleObj gives random settings... diagonal = self.diagonal particle_count = len(partsysMod.particles) if particle_count == 0: #require more that three strands return self.particle_hair_to_points(context) pointsList_hair = [] coords = [] global PARTICLE_CO #cache strands for drawing strands_len = len(partsysMod.particles[0].hair_keys) - 1 for strand in partsysMod.particles: # for strand point pointsList_hair.append([ hair_key.co for hair_key in strand.hair_keys ]) # DONE: exclude duplicates if first strand[0] in list already if context.active_object.modal_hair.drawStrands: for i, point in enumerate( strand.hair_keys): # create p1,p2, p2,p3, p3,p4 if i == 0 or i == strands_len: coords.append((point.co[0], point.co[1], point.co[2])) else: coords.extend([(point.co[0], point.co[1], point.co[2]), (point.co[0], point.co[1], point.co[2]) ]) else: coords = [(0, 0, 0)] PARTICLE_CO = coords if particle_count == 1: #create two fake strands so that barycentric works pointsList_hair.append([ x.xyz + Vector((0.01 * diagonal, 0, 0)) for x in pointsList_hair[0] ]) pointsList_hair.append([ x.xyz + Vector((0, 0.01 * diagonal, 0)) for x in pointsList_hair[0] ]) elif particle_count == 2: #create one fake strands so that barycentric works pointsList_hair.append([ x.xyz + Vector((0.01 * diagonal, 0, 0)) for x in pointsList_hair[0] ]) #TODO: damm it is slow - 0.3 sec - on 3333 particle mesh (while doing sinterpol is just 0.02 sec) #FIXED: it was here cos particles woudl fail on duplicates.... Fixed by removing duplis in init # pointsList_uniq = [] # [pointsList_uniq.append(x) for x in pointsList_hair if x not in pointsList_uniq] # removing doubles (can cause zero size tris) pointsList_uniq = pointsList_hair # same_point_count cos barycentric transform requires it pointsList = interpol_Catmull_Rom( pointsList_uniq, particle_modal_settings.t_in_y, uniform_spacing=False, same_point_count=True) # just gives smoother result on borders searchDistance = 100 * diagonal parentRoots = [strand[0] for strand in pointsList] # first point of roots # create nnew Part Sytem with uniform points pointsChildRoots = HT_OT_RibbonsFromParticleHairChild.createUniformParticleSystem( context, particle_modal_settings.childCount, particle_modal_settings.PlacementJittering ) # return child part roots positions kd = kdtree.KDTree(len(parentRoots)) for i, root in enumerate(parentRoots): kd.insert(root, i) kd.balance() sourceSurface_BVHT = self.sourceSurface_BVHT childStrandsPoints = [] # will contain strands with child points childStrandRootNormals = [] seed(a=particle_modal_settings.lenSeed, version=2) for i, childRoot in enumerate( pointsChildRoots ): # for each child find it three parents and genereate strands by barycentric transform snappedPoint, normalChildRoot, rootHitIndex, distance = sourceSurface_BVHT.find_nearest( childRoot, searchDistance) childStrandRootNormals.append(normalChildRoot) threeClosestParentRoots = kd.find_n( childRoot, 3) # find three closes parent roots rootTri_co, ParentRootIndices, distances = zip( *threeClosestParentRoots) # split it into 3 arrays sourceTri_BVHT = BVHTree.FromPolygons( rootTri_co, [(0, 1, 2)], all_triangles=True) # [0,1,2] - polygon == vert indices list childRootSnapped, normalChildProjected, index, distance = sourceTri_BVHT.find_nearest( childRoot, searchDistance ) # snap generated child to parent triangle ares \normals are sometimes flipped childRootSnapped2, normalChildProjected2, index2, distance2 = sourceSurface_BVHT.find_nearest( childRootSnapped, searchDistance) # this gives ok normals always lenWeight = 1 + uniform( -particle_modal_settings.RandomizeLengthMinus, particle_modal_settings.RandomizeLengthPlus) rotQuat = normalChildProjected2.rotation_difference( normalChildRoot) translationMatrix = Matrix.Translation(childRoot) rotMatrixRot = rotQuat.to_matrix().to_4x4() mat_sca = Matrix.Scale(lenWeight, 4) transformMatrix = translationMatrix @ rotMatrixRot strandPoints = [] # for childRootSnapped points transform them from parent root triangles to parent next segment triangle t1,t2,t3 # and compensate child snapping to root triangle from before for j, (t1, t2, t3) in enumerate( zip(pointsList[ParentRootIndices[0]], pointsList[ParentRootIndices[1]], pointsList[ParentRootIndices[2]])): pointTransformed = barycentric_transform( childRootSnapped, rootTri_co[0], rootTri_co[1], rootTri_co[2], Vector(t1), Vector(t2), Vector(t3)) childInterpolatedPoint = transformMatrix @ mat_sca @ ( pointTransformed - childRootSnapped ) # rotate child strand to original pos (from before snapt) strandPoints.append(childInterpolatedPoint) childStrandsPoints.append(strandPoints) if particle_modal_settings.use_parent_strands: childStrandsPoints.extend(pointsList) return childStrandsPoints
def execute(self, context): depsgraph = bpy.context.depsgraph ob = bpy.context.active_object obj_eval = depsgraph.objects.get(ob.name, None) # particleObj = context.active_object particleObj = obj_eval if bpy.context.active_object.particle_systems is None: # create new one self.report({'INFO'}, 'No active Particle Hair System found!') return {"CANCELLED"} index = particleObj.particle_systems.active_index psys_active = particleObj.particle_systems[index] if psys_active.settings.type != 'HAIR': # create new one self.report({'INFO'}, 'Active Particle System is not Hair type! Cancelling') return {"CANCELLED"} pointsList_hair = [] context.scene.update() if len(psys_active.particles) == 0: # require more that three strands self.report({'INFO'}, 'Active Particle System has zero strands! Cancelling') return {"CANCELLED"} diagonal = sqrt( pow(particleObj.dimensions[0], 2) + pow(particleObj.dimensions[1], 2) + pow(particleObj.dimensions[2], 2)) # to normalize some values for particle in psys_active.particles: # for strand point pointsList_hair.append([ hair_key.co for hair_key in particle.hair_keys ]) # DONE: exclude duplicates if first strand[0] in list already if len(psys_active.particles ) == 1: #create two fake strands so that barycentric works pointsList_hair.append([ x.xyz + Vector((0.01 * diagonal, 0, 0)) for x in pointsList_hair[0] ]) pointsList_hair.append([ x.xyz + Vector((0, 0.01 * diagonal, 0)) for x in pointsList_hair[0] ]) elif len(psys_active.particles ) == 2: #create one fake strands so that barycentric works pointsList_hair.append([ x.xyz + Vector((0.01 * diagonal, 0, 0)) for x in pointsList_hair[0] ]) pointsList_uniq = [] [ pointsList_uniq.append(x) for x in pointsList_hair if x not in pointsList_uniq ] #removing doubles (can cause zero size tris) #same_point_count cos barycentric transform requires it pointsList = interpol_Catmull_Rom( pointsList_uniq, self.t_in_y, uniform_spacing=True, same_point_count=True) # just gives smoother result on borders searchDistance = 100 * diagonal parentRoots = [strand[0] for strand in pointsList] # first point of roots #create nnew Part Sytem with uniform points pointsChildRoots = self.createUniformParticleSystem( context, self.childCount, self.PlacementJittering, self.Seed) # return child part roots positions kd = kdtree.KDTree(len(parentRoots)) for i, root in enumerate(parentRoots): kd.insert(root, i) kd.balance() sourceSurface_BVHT = BVHTree.FromObject(particleObj, context.depsgraph) childStrandsPoints = [] #will contain strands with child points childStrandRootNormals = [] length_ver_group_index = -1 vertex_group_length_name = psys_active.vertex_group_length if vertex_group_length_name: # calc weight based on root point length_ver_group_index = particleObj.vertex_groups[ vertex_group_length_name].index particleObjMesh = particleObj.to_mesh(context.depsgraph, apply_modifiers=True, calc_undeformed=False) seed(a=self.lenSeed, version=2) embed = self.embed * 0.04 * diagonal cpow = calc_power(self.noiseFalloff) cpowClump = calc_power(self.ClumpingFalloff) noiseFalloff = [pow(i / self.t_in_y, cpow) for i in range(self.t_in_y)] ClumpFalloff = [ pow((i + 1) / self.t_in_y, cpowClump) for i in range(self.t_in_y) ] for i, childRoot in enumerate( pointsChildRoots ): #for each child find it three parents and genereate strands by barycentric transform snappedPoint, normalChildRoot, rootHitIndex, distance = sourceSurface_BVHT.find_nearest( childRoot, searchDistance) childStrandRootNormals.append(normalChildRoot) threeClosestParentRoots = kd.find_n( childRoot, 3) #find three closes parent roots rootTri_co, ParentRootIndices, distances = zip( *threeClosestParentRoots) #split it into 3 arrays sourceTri_BVHT = BVHTree.FromPolygons( rootTri_co, [(0, 1, 2)], all_triangles=True) # [0,1,2] - polygon == vert indices list childRootSnapped, normalChildProjected, index, distance = sourceTri_BVHT.find_nearest( childRoot, searchDistance ) #snap generated child to parent triangle ares \normals are sometimes flipped childRootSnapped2, normalChildProjected2, index2, distance2 = sourceSurface_BVHT.find_nearest( childRootSnapped, searchDistance) #this gives ok normals always lenWeight = 1 if length_ver_group_index != -1: # if vg exist averageWeight = 0 for vertIndex in particleObjMesh.polygons[ rootHitIndex].vertices: #DONE: check if work on mesh with modifiers for group in particleObjMesh.vertices[vertIndex].groups: if group.group == length_ver_group_index: averageWeight += group.weight break lenWeight = averageWeight / len( particleObjMesh.polygons[rootHitIndex].vertices) ranLen = uniform(-self.RandomizeLengthMinus, self.RandomizeLengthPlus) lenWeight *= (1 + ranLen) # diff = childRoot - childRootSnapped # mat_loc = Matrix.Translation(childRootSnapped) # matTriangleSpaceInv = mat_loc #* rotMatrix # matTriangleSpaceInv.invert() rotQuat = normalChildProjected2.rotation_difference( normalChildRoot) translationMatrix = Matrix.Translation(childRoot) rotMatrixRot = rotQuat.to_matrix().to_4x4() mat_sca = Matrix.Scale(lenWeight, 4) transformMatrix = translationMatrix @ rotMatrixRot strandPoints = [] #for childRootSnapped points transform them from parent root triangles to parent next segment triangle t1,t2,t3 # and compensate child snapping to root triangle from before for j, (t1, t2, t3) in enumerate( zip(pointsList[ParentRootIndices[0]], pointsList[ParentRootIndices[1]], pointsList[ParentRootIndices[2]])): pointTransformed = barycentric_transform( childRootSnapped, rootTri_co[0], rootTri_co[1], rootTri_co[2], Vector(t1), Vector(t2), Vector(t3)) childInterpolatedPoint = transformMatrix @ mat_sca @ ( pointTransformed - childRootSnapped ) #rotate child strand to original pos (from before snapt) #do noise noise.seed_set(self.Seed + i) # add seed per strand/ring ? noiseVectorPerStrand = noise.noise_vector( childInterpolatedPoint * self.freq / diagonal, noise_basis='PERLIN_ORIGINAL' ) * noiseFalloff[j] * self.noiseAmplitude * diagonal / 10 # childInterpolatedPoint += noiseVectorPerStrand #do clumping diff = Vector( t1 ) - childInterpolatedPoint # calculate distance to parent strand (first strand from trio) # point += noiseVectorPerStrand * noiseFalloff[j] * self.noiseAmplitude * diagonal / 10 # childClumped = childInterpolatedPoint + ClumpFalloff[j] * self.Clumping * diff + noiseVectorPerStrand * (1-ClumpFalloff[j]) childClumped = childInterpolatedPoint + ClumpFalloff[ j] * self.Clumping * diff + noiseVectorPerStrand * ( 1 - ClumpFalloff[j] * self.Clumping) # childClumped = childInterpolatedPoint + noiseVectorPerStrand strandPoints.append(childClumped) # embeding roots diff = strandPoints[0] - strandPoints[1] diff.normalize() normalWeight = abs(diff.dot(normalChildRoot)) strandPoints[0] += ( diff * normalWeight - normalChildRoot * (1 - normalWeight) ) * embed # do childStrandRootNormal to move it more into mesh surface childStrandsPoints.append(strandPoints) bpy.data.meshes.remove(particleObjMesh) # create the Curve Datablock curveData = bpy.data.curves.new(particleObj.name + '_curve', type='CURVE') splinePointsNp = np.array(childStrandsPoints, dtype=np.float32) if self.hairType != 'BEZIER': splinePointsNpOnes = np.ones( (len(childStrandsPoints), self.t_in_y, 4), dtype=np.float32) # 4 coord x,y,z ,1 splinePointsNpOnes[:, :, :-1] = splinePointsNp splinePointsNp = splinePointsNpOnes for strandPoints in splinePointsNp: # for strand point curveLength = len(strandPoints) polyline = curveData.splines.new(self.hairType) if self.hairType == 'BEZIER': polyline.bezier_points.add(curveLength - 1) elif self.hairType == 'POLY' or self.hairType == 'NURBS': polyline.points.add(curveLength - 1) if self.hairType == 'NURBS': polyline.order_u = 3 # like bezier thing polyline.use_endpoint_u = True if self.hairType == 'BEZIER': # polyline.bezier_points.co = (x, y, z) polyline.bezier_points.foreach_set("co", strandPoints.ravel()) polyline.bezier_points.foreach_set('handle_left_type', 'AUTO') polyline.bezier_points.foreach_set('handle_right_type', 'AUTO') else: polyline.points.foreach_set("co", strandPoints.ravel()) # polyline.points[i].co = (x, y, z, 1) curveData.resolution_u = self.strandResU curveData.dimensions = '3D' # create Object curveOB = bpy.data.objects.new(particleObj.name + '_curve', curveData) curveOB.matrix_world = particleObj.matrix_world scn = context.scene scn.collection.objects.link(curveOB) curveOB.targetObjPointer = particleObj.name # store source surface for snapping oper context.view_layer.objects.active = curveOB curveOB.select_set(True) # curveOB.data.show_normal_face = False if self.generateRibbons: bpy.ops.object.generate_ribbons(strandResU=self.strandResU, strandResV=self.strandResV, strandWidth=self.strandWidth, strandPeak=self.strandPeak, strandUplift=self.strandUplift, alignToSurface=self.alignToSurface) HT_OT_CurvesUVRefresh.uvCurveRefresh(curveOB) context.view_layer.objects.active = particleObj else: curveData.fill_mode = 'FULL' curveData.bevel_depth = 0.004 * diagonal curveData.bevel_resolution = 2 bpy.ops.object.curve_taper(TipRadiusFalloff=self.RadiusFalloff, TipRadius=self.TipRadius, MainRadius=self.Radius) return {"FINISHED"}
def __stroke_apply(self, context, _): sc = context.scene obj = context.active_object world_mat = obj.matrix_world bm = bmesh.from_edit_mesh(obj.data) uv_layer = bm.loops.layers.uv.verify() mco = self.current_mco if sc.muv_uv_sculpt_tools == 'GRAB': for info in self.__loop_info: diff_uv = (mco - self.__initial_mco) * info["strength"] l = bm.faces[info["face_idx"]].loops[info["loop_idx"]] l[uv_layer].uv = info["initial_uv"] + diff_uv / 100.0 elif sc.muv_uv_sculpt_tools == 'PINCH': _, region, space = common.get_space('VIEW_3D', 'WINDOW', 'VIEW_3D') loop_info = [] for f in bm.faces: if not f.select: continue for i, l in enumerate(f.loops): loc_2d = view3d_utils.location_3d_to_region_2d( region, space.region_3d, compat.matmul(world_mat, l.vert.co)) diff = loc_2d - self.__initial_mco if diff.length < sc.muv_uv_sculpt_radius: info = { "face_idx": f.index, "loop_idx": i, "initial_vco": l.vert.co.copy(), "initial_vco_2d": loc_2d, "initial_uv": l[uv_layer].uv.copy(), "strength": _get_strength( diff.length, sc.muv_uv_sculpt_radius, sc.muv_uv_sculpt_strength) } loop_info.append(info) # mouse coordinate to UV coordinate ray_vec = view3d_utils.region_2d_to_vector_3d(region, space.region_3d, mco) ray_vec.normalize() ray_orig = view3d_utils.region_2d_to_origin_3d(region, space.region_3d, mco) ray_tgt = ray_orig + ray_vec * 1000000.0 mwi = world_mat.inverted() ray_orig_obj = compat.matmul(mwi, ray_orig) ray_tgt_obj = compat.matmul(mwi, ray_tgt) ray_dir_obj = ray_tgt_obj - ray_orig_obj ray_dir_obj.normalize() tree = BVHTree.FromBMesh(bm) loc, _, fidx, _ = tree.ray_cast(ray_orig_obj, ray_dir_obj) if not loc: return loops = [l for l in bm.faces[fidx].loops] uvs = [Vector((l[uv_layer].uv.x, l[uv_layer].uv.y, 0.0)) for l in loops] target_uv = barycentric_transform( loc, loops[0].vert.co, loops[1].vert.co, loops[2].vert.co, uvs[0], uvs[1], uvs[2]) target_uv = Vector((target_uv.x, target_uv.y)) # move to target UV coordinate for info in loop_info: l = bm.faces[info["face_idx"]].loops[info["loop_idx"]] if sc.muv_uv_sculpt_pinch_invert: diff_uv = (l[uv_layer].uv - target_uv) * info["strength"] else: diff_uv = (target_uv - l[uv_layer].uv) * info["strength"] l[uv_layer].uv = l[uv_layer].uv + diff_uv / 10.0 elif sc.muv_uv_sculpt_tools == 'RELAX': _, region, space = common.get_space('VIEW_3D', 'WINDOW', 'VIEW_3D') # get vertex and loop relation vert_db = {} for f in bm.faces: for l in f.loops: if l.vert in vert_db: vert_db[l.vert]["loops"].append(l) else: vert_db[l.vert] = {"loops": [l]} # get relaxation information for k in vert_db.keys(): d = vert_db[k] d["uv_sum"] = Vector((0.0, 0.0)) d["uv_count"] = 0 for l in d["loops"]: ln = l.link_loop_next lp = l.link_loop_prev d["uv_sum"] = d["uv_sum"] + ln[uv_layer].uv d["uv_sum"] = d["uv_sum"] + lp[uv_layer].uv d["uv_count"] = d["uv_count"] + 2 d["uv_p"] = d["uv_sum"] / d["uv_count"] d["uv_b"] = d["uv_p"] - d["loops"][0][uv_layer].uv for k in vert_db.keys(): d = vert_db[k] d["uv_sum_b"] = Vector((0.0, 0.0)) for l in d["loops"]: ln = l.link_loop_next lp = l.link_loop_prev dn = vert_db[ln.vert] dp = vert_db[lp.vert] d["uv_sum_b"] = d["uv_sum_b"] + dn["uv_b"] + dp["uv_b"] # apply for f in bm.faces: if not f.select: continue for i, l in enumerate(f.loops): loc_2d = view3d_utils.location_3d_to_region_2d( region, space.region_3d, compat.matmul(world_mat, l.vert.co)) diff = loc_2d - self.__initial_mco if diff.length >= sc.muv_uv_sculpt_radius: continue db = vert_db[l.vert] strength = _get_strength(diff.length, sc.muv_uv_sculpt_radius, sc.muv_uv_sculpt_strength) base = (1.0 - strength) * l[uv_layer].uv if sc.muv_uv_sculpt_relax_method == 'HC': t = 0.5 * (db["uv_b"] + db["uv_sum_b"] / d["uv_count"]) diff = strength * (db["uv_p"] - t) target_uv = base + diff elif sc.muv_uv_sculpt_relax_method == 'LAPLACIAN': diff = strength * db["uv_p"] target_uv = base + diff else: continue l[uv_layer].uv = target_uv bmesh.update_edit_mesh(obj.data)