nortikin / sverchok

Sverchok
http://nortikin.github.io/sverchok/
GNU General Public License v3.0
2.26k stars 233 forks source link

Flip normals node #4491

Open Durman opened 2 years ago

Durman commented 2 years ago

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

zeffii commented 2 years ago

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 commented 2 years ago

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 ```python """ >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]) ```
Durman commented 2 years ago

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 commented 2 years ago

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

zeffii commented 2 years ago

(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 commented 2 years ago

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.

Durman commented 2 years ago

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).