From b0ce2e34ff77a54049c212c94f13432eda3ddd0e Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Mon, 11 May 2020 22:56:24 +0500 Subject: [PATCH 1/3] "Surface Frame" node. --- index.md | 1 + nodes/surface/normals.py | 203 +++++++++++++++++++++++++++++++++++++++ utils/surface.py | 69 +++++++++++++ 3 files changed, 273 insertions(+) create mode 100644 nodes/surface/normals.py diff --git a/index.md b/index.md index cd39fcc7e..d5336f37b 100644 --- a/index.md +++ b/index.md @@ -103,6 +103,7 @@ SvExSurfaceSubdomainNode SvFlipSurfaceNode SvSwapSurfaceNode + SvSurfaceNormalsNode SvSurfaceGaussCurvatureNode SvSurfaceCurvaturesNode --- diff --git a/nodes/surface/normals.py b/nodes/surface/normals.py new file mode 100644 index 000000000..6935b52ed --- /dev/null +++ b/nodes/surface/normals.py @@ -0,0 +1,203 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty + +import sverchok +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level, get_data_nesting_level, repeat_last_for_length +from sverchok.utils.logging import info, exception +from sverchok.utils.surface import SvSurface + +class SvSurfaceNormalsNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Surface Normals Tangents + Tooltip: Calculate surface normals and tangents + """ + bl_idname = 'SvSurfaceNormalsNode' + bl_label = 'Surface Frame' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_SURFACE_CURVATURE' + + @throttled + def update_sockets(self, context): + self.inputs['U'].hide_safe = self.input_mode == 'VERTICES' + self.inputs['V'].hide_safe = self.input_mode == 'VERTICES' + self.inputs['UVPoints'].hide_safe = self.input_mode == 'PAIRS' + + input_modes = [ + ('PAIRS', "Separate", "Separate U V (or X Y) sockets", 0), + ('VERTICES', "Vertices", "Single socket for vertices", 1) + ] + + input_mode : EnumProperty( + name = "Input mode", + items = input_modes, + default = 'PAIRS', + update = update_sockets) + + clamp_modes = [ + ('NO', "As is", "Do not clamp input values - try to process them as is (you will get either error or extrapolation on out-of-bounds values, depending on specific surface type", 0), + ('CLAMP', "Clamp", "Clamp input values into bounds - for example, turn -0.1 into 0", 1), + ('WRAP', "Wrap", "Wrap input values into bounds - for example, turn -0.1 into 0.9", 2) + ] + + clamp_mode : EnumProperty( + name = "Clamp", + items = clamp_modes, + default = 'NO', + update = updateNode) + + axes = [ + ('XY', "X Y", "XOY plane", 0), + ('YZ', "Y Z", "YOZ plane", 1), + ('XZ', "X Z", "XOZ plane", 2) + ] + + orientation : EnumProperty( + name = "Orientation", + items = axes, + default = 'XY', + update = updateNode) + + u_value : FloatProperty( + name = "U", + description = "Surface U parameter", + default = 0.5, + update = updateNode) + + v_value : FloatProperty( + name = "V", + description = "Surface V parameter", + default = 0.5, + update = updateNode) + + def draw_buttons(self, context, layout): + layout.label(text="Input mode:") + layout.prop(self, "input_mode", expand=True) + if self.input_mode == 'VERTICES': + layout.label(text="Input orientation:") + layout.prop(self, "orientation", expand=True) + layout.prop(self, 'clamp_mode', expand=True) + + def sv_init(self, context): + self.inputs.new('SvSurfaceSocket', "Surface") + self.inputs.new('SvVerticesSocket', "UVPoints") + self.inputs.new('SvStringsSocket', "U").prop_name = 'u_value' + self.inputs.new('SvStringsSocket', "V").prop_name = 'v_value' + self.outputs.new('SvVerticesSocket', "Normal") + self.outputs.new('SvVerticesSocket', "TangentU") + self.outputs.new('SvVerticesSocket', "TangentV") + self.outputs.new('SvStringsSocket', "AreaStretch") + self.outputs.new('SvStringsSocket', "StretchU") + self.outputs.new('SvStringsSocket', "StretchV") + self.outputs.new('SvMatrixSocket', "Matrix") + self.update_sockets(context) + + def parse_input(self, verts): + verts = np.array(verts) + if self.orientation == 'XY': + us, vs = verts[:,0], verts[:,1] + elif self.orientation == 'YZ': + us, vs = verts[:,1], verts[:,2] + else: # XZ + us, vs = verts[:,0], verts[:,2] + return us, vs + + def _clamp(self, surface, us, vs): + u_min = surface.get_u_min() + u_max = surface.get_u_max() + v_min = surface.get_v_min() + v_max = surface.get_v_max() + us = np.clip(us, u_min, u_max) + vs = np.clip(vs, v_min, v_max) + return us, vs + + def _wrap(self, surface, us, vs): + u_min = surface.get_u_min() + u_max = surface.get_u_max() + v_min = surface.get_v_min() + v_max = surface.get_v_max() + u_len = u_max - u_min + v_len = v_max - u_min + us = (us - u_min) % u_len + u_min + vs = (vs - v_min) % v_len + v_min + return us, vs + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + surfaces_s = self.inputs['Surface'].sv_get() + surfaces_s = ensure_nesting_level(surfaces_s, 2, data_types=(SvSurface,)) + src_point_s = self.inputs['UVPoints'].sv_get(default=[[]]) + src_point_s = ensure_nesting_level(src_point_s, 4) + src_u_s = self.inputs['U'].sv_get() + src_u_s = ensure_nesting_level(src_u_s, 3) + src_v_s = self.inputs['V'].sv_get() + src_v_s = ensure_nesting_level(src_v_s, 3) + + normal_out = [] + tangent_u_out = [] + tangent_v_out = [] + matrix_out = [] + area_out = [] + du_out = [] + dv_out = [] + + for surfaces, src_points_i, src_u_i, src_v_i in zip_long_repeat(surfaces_s, src_point_s, src_u_s, src_v_s): + new_normals = [] + new_tangent_u = [] + new_tangent_v = [] + new_area = [] + new_du = [] + new_dv = [] + for surface, src_points, src_us, src_vs in zip_long_repeat(surfaces, src_points_i, src_u_i, src_v_i): + if self.input_mode == 'VERTICES': + us, vs = self.parse_input(src_points) + else: + maxlen = max(len(src_us), len(src_vs)) + src_us = repeat_last_for_length(src_us, maxlen) + src_vs = repeat_last_for_length(src_vs, maxlen) + us, vs = np.array(src_us), np.array(src_vs) + + data = surface.derivatives_data_array(us, vs) + + new_normals.append(data.unit_normals().tolist()) + du, dv = data.unit_tangents() + new_tangent_u.append(du.tolist()) + new_tangent_v.append(dv.tolist()) + + normals_len = [n[0] for n in data.normals_len().tolist()] + new_area.append(normals_len) + + du_len, dv_len = data.tangent_lens() + du_len = [n[0] for n in du_len.tolist()] + dv_len = [n[0] for n in dv_len.tolist()] + new_du.append(du_len) + new_dv.append(dv_len) + + matrix_out.extend(data.matrices(as_mathutils = True)) + + normal_out.append(new_normals) + tangent_u_out.append(new_tangent_u) + tangent_v_out.append(new_tangent_v) + area_out.append(new_area) + du_out.append(new_du) + dv_out.append(new_dv) + + self.outputs['Normal'].sv_set(normal_out) + self.outputs['TangentU'].sv_set(tangent_u_out) + self.outputs['TangentV'].sv_set(tangent_v_out) + self.outputs['Matrix'].sv_set(matrix_out) + self.outputs['AreaStretch'].sv_set(area_out) + self.outputs['StretchU'].sv_set(du_out) + self.outputs['StretchV'].sv_set(dv_out) + +def register(): + bpy.utils.register_class(SvSurfaceNormalsNode) + +def unregister(): + bpy.utils.unregister_class(SvSurfaceNormalsNode) + diff --git a/utils/surface.py b/utils/surface.py index 43675c330..ba1008417 100644 --- a/utils/surface.py +++ b/utils/surface.py @@ -243,6 +243,63 @@ class SurfaceCurvatureCalculator(object): data.mean = self.mean() return data +class SurfaceDerivativesData(object): + def __init__(self, points, du, dv): + self.points = points + self.du = du + self.dv = dv + self._normals = None + self._normals_len = None + self._unit_normals = None + self._unit_du = None + self._unit_dv = None + self._du_len = self._dv_len = None + + def normals(self): + if self._normals is None: + self._normals = np.cross(self.du, self.dv) + return self._normals + + def normals_len(self): + if self._normals_len is None: + normals = self.normals() + self._normals_len = np.linalg.norm(normals, axis=1)[np.newaxis].T + return self._normals_len + + def unit_normals(self): + if self._unit_normals is None: + normals = self.normals() + norm = self.normals_len() + self._unit_normals = normals / norm + return self._unit_normals + + def tangent_lens(self): + if self._du_len is None: + self._du_len = np.linalg.norm(self.du, axis=1, keepdims=True) + self._dv_len = np.linalg.norm(self.dv, axis=1, keepdims=True) + return self._du_len, self._dv_len + + def unit_tangents(self): + if self._unit_du is None: + du_norm, dv_norm = self.tangent_lens() + self._unit_du = self.du / du_norm + self._unit_dv = self.dv / dv_norm + return self._unit_du, self._unit_dv + + def matrices(self, as_mathutils = False): + normals = self.unit_normals() + du, dv = self.unit_tangents() + matrices_np = np.dstack((du, dv, normals)) + matrices_np = np.transpose(matrices_np, axes=(0,2,1)) + matrices_np = np.linalg.inv(matrices_np) + if as_mathutils: + matrices = [Matrix(m.tolist()).to_4x4() for m in matrices_np] + for m, p in zip(matrices, self.points): + m.translation = Vector(p) + return matrices + else: + return matrices_np + class SvSurface(object): def __repr__(self): if hasattr(self, '__description__'): @@ -284,6 +341,18 @@ class SvSurface(object): #self.info("Normals: %s", normal) return normal + def derivatives_data_array(self, us, vs): + if hasattr(self, 'normal_delta'): + h = self.normal_delta + else: + h = 0.0001 + surf_vertices = self.evaluate_array(us, vs) + u_plus = self.evaluate_array(us + h, vs) + v_plus = self.evaluate_array(us, vs + h) + du = (u_plus - surf_vertices) / h + dv = (v_plus - surf_vertices) / h + return SurfaceDerivativesData(surf_vertices, du, dv) + def curvature_calculator(self, us, vs, order=True): if hasattr(self, 'normal_delta'): h = self.normal_delta -- GitLab From fe8b4282034c229e51ed3186651c0f60591cae88 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Mon, 11 May 2020 23:05:15 +0500 Subject: [PATCH 2/3] Icon for "Surface Frame" node. --- nodes/surface/normals.py | 2 +- ui/icons/sv_surface_frame.png | Bin 0 -> 2290 bytes ui/icons/svg/sv_surface_frame.svg | 224 ++++++++++++++++++++++++++++++ 3 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 ui/icons/sv_surface_frame.png create mode 100644 ui/icons/svg/sv_surface_frame.svg diff --git a/nodes/surface/normals.py b/nodes/surface/normals.py index 6935b52ed..958b0eeab 100644 --- a/nodes/surface/normals.py +++ b/nodes/surface/normals.py @@ -18,7 +18,7 @@ class SvSurfaceNormalsNode(bpy.types.Node, SverchCustomTreeNode): bl_idname = 'SvSurfaceNormalsNode' bl_label = 'Surface Frame' bl_icon = 'OUTLINER_OB_EMPTY' - sv_icon = 'SV_SURFACE_CURVATURE' + sv_icon = 'SV_SURFACE_FRAME' @throttled def update_sockets(self, context): diff --git a/ui/icons/sv_surface_frame.png b/ui/icons/sv_surface_frame.png new file mode 100644 index 0000000000000000000000000000000000000000..653b3e2fba9a2db1974c4e1a343b212b87bc34fb GIT binary patch literal 2290 zcmV3W9%0oQ z+ycUCiwabL;144K<|R-oUUHCI3E{X1iy+)$Bru_(0-RJuK;^1n2!--Y1L2Vx+odI7 zX!YB*Qe#|1*J?{AD1}ALxhjNTvAe zcd6=k0qwv;RZ(A0G`;*OeMSwGA zipKaZa2#k2)0S&yo&7Y76RS!Oa07S;_%Yy!jW?70#M&an`waNGS*N85A|0+gM(MLZ zB>1t7ghJqbltJ!k82|r4>9P@#1`G>z(PpzzR8)k^<-%sOVYl1q?Ck8l{`~oK9zJ{+ zRJyyn&BPr5%=eeuKsB)2PbwT5{AB=e3FCMCb6^`vcQl1;Uun008f*gN_|LHX#B8aPQtdS+HP%*le~)<3yu| zJiyniCJ#zi2n;X+$gNwq5-Ng-NJB$|j2t;K+|w|^DuR(F3!egiQ~z+_fTi8c5N@}d zapT5?x;zl&$+UWU9TE#s-GRcw!i3)ao}M0AxNxDFdBcY&3!sGK6?I^z(ql9ct+sA?=<$dnV(@k2jh% z{vc5TyrT}bW5*6l$gf~^~xJ$f`MKaU?jmO+CCX+>CXwE!Qg1Drp9K5hX-M9Ry{b=|<>aLDoF z$D{JIdGltipHHk7z@rXu|NebT1P~Fasj1QRRXuvfs03 zk5+(>tP;Q2?~d8 z+qUUM7&vgCse!k!uuy9|H(v57XjXz(uU<{403sq=w{F#m;B-3W;>C+$?KW)KptXBD zUID(Rb~t$OV9N)HDW zaQX6O!}^VljoSL2cm?=a?Qq||eSIo`h)7vknNEZuLx#w;Yu9vjdwP0gz<>eT`q{CP z&sRI@*RP+nx3~AX03sqAH*Pc(;rjLKy1JuAjndW~6{`SMYDZJ2PE7*!7g)c3y-oz9 z)uTb4V0<`|q0&ehzykFTB_$<66-0#Q=4M)2TJU&0csw4wUT^RAcxGlMcDtRNoE-A< z^2p1}BR4k}yWJk;eNeDv%NBe-AC;Ary}z}!wJ~|}WEvV87(IG404|qHYx+_w@s^_* z{oC8yrLL|{R;^kkZnxX0uSF~lheIYzm>_G`tdWBU4~p08jp`}z`Fyf``SKv2si~=Q z`0(N0I&k`Q&VOC{{2DM z=ggU-jWZ`&0e)pQQ8UGEx6Azb^X0;Y3#LRUEiDbY^8pxSji-ept399d8t_k)<8o%; z1+Js)%zFJLa7?a0N`CHTAP*RcvXvbX6DLljqM`z~+ikKLFEDA+B+i~a8}t)!HbiwD zTqtLDLfxs`C?_$00t}D3b;>Tz&rnj>ABBk{fXtjZQyLo^O?8a`^7Hexe#S)G$T0+; z!1^yrqVd}?5RQabfwjO*LWq!_o-S2YRi?J%d_JFKWo2o7#556_5Bvl81qt49@xz94 ze()HYLm7cGWy%z}d-rZw0q)$nqqU2BDx3^_AI(-=pycG_gj@%Rh#Wh1OlxyGStR0x z4VZ_L;tnE}m6a7TXvaN5n>I}=z>=i3+(OuZax@}j zWMl*#-Rtb^G#UWEm8?c50;MP$puGgMv$LhOwN*rvZ8Cz%-%Vaq6NjKR3LwSB#Uf(3 z*Z6MonweN^Mk7K^O^u8gF+wXq+zHXoD^gHiZ@pyu_wTPI|CByBHj_-eg7VElsA=?} zq_wOM@oPd0VESv3FEU&x-vw&Pw-LoBIi@}<`JAEy_=~~szeo81>??s~1mFG7P|i1h zWiW + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + -- GitLab From 04959cecad41cce864384d3cef47db2a6d7c1b87 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Tue, 12 May 2020 21:57:00 +0500 Subject: [PATCH 3/3] Documentation for the "surface frame" node. --- docs/nodes/surface/normals.rst | 106 +++++++++++++++++++++++++++ docs/nodes/surface/surface_index.rst | 1 + nodes/surface/normals.py | 10 ++- 3 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 docs/nodes/surface/normals.rst diff --git a/docs/nodes/surface/normals.rst b/docs/nodes/surface/normals.rst new file mode 100644 index 000000000..4db20ded0 --- /dev/null +++ b/docs/nodes/surface/normals.rst @@ -0,0 +1,106 @@ +Surface Frame +============= + +Functionality +------------- + +This node outputs information about normals and tangents of the surface at +given points, together with a matrix giving reference frame according to +surface normals. + +Inputs +------ + +* **Surface**. The surface to analyze. This input is mandatory. +* **U**, **V**. Values of U and V surface parameters. These inputs are + available only when **Input mode** parameter isset to **Separate**. The + default value is 0.5. +* **UVPoints**. Points at which the surface is to be analyzed. Only two of + three coordinates will be used; the coordinates used are defined by the + **Orientation** parameter. This input is available and mandatory if the + **Input mode** parameter is set to **Vertices**. + +Parameters +---------- + +This node has the following parameters: + +* **Input mode**. The available options are: + + * **Separate**. The values of U and V surface parameters will be provided in + **U** and **V** inputs, correspondingly. + * **Vertices**. The values of U and V surface parameters will be provided in + **Vertices** input; only two of three coordinates of the input vertices + will be used. + + The default mode is **Separate**. + +* **Input orientation**. This parameter is available only when **Input mode** + parameter is set to **Vertices**. This defines which coordinates of vertices + provided in the **Vertices** input will be used. The available options are + XY, YZ and XZ. For example, if this is set to XY, then the X coordinate of + vertices will be used as surface U parameter, and Y coordinate will be used + as V parameter. The default value is XY. +* **Clamp**. This defines how the node will process the values of + surface U and V parameters which are out of the surface's domain. The + available options are: + + * **As is**. Do not do anything special, just pass the parameters to the + surface calculation algorithm as they are. The behaviour of the surface + when the values of parameters provided are out of domain depends on + specific surface: some will just calculate points by the same formula, + others will give an error. + * **Clamp**. Restrict the parameter values to the surface domain: replace + values that are greater than the higher bound with higher bound value, + replace values that are smaller than the lower bound with the lower bound + value. For example, if the surface domain along U direction is `[0 .. 1]`, + and the value of U parameter is 1.05, calculate the point of the surface + at U = 1.0. + * **Wrap**. Wrap the parameter values to be within the surface domain, i.e. + take the values modulo domain. For example, if the surface domain along U + direction is `[0 .. 1]`, and the value of U parameter is 1.05, evaluate + the surface at U = 0.05. + + The default mode is **As is**. + +Outputs +------- + +This node has the following outputs: + +* **Normal**. Unit normal vectors of the surface at the specified points. +* **TangentU**. Unit tangent vectors of the surface at the specified points + along the U direction; more precisely, if the surface is defined by ``P = + F(u, v)``, then this is ``dF/du`` vector divided by it's length. +* **TangentV**. Unit tangent vectors of the surface at the specified points + along the V direction; more precisely, if the surface is defined by ``P = + F(u, v)``, then this is ``dF/dv`` vector divided by it's length. +* **AreaStretch**. Coefficient of the stretching of the surface area in the + mapping of areas in the UV space to 3D space, in the provided points. This + equals to ``|dF/du| * |dF/dv|`` (norm of derivative by U multiplied by norm + of derivative by V). So, **AreaStretch** is always equal to product of + **StretchU** by **StretchV**. +* **StretchU**. Coefficient of stretching the surface along the U direction; + this equals to ``|dF/du|``. +* **StretchV**. Coefficient of stretching the surface along the V direction; + this equals to ``|dF/dv|``. +* **Matrix**. Reference frame at the surface point, defined by the surface's + normal and parametric tangents: it's Z axis is looking along surface normal; + it's X axis is looking along **TangentU**, and it's Y axis is looking along + **TangentV**. + +Examples of Usage +----------------- + +Visualize Matrix outputfor some formula-defined surface: + +.. image:: https://user-images.githubusercontent.com/284644/81722587-06042c80-949b-11ea-81a4-43d134c9ca77.png + +Use that matrices to place cubes, oriented accordingly: + +.. image:: https://user-images.githubusercontent.com/284644/81722585-056b9600-949b-11ea-9ad9-01f7093c9c80.png + +As you can see, the surface in areas that are far from the center, so that cubes are put sparsely. Let's use StretchU and StretchV outputs to scale them: + +.. image:: https://user-images.githubusercontent.com/284644/81722582-03a1d280-949b-11ea-8e99-9d9354f5e906.png + diff --git a/docs/nodes/surface/surface_index.rst b/docs/nodes/surface/surface_index.rst index f05d5ba0d..6bc7de563 100644 --- a/docs/nodes/surface/surface_index.rst +++ b/docs/nodes/surface/surface_index.rst @@ -21,6 +21,7 @@ Surface subdomain flip swap + normals curvatures gauss_curvature adaptive_tessellate diff --git a/nodes/surface/normals.py b/nodes/surface/normals.py index 958b0eeab..e0f48a0c0 100644 --- a/nodes/surface/normals.py +++ b/nodes/surface/normals.py @@ -130,9 +130,11 @@ class SvSurfaceNormalsNode(bpy.types.Node, SverchCustomTreeNode): return surfaces_s = self.inputs['Surface'].sv_get() + surface_level = get_data_nesting_level(surfaces_s, data_types=(SvSurface,)) surfaces_s = ensure_nesting_level(surfaces_s, 2, data_types=(SvSurface,)) src_point_s = self.inputs['UVPoints'].sv_get(default=[[]]) src_point_s = ensure_nesting_level(src_point_s, 4) + src_u_s = self.inputs['U'].sv_get() src_u_s = ensure_nesting_level(src_u_s, 3) src_v_s = self.inputs['V'].sv_get() @@ -184,8 +186,12 @@ class SvSurfaceNormalsNode(bpy.types.Node, SverchCustomTreeNode): tangent_u_out.append(new_tangent_u) tangent_v_out.append(new_tangent_v) area_out.append(new_area) - du_out.append(new_du) - dv_out.append(new_dv) + if surface_level == 2: + du_out.append(new_du) + dv_out.append(new_dv) + else: + du_out.extend(new_du) + dv_out.extend(new_dv) self.outputs['Normal'].sv_set(normal_out) self.outputs['TangentU'].sv_set(tangent_u_out) -- GitLab