From 2a58d659aa9ed2966c4838ca6a4ee033e81533ed Mon Sep 17 00:00:00 2001 From: Thomas Barlow Date: Wed, 26 Apr 2023 23:34:16 +0100 Subject: [PATCH] FBX IO: Edge/face sharp access with attributes Blender 3.5 moved edge sharp to a generic attribute and 3.6 moved face smooth to a generic attribute. The old API still works for now, but is slower and may be removed in 4.0, so this patch updates FBX IO to use the new "sharp_edge" and "sharp_face" attributes. Because the old API was face smooth, not sharp, the logic for working with "sharp_face" is inverted compared to before. When the "sharp_edge" or "sharp_face" attributes do not exist, all edges or faces can be assumed to not be marked sharp, reducing necessary work. A comment comparing the speed of calculating polygon sides from `t_ls` vs getting polygon sides from polygon "loop_total" has been updated to reflect the change that "loop_total" is not stored internally as of 3.6 and is now slower to access. This patch makes a minor change to the import of FBX files, whereby code that used to set every face to smooth now removes the "sharp_face" attribute instead. --- io_scene_fbx/export_fbx_bin.py | 76 ++++++++++++++++++---------------- io_scene_fbx/import_fbx.py | 28 ++++++++----- 2 files changed, 58 insertions(+), 46 deletions(-) diff --git a/io_scene_fbx/export_fbx_bin.py b/io_scene_fbx/export_fbx_bin.py index 65c5a8ac1..7908da7a3 100644 --- a/io_scene_fbx/export_fbx_bin.py +++ b/io_scene_fbx/export_fbx_bin.py @@ -1030,12 +1030,18 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes): # Smoothing. if smooth_type in {'FACE', 'EDGE'}: ps_fbx_dtype = np.int32 - poly_use_smooth_dtype = bool - edge_use_sharp_dtype = bool _map = b"" if smooth_type == 'FACE': - t_ps = np.empty(len(me.polygons), dtype=poly_use_smooth_dtype) - me.polygons.foreach_get("use_smooth", t_ps) + # The FBX integer values are usually interpreted as boolean where 0 is False (sharp) and 1 is True + # (smooth). + # The values may also be used to represent smoothing group bitflags, but this does not seem well-supported. + t_ps = MESH_ATTRIBUTE_SHARP_FACE.get_ndarray(attributes) + if t_ps is not None: + # FBX sharp is False, but Blender sharp is True, so invert. + t_ps = np.logical_not(t_ps) + else: + # The mesh has no "sharp_face" attribute, so every face is smooth. + t_ps = np.ones(len(me.polygons), dtype=ps_fbx_dtype) _map = b"ByPolygon" else: # EDGE _map = b"ByEdge" @@ -1050,37 +1056,40 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes): mesh_t_ls_view = t_ls[:mesh_poly_nbr] mesh_t_lei_view = t_lei[:mesh_loop_nbr] - # - Get sharp edges from flat shaded faces - # Get the 'use_smooth' attribute of all polygons. - p_use_smooth_mask = np.empty(mesh_poly_nbr, dtype=poly_use_smooth_dtype) - me.polygons.foreach_get('use_smooth', p_use_smooth_mask) - # Invert to get all flat shaded polygons. - p_flat_mask = np.invert(p_use_smooth_mask, out=p_use_smooth_mask) - # Convert flat shaded polygons to flat shaded loops by repeating each element by the number of sides of - # that polygon. - # Polygon sides can be calculated from the element-wise difference of loop starts appended by the number - # of loops. Alternatively, polygon sides can be retrieved directly from the 'loop_total' attribute of - # polygons, but since we already have t_ls, it tends to be quicker to calculate from t_ls when above - # around 10_000 polygons. - polygon_sides = np.diff(mesh_t_ls_view, append=mesh_loop_nbr) - p_flat_loop_mask = np.repeat(p_flat_mask, polygon_sides) - # Convert flat shaded loops to flat shaded (sharp) edge indices. - # Note that if an edge is in multiple loops that are part of flat shaded faces, its edge index will end - # up in sharp_edge_indices_from_polygons multiple times. - sharp_edge_indices_from_polygons = mesh_t_lei_view[p_flat_loop_mask] - - # - Get sharp edges from edges marked as sharp - e_use_sharp_mask = np.empty(mesh_edge_nbr, dtype=edge_use_sharp_dtype) - me.edges.foreach_get('use_edge_sharp', e_use_sharp_mask) - # - Get sharp edges from edges used by more than two loops (and therefore more than two faces) e_more_than_two_faces_mask = np.bincount(mesh_t_lei_view, minlength=mesh_edge_nbr) > 2 - # - Combine with edges that are sharp because they're in more than two faces - e_use_sharp_mask = np.logical_or(e_use_sharp_mask, e_more_than_two_faces_mask, out=e_use_sharp_mask) + # - Get sharp edges from the "sharp_edge" attribute. The attribute may not exist, in which case, there + # are no edges marked as sharp. + e_use_sharp_mask = MESH_ATTRIBUTE_SHARP_EDGE.get_ndarray(attributes) + if e_use_sharp_mask is not None: + # - Combine with edges that are sharp because they're in more than two faces + e_use_sharp_mask = np.logical_or(e_use_sharp_mask, e_more_than_two_faces_mask, out=e_use_sharp_mask) + else: + e_use_sharp_mask = e_more_than_two_faces_mask - # - Combine with edges that are sharp because a polygon they're in has flat shading - e_use_sharp_mask[sharp_edge_indices_from_polygons] = True + # - Get sharp edges from flat shaded faces + p_flat_mask = MESH_ATTRIBUTE_SHARP_FACE.get_ndarray(attributes) + if p_flat_mask is not None: + # Convert flat shaded polygons to flat shaded loops by repeating each element by the number of sides + # of that polygon. + # Polygon sides can be calculated from the element-wise difference of loop starts appended by the + # number of loops. Alternatively, polygon sides can be retrieved directly from the 'loop_total' + # attribute of polygons, but since we already have t_ls, it tends to be quicker to calculate from + # t_ls. + polygon_sides = np.diff(mesh_t_ls_view, append=mesh_loop_nbr) + p_flat_loop_mask = np.repeat(p_flat_mask, polygon_sides) + # Convert flat shaded loops to flat shaded (sharp) edge indices. + # Note that if an edge is in multiple loops that are part of flat shaded faces, its edge index will + # end up in sharp_edge_indices_from_polygons multiple times. + sharp_edge_indices_from_polygons = mesh_t_lei_view[p_flat_loop_mask] + + # - Combine with edges that are sharp because a polygon they're in has flat shading + e_use_sharp_mask[sharp_edge_indices_from_polygons] = True + del sharp_edge_indices_from_polygons + del p_flat_loop_mask + del polygon_sides + del p_flat_mask # - Convert sharp edges to sharp edge keys (t_pvi) ek_use_sharp_mask = e_use_sharp_mask[t_pvi_edge_indices] @@ -1089,11 +1098,6 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes): t_ps = np.invert(ek_use_sharp_mask, out=ek_use_sharp_mask) del ek_use_sharp_mask del e_use_sharp_mask - del sharp_edge_indices_from_polygons - del p_flat_loop_mask - del polygon_sides - del p_flat_mask - del p_use_smooth_mask del mesh_t_lei_view del mesh_t_ls_view else: diff --git a/io_scene_fbx/import_fbx.py b/io_scene_fbx/import_fbx.py index fd9d835aa..e17ee43ef 100644 --- a/io_scene_fbx/import_fbx.py +++ b/io_scene_fbx/import_fbx.py @@ -1315,25 +1315,29 @@ def blen_read_geom_layer_smooth(fbx_obj, mesh): print("warning skipping sharp edges data, no valid edges...") return False - blen_data = mesh.edges + blen_data = MESH_ATTRIBUTE_SHARP_EDGE.ensure(mesh.attributes).data + fbx_item_size = 1 + assert(fbx_item_size == MESH_ATTRIBUTE_SHARP_EDGE.item_size) blen_read_geom_array_mapped_edge( - mesh, blen_data, "use_edge_sharp", bool, + mesh, blen_data, MESH_ATTRIBUTE_SHARP_EDGE.foreach_attribute, MESH_ATTRIBUTE_SHARP_EDGE.dtype, fbx_layer_data, None, fbx_layer_mapping, fbx_layer_ref, - 1, 1, layer_id, - xform=np.logical_not, + 1, fbx_item_size, layer_id, + xform=np.logical_not, # in FBX, 0 (False) is sharp, but in Blender True is sharp. ) # We only set sharp edges here, not face smoothing itself... mesh.use_auto_smooth = True return False elif fbx_layer_mapping == b'ByPolygon': - blen_data = mesh.polygons + blen_data = MESH_ATTRIBUTE_SHARP_FACE.ensure(mesh.attributes).data + fbx_item_size = 1 + assert(fbx_item_size == MESH_ATTRIBUTE_SHARP_FACE.item_size) return blen_read_geom_array_mapped_polygon( - mesh, blen_data, "use_smooth", bool, + mesh, blen_data, MESH_ATTRIBUTE_SHARP_FACE.foreach_attribute, MESH_ATTRIBUTE_SHARP_FACE.dtype, fbx_layer_data, None, fbx_layer_mapping, fbx_layer_ref, - 1, 1, layer_id, - xform=lambda s: (s != 0), # smoothgroup bitflags, treat as booleans for now + 1, fbx_item_size, layer_id, + xform=lambda s: (s == 0), # smoothgroup bitflags, treat as booleans for now ) else: print("warning layer %r mapping type unsupported: %r" % (fbx_layer.id, fbx_layer_mapping)) @@ -1563,7 +1567,9 @@ def blen_read_geom(fbx_tmpl, fbx_obj, settings): mesh.loops.foreach_get("normal", clnors) if not ok_smooth: - mesh.polygons.foreach_set("use_smooth", np.full(len(mesh.polygons), True, dtype=bool)) + sharp_face = MESH_ATTRIBUTE_SHARP_FACE.get(attributes) + if sharp_face: + attributes.remove(sharp_face) ok_smooth = True # Iterating clnors into a nested tuple first is faster than passing clnors.reshape(-1, 3) directly into @@ -1575,7 +1581,9 @@ def blen_read_geom(fbx_tmpl, fbx_obj, settings): mesh.free_normals_split() if not ok_smooth: - mesh.polygons.foreach_set("use_smooth", np.full(len(mesh.polygons), True, dtype=bool)) + sharp_face = MESH_ATTRIBUTE_SHARP_FACE.get(attributes) + if sharp_face: + attributes.remove(sharp_face) if settings.use_custom_props: blen_read_custom_properties(fbx_obj, mesh, settings) -- 2.30.2