From b1fa311570941f739d8a0c44051583cb25d80706 Mon Sep 17 00:00:00 2001 From: Thomas Barlow Date: Thu, 26 Jan 2023 21:48:45 +0000 Subject: [PATCH] Speed up FBX export of normals/tangents/bitangents with numpy Use buffers matching the C types of the data in foreach_get to avoid having to iterate and cast every single element in the C foreach_getset function. Replace nors_transformed_gen mesh transform helper with numpy version nors_transformed. This patch can slightly change the exported normals/tangent/bitangents when geom_mat_no is set by skipping a cast from double to single precision before casting as float64 (usually double precision): The original code would multiply mathutils.Matrix (single precision) and mathutils.Vector (single precision) together, which casts the Matrix elements to double precision, performs the multiplication, and then casts the result back to single precision. These single precision multiplied vectors would then be cast to float64 to be exported. The new code performs the same cast of the matrix to double precision, but skips the step of casting back to single precision, instead casting directly to float64. Even if the new code were to cast back to single precision float and then to float64 like the original code, there does tend to be a small difference in the result, presumably due to precision error. In most cases, however, it seems that there is no change because geom_mat_no is usually an identity matrix, so both the original and new code end up at the same result. For subdivided default cubes with 1538 to 1572864 vertices: ~5-8 times faster (geom_mat_no is None and tangents are exported) ~8-11 times faster (geom_mat_no is set and tangents are exported) ~13-16 times faster (geom_mat_no is None and no tangents are exported) ~18-21 times faster (geom_mat_no is set and no tangents are exported) --- io_scene_fbx/export_fbx_bin.py | 42 ++++++++++++++++++++-------------- io_scene_fbx/fbx_utils.py | 7 ------ 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/io_scene_fbx/export_fbx_bin.py b/io_scene_fbx/export_fbx_bin.py index 16188eee7..6eacdbbdd 100644 --- a/io_scene_fbx/export_fbx_bin.py +++ b/io_scene_fbx/export_fbx_bin.py @@ -49,7 +49,7 @@ from .fbx_utils import ( units_blender_to_fbx_factor, units_convertor, units_convertor_iter, matrix4_to_array, similar_values, similar_values_iter, astype_view_signedness, fast_first_axis_unique, # Mesh transform helpers. - vcos_transformed_gen, nors_transformed_gen, vcos_transformed, nors_transformed, + vcos_transformed_gen, vcos_transformed, nors_transformed, # UUID from key. get_fbx_uuid_from_key, # Key generators. @@ -1033,28 +1033,35 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes): # but this does not seem well supported by apps currently... me.calc_normals_split() - t_ln = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(me.loops) * 3 + 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_gen(t_ln, geom_mat_no) + t_ln = nors_transformed(t_ln, geom_mat_no, ln_fbx_dtype) if 0: - t_ln = tuple(t_ln) # No choice... :/ - + 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"ReferenceInformationType", b"IndexToDirect") - ln2idx = tuple(set(t_ln)) - elem_data_single_float64_array(lay_nor, b"Normals", chain(*ln2idx)) + # Tuple of unique sorted normals and then the index in the unique sorted normals of each normal in t_ln. + # 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) + + # 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) # Normal weights, no idea what it is. - # t_lnw = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(ln2idx) + # t_lnw = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(t_ln) # elem_data_single_float64_array(lay_nor, b"NormalsW", t_lnw) - ln2idx = {nor: idx for idx, nor in enumerate(ln2idx)} - elem_data_single_int32_array(lay_nor, b"NormalsIndex", (ln2idx[n] for n in t_ln)) + elem_data_single_int32_array(lay_nor, b"NormalsIndex", t_lnidx) - del ln2idx + del t_lnidx # del t_lnw else: lay_nor = elem_data_single_int32(geom, b"LayerElementNormal", 0) @@ -1062,7 +1069,7 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes): 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"ReferenceInformationType", b"Direct") - elem_data_single_float64_array(lay_nor, b"Normals", chain(*t_ln)) + elem_data_single_float64_array(lay_nor, b"Normals", t_ln) # Normal weights, no idea what it is. # t_ln = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(me.loops) # elem_data_single_float64_array(lay_nor, b"NormalsW", t_ln) @@ -1073,9 +1080,10 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes): tspacenumber = len(me.uv_layers) if tspacenumber: # We can only compute tspace on tessellated meshes, need to check that here... - t_lt = [None] * len(me.polygons) + lt_bl_dtype = np.uintc + t_lt = np.empty(len(me.polygons), dtype=lt_bl_dtype) me.polygons.foreach_get("loop_total", t_lt) - if any((lt > 4 for lt in t_lt)): + if (t_lt > 4).any(): del t_lt scene_data.settings.report( {'WARNING'}, @@ -1084,7 +1092,7 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes): else: del t_lt num_loops = len(me.loops) - t_ln = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * num_loops * 3 + t_ln = np.empty(num_loops * 3, dtype=ln_bl_dtype) # t_lnw = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(me.loops) uv_names = [uvlayer.name for uvlayer in me.uv_layers] # Annoying, `me.calc_tangent` errors in case there is no geometry... @@ -1102,7 +1110,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", - chain(*nors_transformed_gen(t_ln, geom_mat_no))) + nors_transformed(t_ln, geom_mat_no, ln_fbx_dtype)) # Binormal weights, no idea what it is. # elem_data_single_float64_array(lay_nor, b"BinormalsW", t_lnw) @@ -1115,7 +1123,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", - chain(*nors_transformed_gen(t_ln, geom_mat_no))) + nors_transformed(t_ln, geom_mat_no, ln_fbx_dtype)) # Tangent weights, no idea what it is. # elem_data_single_float64_array(lay_nor, b"TangentsW", t_lnw) diff --git a/io_scene_fbx/fbx_utils.py b/io_scene_fbx/fbx_utils.py index 9698a3609..816e6b731 100644 --- a/io_scene_fbx/fbx_utils.py +++ b/io_scene_fbx/fbx_utils.py @@ -265,13 +265,6 @@ def vcos_transformed_gen(raw_cos, m=None): gen = zip(*(iter(raw_cos),) * 3) return gen if m is None else (m @ Vector(v) for v in gen) -def nors_transformed_gen(raw_nors, m=None): - # Great, now normals are also expected 4D! - # XXX Back to 3D normals for now! - # gen = zip(*(iter(raw_nors),) * 3 + (_infinite_gen(1.0),)) - gen = zip(*(iter(raw_nors),) * 3) - return gen if m is None else (m @ Vector(v) for v in gen) - def _mat4_vec3_array_multiply(mat4, vec3_array, dtype=None, return_4d=False): """Multiply a 4d matrix by each 3d vector in an array and return as an array of either 3d or 4d vectors. -- 2.30.2