From f78321c58c1364c874b750472003a32e61f91c83 Mon Sep 17 00:00:00 2001 From: Thomas Barlow Date: Sun, 22 Oct 2023 18:26:35 +0100 Subject: [PATCH 1/3] FBX IO: Export normals matching the mesh's normals_domain The appropriate normals array and FBX layer mapping are now used according to the Mesh.normals_domain property. This results in slightly faster exports with smaller file size when meshes are either fully smooth shaded or fully flat shaded because the FACE and POINT domains almost always have fewer normals than the CORNER domain and never have more (a fully smooth shaded mesh where each face has its own vertices would be POINT domain with as many vertices as corners). Additionally, getting the normals from the new Mesh.corner_normals property runs in about 60% of the time of getting the normals through MeshLoop.normal. For me with a subdivided cube with 185856 corners: MeshLoop.normal: ~6.7ms Mesh.corner_normals: ~4.0ms Import is unaffected, because only custom split normals can be set. --- io_scene_fbx/export_fbx_bin.py | 56 ++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/io_scene_fbx/export_fbx_bin.py b/io_scene_fbx/export_fbx_bin.py index 9592a7607..876c91d65 100644 --- a/io_scene_fbx/export_fbx_bin.py +++ b/io_scene_fbx/export_fbx_bin.py @@ -1155,34 +1155,52 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes): # Loop normals. tspacenumber = 0 if write_normals: - # NOTE: this is not supported by importer currently. + # NOTE: ByVertice-IndexToDirect is not supported by the importer currently. # XXX Official docs says normals should use IndexToDirect, # but this does not seem well supported by apps currently... - ln_bl_dtype = np.single - ln_fbx_dtype = np.float64 - t_ln = np.empty(len(me.loops) * 3, dtype=ln_bl_dtype) - me.loops.foreach_get("normal", t_ln) - t_ln = nors_transformed(t_ln, geom_mat_no, ln_fbx_dtype) + normal_bl_dtype = np.single + normal_fbx_dtype = np.float64 + match me.normals_domain: + case 'POINT': + # All faces are smooth shaded, so we can get normals from the vertices. + normal_source = me.vertex_normals + normal_mapping = b"ByVertice" + case 'FACE': + # Either all faces or all edges are sharp, so we can get normals from the faces. + normal_source = me.polygon_normals + normal_mapping = b"ByPolygon" + case 'CORNER': + # We have a mix of sharp/smooth edges/faces or custom split normals, so need to get normals from + # corners. + normal_source = me.corner_normals + normal_mapping = b"ByPolygonVertex" + case _: + # Unreachable + raise AssertionError("Unexpected normals domain '%s'" % me.normals_domain) + # Each normal has 3 components, so the length is multiplied by 3. + t_normal = np.empty(len(normal_source) * 3, dtype=normal_bl_dtype) + normal_source.foreach_get("vector", t_normal) + t_normal = nors_transformed(t_normal, geom_mat_no, normal_fbx_dtype) if 0: lnidx_fbx_dtype = np.int32 lay_nor = elem_data_single_int32(geom, b"LayerElementNormal", 0) elem_data_single_int32(lay_nor, b"Version", FBX_GEOMETRY_NORMAL_VERSION) elem_data_single_string(lay_nor, b"Name", b"") - elem_data_single_string(lay_nor, b"MappingInformationType", b"ByPolygonVertex") + elem_data_single_string(lay_nor, b"MappingInformationType", normal_mapping) elem_data_single_string(lay_nor, b"ReferenceInformationType", b"IndexToDirect") - # Tuple of unique sorted normals and then the index in the unique sorted normals of each normal in t_ln. + # Tuple of unique sorted normals and then the index in the unique sorted normals of each normal in t_normal. # Since we don't care about how the normals are sorted, only that they're unique, we can use the fast unique # helper function. - t_ln, t_lnidx = fast_first_axis_unique(t_ln.reshape(-1, 3), return_inverse=True) + t_normal, t_lnidx = fast_first_axis_unique(t_normal.reshape(-1, 3), return_inverse=True) # Convert to the type for fbx t_lnidx = astype_view_signedness(t_lnidx, lnidx_fbx_dtype) - elem_data_single_float64_array(lay_nor, b"Normals", t_ln) + elem_data_single_float64_array(lay_nor, b"Normals", t_normal) # Normal weights, no idea what it is. - # t_lnw = np.zeros(len(t_ln), dtype=np.float64) + # t_lnw = np.zeros(len(t_normal), dtype=np.float64) # elem_data_single_float64_array(lay_nor, b"NormalsW", t_lnw) elem_data_single_int32_array(lay_nor, b"NormalsIndex", t_lnidx) @@ -1193,13 +1211,13 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes): lay_nor = elem_data_single_int32(geom, b"LayerElementNormal", 0) elem_data_single_int32(lay_nor, b"Version", FBX_GEOMETRY_NORMAL_VERSION) elem_data_single_string(lay_nor, b"Name", b"") - elem_data_single_string(lay_nor, b"MappingInformationType", b"ByPolygonVertex") + elem_data_single_string(lay_nor, b"MappingInformationType", normal_mapping) elem_data_single_string(lay_nor, b"ReferenceInformationType", b"Direct") - elem_data_single_float64_array(lay_nor, b"Normals", t_ln) + elem_data_single_float64_array(lay_nor, b"Normals", t_normal) # Normal weights, no idea what it is. - # t_ln = np.zeros(len(me.loops), dtype=np.float64) - # elem_data_single_float64_array(lay_nor, b"NormalsW", t_ln) - del t_ln + # t_normal = np.zeros(len(me.loops), dtype=np.float64) + # elem_data_single_float64_array(lay_nor, b"NormalsW", t_normal) + del t_normal # tspace if scene_data.settings.use_tspace: @@ -1218,7 +1236,7 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes): else: del t_lt num_loops = len(me.loops) - t_ln = np.empty(num_loops * 3, dtype=ln_bl_dtype) + t_ln = np.empty(num_loops * 3, dtype=normal_bl_dtype) # t_lnw = np.zeros(len(me.loops), dtype=np.float64) uv_names = [uvlayer.name for uvlayer in me.uv_layers] # Annoying, `me.calc_tangent` errors in case there is no geometry... @@ -1236,7 +1254,7 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes): elem_data_single_string(lay_nor, b"MappingInformationType", b"ByPolygonVertex") elem_data_single_string(lay_nor, b"ReferenceInformationType", b"Direct") elem_data_single_float64_array(lay_nor, b"Binormals", - nors_transformed(t_ln, geom_mat_no, ln_fbx_dtype)) + nors_transformed(t_ln, geom_mat_no, normal_fbx_dtype)) # Binormal weights, no idea what it is. # elem_data_single_float64_array(lay_nor, b"BinormalsW", t_lnw) @@ -1249,7 +1267,7 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes): elem_data_single_string(lay_nor, b"MappingInformationType", b"ByPolygonVertex") elem_data_single_string(lay_nor, b"ReferenceInformationType", b"Direct") elem_data_single_float64_array(lay_nor, b"Tangents", - nors_transformed(t_ln, geom_mat_no, ln_fbx_dtype)) + nors_transformed(t_ln, geom_mat_no, normal_fbx_dtype)) # Tangent weights, no idea what it is. # elem_data_single_float64_array(lay_nor, b"TangentsW", t_lnw) -- 2.30.2 From a811d2fce8311f113fb0e340a61403fecb8510a3 Mon Sep 17 00:00:00 2001 From: Thomas Barlow Date: Fri, 3 Nov 2023 13:13:44 +0000 Subject: [PATCH 2/3] Rename missed unused variables Since the normals are no longer always loop/corner normals the "l" in the variable names doesn't make sense anymore. A few variable names were missed in the disabled "IndexToDirect" code and comments. --- io_scene_fbx/export_fbx_bin.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/io_scene_fbx/export_fbx_bin.py b/io_scene_fbx/export_fbx_bin.py index 876c91d65..2b384cae8 100644 --- a/io_scene_fbx/export_fbx_bin.py +++ b/io_scene_fbx/export_fbx_bin.py @@ -1183,7 +1183,7 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes): normal_source.foreach_get("vector", t_normal) t_normal = nors_transformed(t_normal, geom_mat_no, normal_fbx_dtype) if 0: - lnidx_fbx_dtype = np.int32 + normal_idx_fbx_dtype = np.int32 lay_nor = elem_data_single_int32(geom, b"LayerElementNormal", 0) elem_data_single_int32(lay_nor, b"Version", FBX_GEOMETRY_NORMAL_VERSION) elem_data_single_string(lay_nor, b"Name", b"") @@ -1193,20 +1193,20 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes): # Tuple of unique sorted normals and then the index in the unique sorted normals of each normal in t_normal. # Since we don't care about how the normals are sorted, only that they're unique, we can use the fast unique # helper function. - t_normal, t_lnidx = fast_first_axis_unique(t_normal.reshape(-1, 3), return_inverse=True) + t_normal, t_normal_idx = fast_first_axis_unique(t_normal.reshape(-1, 3), return_inverse=True) # Convert to the type for fbx - t_lnidx = astype_view_signedness(t_lnidx, lnidx_fbx_dtype) + t_normal_idx = astype_view_signedness(t_normal_idx, normal_idx_fbx_dtype) elem_data_single_float64_array(lay_nor, b"Normals", t_normal) # Normal weights, no idea what it is. - # t_lnw = np.zeros(len(t_normal), dtype=np.float64) - # elem_data_single_float64_array(lay_nor, b"NormalsW", t_lnw) + # t_normal_w = np.zeros(len(t_normal), dtype=np.float64) + # elem_data_single_float64_array(lay_nor, b"NormalsW", t_normal_w) - elem_data_single_int32_array(lay_nor, b"NormalsIndex", t_lnidx) + elem_data_single_int32_array(lay_nor, b"NormalsIndex", t_normal_idx) - del t_lnidx - # del t_lnw + del t_normal_idx + # del t_normal_w else: lay_nor = elem_data_single_int32(geom, b"LayerElementNormal", 0) elem_data_single_int32(lay_nor, b"Version", FBX_GEOMETRY_NORMAL_VERSION) -- 2.30.2 From 09ac5449e60808fc4cd6ea15dae1a0ab526ee7ee Mon Sep 17 00:00:00 2001 From: Thomas Barlow Date: Fri, 3 Nov 2023 15:17:28 +0000 Subject: [PATCH 3/3] Increase FBX minor version This patch changes the output format of normals in some cases, so is worth a minor version increase. --- io_scene_fbx/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/io_scene_fbx/__init__.py b/io_scene_fbx/__init__.py index baf4fead9..44b2e99eb 100644 --- a/io_scene_fbx/__init__.py +++ b/io_scene_fbx/__init__.py @@ -5,7 +5,7 @@ bl_info = { "name": "FBX format", "author": "Campbell Barton, Bastien Montagne, Jens Restemeier, @Mysteryem", - "version": (5, 9, 1), + "version": (5, 10, 0), "blender": (4, 1, 0), "location": "File > Import-Export", "description": "FBX IO meshes, UVs, vertex colors, materials, textures, cameras, lamps and actions", -- 2.30.2