From 7f54f40b725294f9c40aec1df3777169739c81df Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sun, 3 Jan 2021 19:33:58 +0500 Subject: [PATCH 1/5] "Relax mesh" node. --- index.md | 1 + nodes/modifier_change/relax_mesh.py | 162 ++++++++++++++++++ nodes/modifier_change/triangulate.py | 3 + utils/relax_mesh.py | 235 +++++++++++++++++++++++++++ 4 files changed, 401 insertions(+) create mode 100644 nodes/modifier_change/relax_mesh.py create mode 100644 utils/relax_mesh.py diff --git a/index.md b/index.md index 276efbbb4..68b2a1d73 100644 --- a/index.md +++ b/index.md @@ -470,6 +470,7 @@ SvInsetFaces SvLatheNode SvSmoothNode + SvRelaxMeshNode SvSmoothLines --- CrossSectionNode diff --git a/nodes/modifier_change/relax_mesh.py b/nodes/modifier_change/relax_mesh.py new file mode 100644 index 000000000..3c5cb9836 --- /dev/null +++ b/nodes/modifier_change/relax_mesh.py @@ -0,0 +1,162 @@ +# This file is part of project Sverchok. It's copyrighted by the contributors +# recorded in the version control history of the file, available from +# its original location https://github.com/nortikin/sverchok/commit/master +# +# SPDX-License-Identifier: GPL3 +# License-Filename: LICENSE + +import bpy +from bpy.props import IntProperty, FloatProperty, BoolProperty, EnumProperty + +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, zip_long_repeat, throttle_and_update_node, get_data_nesting_level, ensure_nesting_level +from sverchok.utils.relax_mesh import * + +class SvRelaxMeshNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Relax Mesh + Tooltip: Relax mesh + """ + bl_idname = 'SvRelaxMeshNode' + bl_label = 'Relax Mesh' + bl_icon = 'MOD_SMOOTH' + + iterations: IntProperty( + name="Iterations", + min=0, + max=1000, default=1, update=updateNode) + + factor: FloatProperty( + name="Factor", + description="Smoothing factor", + min=0.0, + max=1.0, + default=0.5, + update=updateNode) + + @throttle_and_update_node + def update_sockets(self, context): + self.inputs['Factor'].hide_safe = self.algorithm not in {'EDGES', 'FACES'} + + algorithms = [ + ('LLOYD', "Lloyd", "Lloyd", 0), + ('EDGES', "Edges", "Edges", 1), + ('FACES', "Faces", "Faces", 2) + ] + + algorithm : EnumProperty( + name = "Algorithm", + items = algorithms, + default = 'LLOYD', + update = update_sockets) + + def get_available_methods(self, context): + items = [] + if self.algorithm in {'EDGES', 'FACES'}: + items.append((NONE, "Do not use", "Do not use", 0)) + if self.algorithm == 'LLOYD': + items.append((LINEAR, "Linear", "Linear", 1)) + items.append((NORMAL, "Normal", "Move points along mesh tangent only", 2)) + items.append((BVH, "BVH", "Use BVH Tree", 3)) + return items + + preserve_shape : EnumProperty( + name = "Preserve shape", + items = get_available_methods, + update = updateNode) + + skip_bounds : BoolProperty( + name = "Skip bounds", + description = "Leave boundary vertices where they were", + default = True, + update = updateNode) + + targets = [ + (AVERAGE, "Average", "Average", 0), + (MINIMUM, "Minimum", "Minimum", 1), + (MAXIMUM, "Maximum", "Maximum", 2) + ] + + target : EnumProperty( + name = "Target", + items = targets, + default = AVERAGE, + update = updateNode) + + def sv_init(self, context): + self.inputs.new('SvVerticesSocket', "Vertices") + self.inputs.new('SvStringsSocket', 'Edges') + self.inputs.new('SvStringsSocket', 'Faces') + self.inputs.new('SvStringsSocket', 'VertMask') + self.inputs.new('SvStringsSocket', 'Iterations').prop_name = "iterations" + self.inputs.new('SvStringsSocket', 'Factor').prop_name = "factor" + + self.outputs.new('SvVerticesSocket', 'Vertices') + #self.outputs.new('SvStringsSocket', 'Edges') + #self.outputs.new('SvStringsSocket', 'Faces') + + self.update_sockets(context) + + def draw_buttons(self, context, layout): + layout.prop(self, 'algorithm') + if self.algorithm in {'EDGES', 'FACES'}: + layout.prop(self, 'target') + layout.prop(self, 'preserve_shape') + layout.prop(self, 'skip_bounds') + + def process(self): + if not any(output.is_linked for output in self.outputs): + return + + vertices_s = self.inputs['Vertices'].sv_get(deepcopy=False) + edges_s = self.inputs['Edges'].sv_get(default=[[]], deepcopy=False) + faces_s = self.inputs['Faces'].sv_get(default=[[]], deepcopy=False) + masks_s = self.inputs['VertMask'].sv_get(default=[[1]], deepcopy=False) + iterations_s = self.inputs['Iterations'].sv_get(deepcopy=False) + factor_s = self.inputs['Factor'].sv_get(deepcopy=False) + + input_level = get_data_nesting_level(vertices_s) + vertices_s = ensure_nesting_level(vertices_s, 4) + edges_s = ensure_nesting_level(edges_s, 4) + faces_s = ensure_nesting_level(faces_s, 4) + masks_s = ensure_nesting_level(masks_s, 3) + iterations_s = ensure_nesting_level(iterations_s, 2) + factor_s = ensure_nesting_level(factor_s, 2) + + nested_output = input_level > 3 + + verts_out = [] + for params in zip_long_repeat(vertices_s, edges_s, faces_s, masks_s, iterations_s, factor_s): + for vertices, edges, faces, mask, iterations, factor in zip_long_repeat(*params): + if self.algorithm == 'LLOYD': + vertices = lloyd_relax(vertices, faces, iterations, + mask = mask, + method = self.preserve_shape, + skip_boundary = self.skip_bounds) + elif self.algorithm == 'EDGES': + vertices = edges_relax(vertices, edges, faces, iterations, + k = factor, + mask = mask, + method = self.preserve_shape, + target = self.target, + skip_boundary = self.skip_bounds) + elif self.algorithm == 'FACES': + vertices = faces_relax(vertices, edges, faces, iterations, + k = factor, + mask = mask, + method = self.preserve_shape, + target = self.target, + skip_boundary = self.skip_bounds) + else: + raise Exception("Unsupported algorithm") + + verts_out.append(vertices) + + self.outputs['Vertices'].sv_set(verts_out) + +def register(): + bpy.utils.register_class(SvRelaxMeshNode) + +def unregister(): + bpy.utils.unregister_class(SvRelaxMeshNode) + diff --git a/nodes/modifier_change/triangulate.py b/nodes/modifier_change/triangulate.py index a8eb908a4..87d51cb9b 100644 --- a/nodes/modifier_change/triangulate.py +++ b/nodes/modifier_change/triangulate.py @@ -22,6 +22,7 @@ import bmesh.ops from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode, match_long_repeat, repeat_last_for_length +from sverchok.utils.sv_mesh_utils import get_unique_faces from sverchok.utils.sv_bmesh_utils import bmesh_from_pydata, pydata_from_bmesh @@ -125,6 +126,8 @@ class SvTriangulateNode(bpy.types.Node, SverchCustomTreeNode): new_face_data = [] bm.free() + new_faces = get_unique_faces(new_faces) + result_vertices.append(new_vertices) result_edges.append(new_edges) result_faces.append(new_faces) diff --git a/utils/relax_mesh.py b/utils/relax_mesh.py new file mode 100644 index 000000000..2cc31ae6f --- /dev/null +++ b/utils/relax_mesh.py @@ -0,0 +1,235 @@ +# This file is part of project Sverchok. It's copyrighted by the contributors +# recorded in the version control history of the file, available from +# its original location https://github.com/nortikin/sverchok/commit/master +# +# SPDX-License-Identifier: GPL3 +# License-Filename: LICENSE + +import numpy as np +from collections import defaultdict +from math import sqrt + +import bmesh +import mathutils +from mathutils.bvhtree import BVHTree + +from sverchok.data_structure import repeat_last_for_length +from sverchok.utils.sv_mesh_utils import polygons_to_edges +from sverchok.utils.sv_bmesh_utils import pydata_from_bmesh, bmesh_from_pydata +from sverchok.utils.geom import center, linear_approximation + +NONE = 'NONE' +BVH = 'BVH' +LINEAR = 'LINEAR' +NORMAL = 'NORMAL' + +MINIMUM = 'MIN' +MAXIMUM = 'MAX' +AVERAGE = 'MEAN' + +def lloyd_relax(vertices, faces, iterations, mask=None, method=NORMAL, skip_boundary=True): + """ + supported shape preservation methods: NORMAL, LINEAR, BVH + """ + + def do_iteration(bvh, bm): + verts_out = [] + face_centers = np.array([face.calc_center_median() for face in bm.faces]) + for bm_vert in bm.verts: + co = bm_vert.co + if (skip_boundary and bm_vert.is_boundary) or (mask is not None and not mask[bm_vert.index]): + new_vert = tuple(co) + else: + normal = bm_vert.normal + cs = np.array([face_centers[face.index] for face in bm_vert.link_faces]) + + if method == NORMAL: + median = mathutils.Vector(cs.mean(axis=0)) + dv = median - co + dv = dv - dv.project(normal) + new_vert = co + dv + elif method == LINEAR: + approx = linear_approximation(cs) + median = mathutils.Vector(approx.center) + plane = approx.most_similar_plane() + dist = plane.distance_to_point(bm_vert.co) + new_vert = median + plane.normal.normalized() * dist + elif method == BVH: + median = mathutils.Vector(cs.mean(axis=0)) + new_vert, normal, idx, dist = bvh.find_nearest(median) + else: + raise Exception("Unsupported volume preservation method") + + new_vert = tuple(new_vert) + + verts_out.append(new_vert) + + return verts_out + + if mask is not None: + mask = repeat_last_for_length(mask, len(vertices)) + + bvh = BVHTree.FromPolygons(vertices, faces) + for i in range(iterations): + bm = bmesh_from_pydata(vertices, [], faces, normal_update=True) + vertices = do_iteration(bvh, bm) + bm.free() + + return vertices + +def edges_relax(vertices, edges, faces, iterations, k, mask=None, method=NONE, target=AVERAGE, skip_boundary=True): + """ + supported shape preservation methods: NONE, NORMAL, BVH + """ + + def do_iteration(bvh, bm, verts): + verts = np.asarray(verts) + v1s = verts[edges[:,0]] + v2s = verts[edges[:,1]] + edge_vecs = v2s - v1s + edge_lens = np.linalg.norm(edge_vecs, axis=1) + + if target == MINIMUM: + target_len = np.min(edge_lens) + elif target == MAXIMUM: + target_len = np.max(edge_lens) + elif target == AVERAGE: + target_len = np.mean(edge_lens) + else: + raise Exception("Unsupported target edge length type") + + forces = defaultdict(lambda: np.zeros((3,))) + counts = defaultdict(int) + for edge_idx, (v1_idx, v2_idx) in enumerate(edges): + edge_vec = edge_vecs[edge_idx] + edge_len = edge_lens[edge_idx] + d_len = (edge_len - target_len)/2.0 + dv1 = d_len * edge_vec + dv2 = - d_len * edge_vec + forces[v1_idx] += dv1 + forces[v2_idx] += dv2 + counts[v1_idx] += 1 + counts[v2_idx] += 1 + + target_verts = verts.copy() + for v_idx in range(len(verts)): + if skip_boundary and bm.verts[v_idx].is_boundary: + continue + if mask is not None and not mask[v_idx]: + continue + count = counts[v_idx] + if count: + forces[v_idx] /= count + target_verts[v_idx] += k*forces[v_idx] + + if method == NONE: + verts_out = target_verts.tolist() + elif method == NORMAL: + verts_out = [] + for bm_vert in bm.verts: + normal = bm_vert.normal + dv = mathutils.Vector(target_verts[bm_vert.index]) - bm_vert.co + dv = dv - dv.project(normal) + new_vert = tuple(bm_vert.co + dv) + verts_out.append(new_vert) + elif method == BVH: + verts_out = [] + for vert in target_verts: + new_vert, normal, idx, dist = bvh.find_nearest(vert) + verts_out.append(tuple(new_vert)) + else: + raise Exception("Unsupported shape preservation method") + + return verts_out + + if not edges: + edges = polygons_to_edges([faces], unique_edges=True)[0] + edges = np.array(edges) + if mask is not None: + mask = repeat_last_for_length(mask, len(vertices)) + bvh = BVHTree.FromPolygons(vertices, faces) + for i in range(iterations): + bm = bmesh_from_pydata(vertices, edges, faces, normal_update=True) + vertices = do_iteration(bvh, bm, vertices) + bm.free() + + return vertices + +def faces_relax(vertices, edges, faces, iterations, k, mask=None, method=NONE, target=AVERAGE, skip_boundary=True): + """ + supported shape preservation methods: NONE, NORMAL, BVH + """ + + def do_iteration(bvh, bm): + areas = np.array([face.calc_area() for face in bm.faces]) + vert_cos = np.array([tuple(vert.co) for vert in bm.verts]) + if target == MINIMUM: + target_area = areas.min() + elif target == MAXIMUM: + target_area = areas.max() + elif target == AVERAGE: + target_area = areas.mean() + else: + raise Exception("Unsupported target face area type") + + forces = defaultdict(lambda: np.zeros((3,))) + counts = defaultdict(int) + for bm_face in bm.faces: + face_vert_idxs = [vert.index for vert in bm_face.verts] + face_verts = vert_cos[face_vert_idxs] + mean = face_verts.mean(axis=0) + face_verts_0 = face_verts - mean + src_area = areas[bm_face.index] + scale = sqrt(target_area / src_area) + dvs = (scale - 1) * face_verts_0 + for vert_idx, dv in zip(face_vert_idxs, dvs): + forces[vert_idx] += dv + counts[vert_idx] += 1 + + target_verts = vert_cos.copy() + for bm_vert in bm.verts: + idx = bm_vert.index + if skip_boundary and bm_vert.is_boundary: + continue + if mask is not None and not mask[idx]: + continue + count = counts[idx] + if count: + forces[idx] /= count + force = forces[idx] + target_verts[idx] += k*force + + if method == NONE: + verts_out = target_verts.tolist() + elif method == NORMAL: + verts_out = [] + for bm_vert in bm.verts: + idx = bm_vert.index + dv = mathutils.Vector(target_verts[idx]) - bm_vert.co + normal = bm_vert.normal + dv = dv - dv.project(normal) + new_vert = tuple(bm_vert.co + dv) + verts_out.append(new_vert) + + elif method == BVH: + verts_out = [] + for bm_vert in bm.verts: + new_vert, normal, idx, dist = bvh.find_nearest(bm_vert.co) + verts_out.append(tuple(new_vert)) + + else: + raise Exception("Unsupported shape preservation method") + + return verts_out + + if mask is not None: + mask = repeat_last_for_length(mask, len(vertices)) + + bvh = BVHTree.FromPolygons(vertices, faces) + for i in range(iterations): + bm = bmesh_from_pydata(vertices, edges, faces, normal_update=True) + vertices = do_iteration(bvh, bm) + bm.free() + + return vertices + -- GitLab From 6ff88b9d9ec5b58ff18981e2e7edb93c907be71b Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Mon, 4 Jan 2021 12:09:17 +0500 Subject: [PATCH 2/5] Update. --- nodes/modifier_change/relax_mesh.py | 42 +++++++++++++++++++++++------ utils/relax_mesh.py | 42 +++++++++++++++++++++++------ 2 files changed, 68 insertions(+), 16 deletions(-) diff --git a/nodes/modifier_change/relax_mesh.py b/nodes/modifier_change/relax_mesh.py index 3c5cb9836..b355e2c8c 100644 --- a/nodes/modifier_change/relax_mesh.py +++ b/nodes/modifier_change/relax_mesh.py @@ -40,8 +40,8 @@ class SvRelaxMeshNode(bpy.types.Node, SverchCustomTreeNode): algorithms = [ ('LLOYD', "Lloyd", "Lloyd", 0), - ('EDGES', "Edges", "Edges", 1), - ('FACES', "Faces", "Faces", 2) + ('EDGES', "Edge Lengths", "Try to make all edges of the same length", 1), + ('FACES', "Face Areas", "Try to make all faces of the same area", 2) ] algorithm : EnumProperty( @@ -52,8 +52,7 @@ class SvRelaxMeshNode(bpy.types.Node, SverchCustomTreeNode): def get_available_methods(self, context): items = [] - if self.algorithm in {'EDGES', 'FACES'}: - items.append((NONE, "Do not use", "Do not use", 0)) + items.append((NONE, "Do not use", "Do not use", 0)) if self.algorithm == 'LLOYD': items.append((LINEAR, "Linear", "Linear", 1)) items.append((NORMAL, "Normal", "Move points along mesh tangent only", 2)) @@ -83,6 +82,18 @@ class SvRelaxMeshNode(bpy.types.Node, SverchCustomTreeNode): default = AVERAGE, update = updateNode) + use_x: BoolProperty( + name="X", description="smooth vertices along X axis", + default=True, update=updateNode) + + use_y: BoolProperty( + name="Y", description="smooth vertices along Y axis", + default=True, update=updateNode) + + use_z: BoolProperty( + name="Z", description="smooth vertices along Z axis", + default=True, update=updateNode) + def sv_init(self, context): self.inputs.new('SvVerticesSocket', "Vertices") self.inputs.new('SvStringsSocket', 'Edges') @@ -103,6 +114,10 @@ class SvRelaxMeshNode(bpy.types.Node, SverchCustomTreeNode): layout.prop(self, 'target') layout.prop(self, 'preserve_shape') layout.prop(self, 'skip_bounds') + row = layout.row(align=True) + row.prop(self, "use_x", toggle=True) + row.prop(self, "use_y", toggle=True) + row.prop(self, "use_z", toggle=True) def process(self): if not any(output.is_linked for output in self.outputs): @@ -110,7 +125,7 @@ class SvRelaxMeshNode(bpy.types.Node, SverchCustomTreeNode): vertices_s = self.inputs['Vertices'].sv_get(deepcopy=False) edges_s = self.inputs['Edges'].sv_get(default=[[]], deepcopy=False) - faces_s = self.inputs['Faces'].sv_get(default=[[]], deepcopy=False) + faces_s = self.inputs['Faces'].sv_get(deepcopy=False) masks_s = self.inputs['VertMask'].sv_get(default=[[1]], deepcopy=False) iterations_s = self.inputs['Iterations'].sv_get(deepcopy=False) factor_s = self.inputs['Factor'].sv_get(deepcopy=False) @@ -125,6 +140,14 @@ class SvRelaxMeshNode(bpy.types.Node, SverchCustomTreeNode): nested_output = input_level > 3 + used_axes = set() + if self.use_x: + used_axes.add(0) + if self.use_y: + used_axes.add(1) + if self.use_z: + used_axes.add(2) + verts_out = [] for params in zip_long_repeat(vertices_s, edges_s, faces_s, masks_s, iterations_s, factor_s): for vertices, edges, faces, mask, iterations, factor in zip_long_repeat(*params): @@ -132,21 +155,24 @@ class SvRelaxMeshNode(bpy.types.Node, SverchCustomTreeNode): vertices = lloyd_relax(vertices, faces, iterations, mask = mask, method = self.preserve_shape, - skip_boundary = self.skip_bounds) + skip_boundary = self.skip_bounds, + use_axes = used_axes) elif self.algorithm == 'EDGES': vertices = edges_relax(vertices, edges, faces, iterations, k = factor, mask = mask, method = self.preserve_shape, target = self.target, - skip_boundary = self.skip_bounds) + skip_boundary = self.skip_bounds, + use_axes = used_axes) elif self.algorithm == 'FACES': vertices = faces_relax(vertices, edges, faces, iterations, k = factor, mask = mask, method = self.preserve_shape, target = self.target, - skip_boundary = self.skip_bounds) + skip_boundary = self.skip_bounds, + use_axes = used_axes) else: raise Exception("Unsupported algorithm") diff --git a/utils/relax_mesh.py b/utils/relax_mesh.py index 2cc31ae6f..73ea3cc22 100644 --- a/utils/relax_mesh.py +++ b/utils/relax_mesh.py @@ -27,9 +27,30 @@ MINIMUM = 'MIN' MAXIMUM = 'MAX' AVERAGE = 'MEAN' -def lloyd_relax(vertices, faces, iterations, mask=None, method=NORMAL, skip_boundary=True): +def mask_axes(src_vert, dst_vert, axes): + if axes == {0,1,2}: + return dst_vert + result = [] + for axis in range(3): + if axis in axes: + result.append(dst_vert[axis]) + else: + result.append(src_vert[axis]) + return result + +def map_mask_axes(src_verts, dst_verts, axes): + if axes == {0,1,2}: + return dst_verts + result = np.asarray(src_verts).copy() + dst = np.asarray(dst_verts) + for i in range(3): + if i in axes: + result[:,i] = dst[:,i] + return result.tolist() + +def lloyd_relax(vertices, faces, iterations, mask=None, method=NORMAL, skip_boundary=True, use_axes={0,1,2}): """ - supported shape preservation methods: NORMAL, LINEAR, BVH + supported shape preservation methods: NONE, NORMAL, LINEAR, BVH """ def do_iteration(bvh, bm): @@ -43,7 +64,9 @@ def lloyd_relax(vertices, faces, iterations, mask=None, method=NORMAL, skip_boun normal = bm_vert.normal cs = np.array([face_centers[face.index] for face in bm_vert.link_faces]) - if method == NORMAL: + if method == NONE: + new_vert = cs.mean(axis=0) + elif method == NORMAL: median = mathutils.Vector(cs.mean(axis=0)) dv = median - co dv = dv - dv.project(normal) @@ -61,6 +84,7 @@ def lloyd_relax(vertices, faces, iterations, mask=None, method=NORMAL, skip_boun raise Exception("Unsupported volume preservation method") new_vert = tuple(new_vert) + new_vert = mask_axes(tuple(co), new_vert, use_axes) verts_out.append(new_vert) @@ -77,7 +101,7 @@ def lloyd_relax(vertices, faces, iterations, mask=None, method=NORMAL, skip_boun return vertices -def edges_relax(vertices, edges, faces, iterations, k, mask=None, method=NONE, target=AVERAGE, skip_boundary=True): +def edges_relax(vertices, edges, faces, iterations, k, mask=None, method=NONE, target=AVERAGE, skip_boundary=True, use_axes={0,1,2}): """ supported shape preservation methods: NONE, NORMAL, BVH """ @@ -140,9 +164,9 @@ def edges_relax(vertices, edges, faces, iterations, k, mask=None, method=NONE, t else: raise Exception("Unsupported shape preservation method") - return verts_out + return map_mask_axes(verts, verts_out, use_axes) - if not edges: + if not edges or not edges[0]: edges = polygons_to_edges([faces], unique_edges=True)[0] edges = np.array(edges) if mask is not None: @@ -155,7 +179,7 @@ def edges_relax(vertices, edges, faces, iterations, k, mask=None, method=NONE, t return vertices -def faces_relax(vertices, edges, faces, iterations, k, mask=None, method=NONE, target=AVERAGE, skip_boundary=True): +def faces_relax(vertices, edges, faces, iterations, k, mask=None, method=NONE, target=AVERAGE, skip_boundary=True, use_axes={0,1,2}): """ supported shape preservation methods: NONE, NORMAL, BVH """ @@ -220,10 +244,12 @@ def faces_relax(vertices, edges, faces, iterations, k, mask=None, method=NONE, t else: raise Exception("Unsupported shape preservation method") - return verts_out + return map_mask_axes(vert_cos, verts_out, use_axes) if mask is not None: mask = repeat_last_for_length(mask, len(vertices)) + if not edges or not edges[0]: + edges = polygons_to_edges([faces], unique_edges=True)[0] bvh = BVHTree.FromPolygons(vertices, faces) for i in range(iterations): -- GitLab From 5f743386b7f4ee43cc84ced382ffd9b6cede8a28 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Mon, 4 Jan 2021 14:06:22 +0500 Subject: [PATCH 3/5] Start documentation. --- .../modifier_change/modifier_change_index.rst | 1 + docs/nodes/modifier_change/relax_mesh.rst | 77 +++++++++++++++++++ nodes/modifier_change/relax_mesh.py | 4 +- 3 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 docs/nodes/modifier_change/relax_mesh.rst diff --git a/docs/nodes/modifier_change/modifier_change_index.rst b/docs/nodes/modifier_change/modifier_change_index.rst index d26792ffe..c7fd57445 100644 --- a/docs/nodes/modifier_change/modifier_change_index.rst +++ b/docs/nodes/modifier_change/modifier_change_index.rst @@ -10,6 +10,7 @@ Modifier Change subdivide_lite unsubdivide smooth + relax_mesh delete_loose edges_intersect_mk2 poke diff --git a/docs/nodes/modifier_change/relax_mesh.rst b/docs/nodes/modifier_change/relax_mesh.rst new file mode 100644 index 000000000..0fec19c2c --- /dev/null +++ b/docs/nodes/modifier_change/relax_mesh.rst @@ -0,0 +1,77 @@ +Relax Mesh +========== + +Functionality +------------- + +This node moves vertices of the input mesh, in order to make it more "relaxed", +i.e. for mesh elements to have more even distribution in some sense. There are +several algorithms supported, each of which has different definition of what "relaxed" is: + +* Lloyd-based algorithm. This is created after well-known Lloyd algorithm. For + each vertex, find centers of all incident faces, and then find the average of + those points; this will be the new location of the vertex. Effectively, this + algorithm tries to make each face as close to circle as possible. This + algorithm shows it's best for meshes that consist of Tris. +* Edge lengths. Scale each edge up or down, trying to make all edges of the + same length. Target edge length can be minimum, maximum or average of all + lengths of edges of the source mesh. +* Face areas. Scale each face up or down, trying to make all faces of the same + area. Target face area can be minimum, maximum or average of all areas of + faces of the source mes. + +These algorithms can change the overall shape of the mesh a lot. In order to +try to preserve original shape of the mesh at least partially, the following +methods are supported: + +* "Linear" method is supported for Lloyd algorithm only. When the algorithm has + found the new location of the vertex, put it at the same distance from the + plane where centers of incident faces lie, as the original vertex was. This + method can be slow for large meshes. +* "Tangent": move vertices along tangent planes of original vertexes only (i.e. + perpendicular to vertex normal). +* "BVH": use BVH tree to find the nearest point to the newly calculated vertex + on the original mesh. + +Inputs +------ + +This node has the following inputs: + +* **Vertices**. Vertices of the original mesh. This input is mandatory. +* **Edges**. Edges of the original mesh. +* **Faces**. Faces of the original mesh. This input is mandatory. +* **VertMask**. Mask defining which vertices are to be moved. By default, all vertices can be moved. +* **Iterations**. Number of iterations of algorithm. Default value is 1. +* **Factor**. This input is available for **Edge Length** and **Face Area** + algorithms. Coefficient to be used at each iteration of the algorithm. + Possible values are between 0 and 1. The default value is 0.5. + +Parameters +---------- + +This node has the following parameters: + +* **Algorithm**. Relaxation algorithm to be used. The available options are + **Lloyd**, **Edge Length** and **Face Area**. The default option is + **Lloyd**. +* **Target**. This parameter is available for **Edge Length** and **Face Area** + algorithms. This defines which value of edge length or face area is to be + used as the target value for the algorithm. The available options are + **Average**, **Minimum** and **Maximum**. The default option is **Average**. +* **Skip bounds**. If checked, the node will not move boundary vertexes of the + original mesh. Checked by default. +* **X**, **Y**, **Z**. These parameters define the coordinate axes along which + it is allowed to move vertices. By default, all three parameters are checked, + which means the node can move vertices in any direction. + +Outputs +------- + +This node has the following output: + +* **Vertices**. Vertices of the relaxed mesh. + +The node does not modify edges or faces of the original mesh, it only moves the vertexes. + + diff --git a/nodes/modifier_change/relax_mesh.py b/nodes/modifier_change/relax_mesh.py index b355e2c8c..065b35084 100644 --- a/nodes/modifier_change/relax_mesh.py +++ b/nodes/modifier_change/relax_mesh.py @@ -55,7 +55,7 @@ class SvRelaxMeshNode(bpy.types.Node, SverchCustomTreeNode): items.append((NONE, "Do not use", "Do not use", 0)) if self.algorithm == 'LLOYD': items.append((LINEAR, "Linear", "Linear", 1)) - items.append((NORMAL, "Normal", "Move points along mesh tangent only", 2)) + items.append((NORMAL, "Tangent", "Move points along mesh tangent only", 2)) items.append((BVH, "BVH", "Use BVH Tree", 3)) return items @@ -98,7 +98,7 @@ class SvRelaxMeshNode(bpy.types.Node, SverchCustomTreeNode): self.inputs.new('SvVerticesSocket', "Vertices") self.inputs.new('SvStringsSocket', 'Edges') self.inputs.new('SvStringsSocket', 'Faces') - self.inputs.new('SvStringsSocket', 'VertMask') + self.inputs.new('SvStringsSocket', 'VertMask').enable_input_link_menu = False self.inputs.new('SvStringsSocket', 'Iterations').prop_name = "iterations" self.inputs.new('SvStringsSocket', 'Factor').prop_name = "factor" -- GitLab From 9a91b835272822f5522bec456434580ce2939c4f Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Mon, 4 Jan 2021 14:08:07 +0500 Subject: [PATCH 4/5] Add an example. --- docs/nodes/modifier_change/relax_mesh.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/nodes/modifier_change/relax_mesh.rst b/docs/nodes/modifier_change/relax_mesh.rst index 0fec19c2c..f48d80d72 100644 --- a/docs/nodes/modifier_change/relax_mesh.rst +++ b/docs/nodes/modifier_change/relax_mesh.rst @@ -74,4 +74,8 @@ This node has the following output: The node does not modify edges or faces of the original mesh, it only moves the vertexes. +Example of Usage +---------------- + +.. image:: https://user-images.githubusercontent.com/284644/103518798-323f4400-4e96-11eb-8980-39cbac4a5a40.png -- GitLab From 56a3f1bf5f1de7277d311d36c570edd52fdf9607 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Mon, 4 Jan 2021 14:13:44 +0500 Subject: [PATCH 5/5] Update documentation. --- docs/nodes/modifier_change/relax_mesh.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/nodes/modifier_change/relax_mesh.rst b/docs/nodes/modifier_change/relax_mesh.rst index f48d80d72..9f25e9526 100644 --- a/docs/nodes/modifier_change/relax_mesh.rst +++ b/docs/nodes/modifier_change/relax_mesh.rst @@ -59,6 +59,10 @@ This node has the following parameters: algorithms. This defines which value of edge length or face area is to be used as the target value for the algorithm. The available options are **Average**, **Minimum** and **Maximum**. The default option is **Average**. +* **Preserve shape**. This defines the method to be used in order to preserve + the shape of the original mesh at least partially. The available options are + **Do not use**, **Linear** (for Lloyd algorithm only), **Tangent** and + **BVH**. The default option is **Do not use**. * **Skip bounds**. If checked, the node will not move boundary vertexes of the original mesh. Checked by default. * **X**, **Y**, **Z**. These parameters define the coordinate axes along which -- GitLab