sverchok icon indicating copy to clipboard operation
sverchok copied to clipboard

Flip normals node

Open Durman opened this issue 2 years ago • 7 comments

Problem statement

It looks there is no need to convert input data into bmesh because it's quite expensive.

https://github.com/nortikin/sverchok/blob/ff53b66cc349b74b0db67139e92947d0d0f51bd6/nodes/modifier_change/flip_normals.py#L44-L60

image

Durman avatar May 25 '22 05:05 Durman

untested..

import numpy as np
from sverchok.utils.modules.polygon_utils import np_process_polygons, np_faces_normals

def flip_to_match_1st_np(geom, reverse): 
    """ 
    this mode expects all faces to be coplanar, else you need to manually generate a flip mask. 
    """ 
    verts, edges, faces = geom 
    normals = np_process_polygons(verts, faces, func=np_faces_normals, output_numpy=True) # already normalized
    direction = normals[0]
    matched = np.linalg.norm((normals - direction), axis=1) < 0.004
    flips = np.invert(matched) if reverse else matched
    b_faces = [poly if flip else poly[::-1] for flip, poly in zip(flips, faces)]

    return verts, edges, b_faces 

zeffii avatar May 25 '22 10:05 zeffii

tested.. works for ngons, i'll assume tris and quads have been tested by the mere fact that inset specials has been used for 14 months now.

snlite

"""
>in verts_in v
>in faces_in s
in reverse s d=0 n=2
out verts_out v
out faces_out s
"""
# import numpy as np
from sverchok.utils.modules.polygon_utils import np_process_polygons, np_faces_normals

def flip_to_match_1st_np(geom, reverse): 
    """ 
    this mode expects all faces to be coplanar, else you need to manually generate a flip mask. 
    """ 
    verts, edges, faces = geom 
    normals = np_process_polygons(verts, faces, func=np_faces_normals, output_numpy=True) # already normalized
    direction = normals[0]
    matched = np.linalg.norm((normals - direction), axis=1) < 0.004
    flips = np.invert(matched) if reverse else matched
    b_faces = [poly if flip else poly[::-1] for flip, poly in zip(flips, faces)]

    return verts, edges, b_faces 

for verts, faces in zip(verts_in, faces_in):
    geom = flip_to_match_1st_np((verts, [], faces), bool(reverse))
    verts_out.append(geom[0])
    faces_out.append(geom[2])

zeffii avatar May 25 '22 11:05 zeffii

It gives me next warning

2022-07-29 08:39:19,258 [WARNING] py.warnings: \sverchok\utils\modules\polygon_utils.py:292: VisibleDeprecationWarning: Creating an ndarray from ragged nested sequences (which is a list-or-tuple of lists-or-tuples-or ndarrays with different lengths or shapes) is deprecated. If you meant to do this, you must specify 'dtype=object' when creating the ndarray. np_faces = np.array(faces)

Also it improves performance only 2 times. I guess it should be better without converting data into bmesh.

Most of the time (or about a half) it spends in converting faces into numpy array now. =)

image

According to the description the node (in this mode) expects coplanar entire mesh. So why not to calculate n-gone normals by any of their 3 vertices. But if we want to save the previous behavior the normals should be calculated in the same way as in bmesh module.

Durman avatar Jul 29 '22 05:07 Durman

This implementation can handle n-gons without warnings but it calculates normals using only 3 first points. It fits into logic description of the node but potentially can effect some layouts. Also there is no significant performance improvement compare to the previous solution. Most of the time it spends on converting first 3 indexes of faces into a numpy array and applying the mask to Python face list.

If we want significant performance improvements the format of faces should be changed somehow.

from sverchok.utils.math import np_normalize_vectors
def flip_to_match_1st_np(geom, reverse):
    """
    this mode expects all faces to be coplanar, else you need to manually generate a flip mask.
    """
    verts, edges, faces = geom
    normals = face_normals_np(verts, faces)
    direction = normals[0]
    flips = np.isclose(np.dot(normals, direction), 1, atol=0.004)
    if reverse:
        flips = ~flips
    b_faces = [poly if flip else poly[::-1] for flip, poly in zip(flips, faces)]

    return verts, edges, b_faces


def face_normals_np(verts, faces):
    def first_3_indexes():
        for f in faces:
            for i in f[:3]:
                yield i
    faces = np.fromiter(first_3_indexes(), dtype=int)  # 2x faster than from list
    faces.shape = (-1, 3)
    v1 = verts[faces[:, 0]]
    v2 = verts[faces[:, 1]]
    v3 = verts[faces[:, 2]]
    d1 = v1 - v2
    d2 = v3 - v2
    normals = np.cross(d1, d2)
    np_normalize_vectors(normals)
    return normals

image

Durman avatar Jul 29 '22 07:07 Durman

(in this mode) expects coplanar entire mesh. So why not to calculate n-gone normals by any of their 3 vertices.

yes, seems sensible.

zeffii avatar Aug 03 '22 09:08 zeffii

but, not sure that returns the desired results for mesh input that has concave polygons however, i suppose

    def first_3_indexes():
        for f in faces:
            for i in f[:3]:
                yield i

could easily return them in reverse if the first 3 indices form a concave section.

zeffii avatar Aug 03 '22 09:08 zeffii

Completely forgot about this case. Also there is corner case when all 3 vertices are on one line. In this case the normal will be (0,0,0).

Durman avatar Aug 03 '22 09:08 Durman