From 9859e253b574f8cf5f70000556fdbdf0f871e564 Mon Sep 17 00:00:00 2001 From: Thomas Barlow Date: Thu, 16 Mar 2023 05:37:00 +0000 Subject: [PATCH] FBX Import: Speed up geometry layers with foreach_set and numpy Replace per-element xform functions with numpy vectorized functions. Setting a stride larger than item_size remains supported despite being unused. Setting a stride smaller than item_size is not implemented. The only specific change to a geometry layer is that vertex and polygon normals are now expanded to loop normals using numpy and foreach_set. The overall speedup will vary depending on what layers are being imported, how many and which mapping is being used for each layer. This can result in a large speedup for imports that have many layers, especially larger layers such as UVs and vertex colors. Since this patch uses foreach_set it does mean that the new functions are limited to importing into blender data that is exposed to the Python API as bpy_prop_collection instances. All existing imported geometry layers meet this criterion. --- io_scene_fbx/import_fbx.py | 412 ++++++++++++++++++++++++++----------- 1 file changed, 293 insertions(+), 119 deletions(-) diff --git a/io_scene_fbx/import_fbx.py b/io_scene_fbx/import_fbx.py index 0f1115c89..f04e0244d 100644 --- a/io_scene_fbx/import_fbx.py +++ b/io_scene_fbx/import_fbx.py @@ -785,87 +785,258 @@ def blen_read_geom_layerinfo(fbx_layer): ) -def blen_read_geom_array_setattr(generator, blen_data, blen_attr, fbx_data, stride, item_size, descr, xform): - """Generic fbx_layer to blen_data setter, generator is expected to yield tuples (ble_idx, fbx_idx).""" - max_blen_idx = len(blen_data) - 1 - max_fbx_idx = len(fbx_data) - 1 - print_error = True +def blen_read_geom_validate_blen_data(blen_data, blen_dtype, item_size): + """Validate blen_data when it's not a bpy_prop_collection. + Returns whether blen_data is a bpy_prop_collection""" + blen_data_is_collection = isinstance(blen_data, bpy.types.bpy_prop_collection) + if not blen_data_is_collection: + if item_size > 1: + assert(len(blen_data.shape) == 2) + assert(blen_data.shape[1] == item_size) + assert(blen_data.dtype == blen_dtype) + return blen_data_is_collection - def check_skip(blen_idx, fbx_idx): - nonlocal print_error - if fbx_idx < 0: # Negative values mean 'skip'. - return True - if blen_idx > max_blen_idx: - if print_error: - print("ERROR: too much data in this Blender layer, compared to elements in mesh, skipping!") - print_error = False - return True - if fbx_idx + item_size - 1 > max_fbx_idx: - if print_error: - print("ERROR: not enough data in this FBX layer, skipping!") - print_error = False - return True - return False - if xform is not None: - if isinstance(blen_data, list): - if item_size == 1: - def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx): - blen_data[blen_idx] = xform(fbx_data[fbx_idx]) - else: - def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx): - blen_data[blen_idx] = xform(fbx_data[fbx_idx:fbx_idx + item_size]) - else: - if item_size == 1: - def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx): - setattr(blen_data[blen_idx], blen_attr, xform(fbx_data[fbx_idx])) - else: - def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx): - setattr(blen_data[blen_idx], blen_attr, xform(fbx_data[fbx_idx:fbx_idx + item_size])) +def blen_read_geom_parse_fbx_data(fbx_data, stride, item_size): + """Parse fbx_data as an array.array into a 2d np.ndarray that shares the same memory, where each row is a single + item""" + # Technically stride < item_size could be supported, but there's probably not a use case for it since it would + # result in a view of the data with self-overlapping memory. + assert(stride >= item_size) + # View the array.array as an np.ndarray. + fbx_data_np = parray_as_ndarray(fbx_data) + + if stride == item_size: + if item_size > 1: + # Need to make sure fbx_data_np has a whole number of items to be able to view item_size elements per row. + items_remainder = len(fbx_data_np) % item_size + if items_remainder: + print("ERROR: not a whole number of items in this FBX layer, skipping the partial item!") + fbx_data_np = fbx_data_np[:-items_remainder] + fbx_data_np = fbx_data_np.reshape(-1, item_size) else: - if isinstance(blen_data, list): - if item_size == 1: - def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx): - blen_data[blen_idx] = fbx_data[fbx_idx] + # Create a view of fbx_data_np that is only the first item_size elements of each stride. Note that the view will + # not be C-contiguous. + stride_remainder = len(fbx_data_np) % stride + if stride_remainder: + if stride_remainder < item_size: + print("ERROR: not a whole number of items in this FBX layer, skipping the partial item!") + # Not enough in the remainder for a full item, so cut off the partial stride + fbx_data_np = fbx_data_np[:-stride_remainder] + # Reshape to one stride per row and then create a view that includes only the first item_size elements + # of each stride. + fbx_data_np = fbx_data_np.reshape(-1, stride)[:, :item_size] else: - def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx): - blen_data[blen_idx] = fbx_data[fbx_idx:fbx_idx + item_size] + print("ERROR: not a whole number of strides in this FBX layer! There are a whole number of items, but" + " this could indicate an error!") + # There is not a whole number of strides, but there is a whole number of items. + # This is a pain to deal with because fbx_data_np.reshape(-1, stride) is not possible. + # A view of just the items can be created using stride_tricks.as_strided by specifying the shape and + # strides of the view manually. + # Extreme care must be taken when using stride_tricks.as_strided because improper usage can result in + # a view that gives access to memory outside the array. + from numpy.lib import stride_tricks + + # fbx_data_np should always start off as flat and C-contiguous. + assert(fbx_data_np.strides == (fbx_data_np.itemsize,)) + + num_whole_strides = len(fbx_data_np) // stride + # Plus the one partial stride that is enough elements for a complete item. + num_items = num_whole_strides + 1 + shape = (num_items, item_size) + + # strides are the number of bytes to step to get to the next element, for each axis. + step_per_item = fbx_data_np.itemsize * stride + step_per_item_element = fbx_data_np.itemsize + strides = (step_per_item, step_per_item_element) + + fbx_data_np = stride_tricks.as_strided(fbx_data_np, shape, strides) else: - if item_size == 1: - def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx): - setattr(blen_data[blen_idx], blen_attr, fbx_data[fbx_idx]) - else: - def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx): - setattr(blen_data[blen_idx], blen_attr, fbx_data[fbx_idx:fbx_idx + item_size]) + # There's a whole number of strides, so first reshape to one stride per row and then create a view that + # includes only the first item_size elements of each stride. + fbx_data_np = fbx_data_np.reshape(-1, stride)[:, :item_size] - for blen_idx, fbx_idx in generator: - if check_skip(blen_idx, fbx_idx): - continue - _process(blen_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx) + return fbx_data_np -# generic generators. -def blen_read_geom_array_gen_allsame(data_len): - return zip(*(range(data_len), (0,) * data_len)) +def blen_read_geom_check_fbx_data_length(blen_data, fbx_data_np, is_indices=False): + """Check that there are the same number of items in blen_data and fbx_data_np. + + Returns a tuple of two elements: + 0: fbx_data_np or, if fbx_data_np contains more items than blen_data, a view of fbx_data_np with the excess + items removed + 1: Whether the returned fbx_data_np contains enough items to completely fill blen_data""" + bl_num_items = len(blen_data) + fbx_num_items = len(fbx_data_np) + enough_data = fbx_num_items >= bl_num_items + if not enough_data: + if is_indices: + print("ERROR: not enough indices in this FBX layer, missing data will be left as default!") + else: + print("ERROR: not enough data in this FBX layer, missing data will be left as default!") + elif fbx_num_items > bl_num_items: + if is_indices: + print("ERROR: too many indices in this FBX layer, skipping excess!") + else: + print("ERROR: too much data in this FBX layer, skipping excess!") + fbx_data_np = fbx_data_np[:bl_num_items] + + return fbx_data_np, enough_data -def blen_read_geom_array_gen_direct(fbx_data, stride): - fbx_data_len = len(fbx_data) - return zip(*(range(fbx_data_len // stride), range(0, fbx_data_len, stride))) +def blen_read_geom_xform(fbx_data_np, xform): + """xform is either None, or a function that takes fbx_data_np as its only positional argument and returns an + np.ndarray with the same total number of elements as fbx_data_np. + It is acceptable for xform to return an array with a different dtype to fbx_data_np. + + Returns xform(fbx_data_np) when xform is not None and ensures the result of xform(fbx_data_np) has the same shape as + fbx_data_np before returning it. + When xform is None, fbx_data_np is returned as is.""" + if xform is not None: + item_size = fbx_data_np.shape[1] + fbx_total_data = fbx_data_np.size + fbx_data_np = xform(fbx_data_np) + # The amount of data should not be changed by xform + assert(fbx_data_np.size == fbx_total_data) + # Ensure fbx_data_np is still item_size elements per row + if len(fbx_data_np.shape) != 2 or fbx_data_np.shape[1] != item_size: + fbx_data_np = fbx_data_np.reshape(-1, item_size) + return fbx_data_np -def blen_read_geom_array_gen_indextodirect(fbx_layer_index, stride): - return ((bi, fi * stride) for bi, fi in enumerate(fbx_layer_index)) +def blen_read_geom_array_foreach_set_direct(blen_data, blen_attr, blen_dtype, fbx_data, stride, item_size, descr, + xform): + """Generic fbx_layer to blen_data foreach setter for Direct layers. + blen_data must be a bpy_prop_collection or 2d np.ndarray whose second axis length is item_size. + fbx_data must be an array.array.""" + fbx_data_np = blen_read_geom_parse_fbx_data(fbx_data, stride, item_size) + fbx_data_np, enough_data = blen_read_geom_check_fbx_data_length(blen_data, fbx_data_np) + fbx_data_np = blen_read_geom_xform(fbx_data_np, xform) + + blen_data_is_collection = blen_read_geom_validate_blen_data(blen_data, blen_dtype, item_size) + + if blen_data_is_collection: + if not enough_data: + blen_total_data = len(blen_data) * item_size + buffer = np.empty(blen_total_data, dtype=blen_dtype) + # It's not clear what values should be used for the missing data, so read the current values into a buffer. + blen_data.foreach_get(blen_attr, buffer) + + # Change the buffer shape to one item per row + buffer.shape = (-1, item_size) + + # Copy the fbx data into the start of the buffer + buffer[:len(fbx_data_np)] = fbx_data_np + else: + # Convert the buffer to the Blender C type of blen_attr + buffer = astype_view_signedness(fbx_data_np, blen_dtype) + + # Set blen_attr of blen_data. The buffer must be flat and C-contiguous, which ravel() ensures + blen_data.foreach_set(blen_attr, buffer.ravel()) + else: + assert(blen_data.size % item_size == 0) + blen_data = blen_data.view() + blen_data.shape = (-1, item_size) + blen_data[:len(fbx_data_np)] = fbx_data_np -def blen_read_geom_array_gen_direct_looptovert(mesh, fbx_data, stride): - fbx_data_len = len(fbx_data) // stride - loops = mesh.loops - for p in mesh.polygons: - for lidx in p.loop_indices: - vidx = loops[lidx].vertex_index - if vidx < fbx_data_len: - yield lidx, vidx * stride +def blen_read_geom_array_foreach_set_indexed(blen_data, blen_attr, blen_dtype, fbx_data, fbx_layer_index, stride, + item_size, descr, xform): + """Generic fbx_layer to blen_data foreach setter for IndexToDirect layers. + blen_data must be a bpy_prop_collection or 2d np.ndarray whose second axis length is item_size. + fbx_data must be an array.array or a 1d np.ndarray.""" + fbx_data_np = blen_read_geom_parse_fbx_data(fbx_data, stride, item_size) + fbx_data_np = blen_read_geom_xform(fbx_data_np, xform) + + # fbx_layer_index is allowed to be a 1d np.ndarray for use with blen_read_geom_array_foreach_set_looptovert. + if not isinstance(fbx_layer_index, np.ndarray): + fbx_layer_index = parray_as_ndarray(fbx_layer_index) + + fbx_layer_index, enough_indices = blen_read_geom_check_fbx_data_length(blen_data, fbx_layer_index, is_indices=True) + + blen_data_is_collection = blen_read_geom_validate_blen_data(blen_data, blen_dtype, item_size) + + blen_data_items_len = len(blen_data) + blen_data_len = blen_data_items_len * item_size + fbx_num_items = len(fbx_data_np) + + # Find all indices that are out of bounds of fbx_data_np. + min_index_inclusive = -fbx_num_items + max_index_inclusive = fbx_num_items - 1 + valid_index_mask = np.equal(fbx_layer_index, fbx_layer_index.clip(min_index_inclusive, max_index_inclusive)) + indices_invalid = not valid_index_mask.all() + + fbx_data_items = fbx_data_np.reshape(-1, item_size) + + if indices_invalid or not enough_indices: + if blen_data_is_collection: + buffer = np.empty(blen_data_len, dtype=blen_dtype) + buffer_item_view = buffer.view() + buffer_item_view.shape = (-1, item_size) + # Since we don't know what the default values should be for the missing data, read the current values into a + # buffer. + blen_data.foreach_get(blen_attr, buffer) + else: + buffer_item_view = blen_data + + if not enough_indices: + # Reduce the length of the view to the same length as the number of indices. + buffer_item_view = buffer_item_view[:len(fbx_layer_index)] + + # Copy the result of indexing fbx_data_items by each element in fbx_layer_index into the buffer. + if indices_invalid: + print("ERROR: indices in this FBX layer out of bounds of the FBX data, skipping invalid indices!") + buffer_item_view[valid_index_mask] = fbx_data_items[fbx_layer_index[valid_index_mask]] + else: + buffer_item_view[:] = fbx_data_items[fbx_layer_index] + + if blen_data_is_collection: + blen_data.foreach_set(blen_attr, buffer.ravel()) + else: + if blen_data_is_collection: + # Cast the buffer to the Blender C type of blen_attr + fbx_data_items = astype_view_signedness(fbx_data_items, blen_dtype) + buffer_items = fbx_data_items[fbx_layer_index] + blen_data.foreach_set(blen_attr, buffer_items.ravel()) + else: + blen_data[:] = fbx_data_items[fbx_layer_index] + + +def blen_read_geom_array_foreach_set_allsame(blen_data, blen_attr, blen_dtype, fbx_data, stride, item_size, descr, + xform): + """Generic fbx_layer to blen_data foreach setter for AllSame layers. + blen_data must be a bpy_prop_collection or 2d np.ndarray whose second axis length is item_size. + fbx_data must be an array.array.""" + fbx_data_np = blen_read_geom_parse_fbx_data(fbx_data, stride, item_size) + fbx_data_np = blen_read_geom_xform(fbx_data_np, xform) + blen_data_is_collection = blen_read_geom_validate_blen_data(blen_data, blen_dtype, item_size) + fbx_items_len = len(fbx_data_np) + blen_items_len = len(blen_data) + + if fbx_items_len < 1: + print("ERROR: not enough data in this FBX layer, skipping!") + return + + if blen_data_is_collection: + # Create an array filled with the value from fbx_data_np + buffer = np.full((blen_items_len, item_size), fbx_data_np[0], dtype=blen_dtype) + + blen_data.foreach_set(blen_attr, buffer.ravel()) + else: + blen_data[:] = fbx_data_np[0] + + +def blen_read_geom_array_foreach_set_looptovert(mesh, blen_data, blen_attr, blen_dtype, fbx_data, stride, item_size, + descr, xform): + """Generic fbx_layer to blen_data foreach setter for polyloop ByVertice layers. + blen_data must be a bpy_prop_collection or 2d np.ndarray whose second axis length is item_size. + fbx_data must be an array.array""" + # The fbx_data is mapped to vertices. To expand fbx_data to polygon loops, get an array of the vertex index of each + # polygon loop that will then be used to index fbx_data + loop_vertex_indices = np.empty(len(mesh.loops), dtype=np.uintc) + mesh.loops.foreach_get("vertex_index", loop_vertex_indices) + blen_read_geom_array_foreach_set_indexed(blen_data, blen_attr, blen_dtype, fbx_data, loop_vertex_indices, stride, + item_size, descr, xform) # generic error printers. @@ -880,7 +1051,7 @@ def blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet=False): def blen_read_geom_array_mapped_vert( - mesh, blen_data, blen_attr, + mesh, blen_data, blen_attr, blen_dtype, fbx_layer_data, fbx_layer_index, fbx_layer_mapping, fbx_layer_ref, stride, item_size, descr, @@ -889,15 +1060,15 @@ def blen_read_geom_array_mapped_vert( if fbx_layer_mapping == b'ByVertice': if fbx_layer_ref == b'Direct': assert(fbx_layer_index is None) - blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data, stride), - blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform) + blen_read_geom_array_foreach_set_direct(blen_data, blen_attr, blen_dtype, fbx_layer_data, stride, item_size, + descr, xform) return True blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet) elif fbx_layer_mapping == b'AllSame': if fbx_layer_ref == b'IndexToDirect': assert(fbx_layer_index is None) - blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data)), - blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform) + blen_read_geom_array_foreach_set_allsame(blen_data, blen_attr, blen_dtype, fbx_layer_data, stride, + item_size, descr, xform) return True blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet) else: @@ -907,7 +1078,7 @@ def blen_read_geom_array_mapped_vert( def blen_read_geom_array_mapped_edge( - mesh, blen_data, blen_attr, + mesh, blen_data, blen_attr, blen_dtype, fbx_layer_data, fbx_layer_index, fbx_layer_mapping, fbx_layer_ref, stride, item_size, descr, @@ -915,15 +1086,15 @@ def blen_read_geom_array_mapped_edge( ): if fbx_layer_mapping == b'ByEdge': if fbx_layer_ref == b'Direct': - blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data, stride), - blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform) + blen_read_geom_array_foreach_set_direct(blen_data, blen_attr, blen_dtype, fbx_layer_data, stride, item_size, + descr, xform) return True blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet) elif fbx_layer_mapping == b'AllSame': if fbx_layer_ref == b'IndexToDirect': assert(fbx_layer_index is None) - blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data)), - blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform) + blen_read_geom_array_foreach_set_allsame(blen_data, blen_attr, blen_dtype, fbx_layer_data, stride, + item_size, descr, xform) return True blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet) else: @@ -933,7 +1104,7 @@ def blen_read_geom_array_mapped_edge( def blen_read_geom_array_mapped_polygon( - mesh, blen_data, blen_attr, + mesh, blen_data, blen_attr, blen_dtype, fbx_layer_data, fbx_layer_index, fbx_layer_mapping, fbx_layer_ref, stride, item_size, descr, @@ -945,22 +1116,22 @@ def blen_read_geom_array_mapped_polygon( # We fallback to 'Direct' mapping in this case. #~ assert(fbx_layer_index is not None) if fbx_layer_index is None: - blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data, stride), - blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform) + blen_read_geom_array_foreach_set_direct(blen_data, blen_attr, blen_dtype, fbx_layer_data, stride, + item_size, descr, xform) else: - blen_read_geom_array_setattr(blen_read_geom_array_gen_indextodirect(fbx_layer_index, stride), - blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform) + blen_read_geom_array_foreach_set_indexed(blen_data, blen_attr, blen_dtype, fbx_layer_data, + fbx_layer_index, stride, item_size, descr, xform) return True elif fbx_layer_ref == b'Direct': - blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data, stride), - blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform) + blen_read_geom_array_foreach_set_direct(blen_data, blen_attr, blen_dtype, fbx_layer_data, stride, item_size, + descr, xform) return True blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet) elif fbx_layer_mapping == b'AllSame': if fbx_layer_ref == b'IndexToDirect': assert(fbx_layer_index is None) - blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data)), - blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform) + blen_read_geom_array_foreach_set_allsame(blen_data, blen_attr, blen_dtype, fbx_layer_data, stride, + item_size, descr, xform) return True blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet) else: @@ -970,7 +1141,7 @@ def blen_read_geom_array_mapped_polygon( def blen_read_geom_array_mapped_polyloop( - mesh, blen_data, blen_attr, + mesh, blen_data, blen_attr, blen_dtype, fbx_layer_data, fbx_layer_index, fbx_layer_mapping, fbx_layer_ref, stride, item_size, descr, @@ -982,29 +1153,29 @@ def blen_read_geom_array_mapped_polyloop( # We fallback to 'Direct' mapping in this case. #~ assert(fbx_layer_index is not None) if fbx_layer_index is None: - blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data, stride), - blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform) + blen_read_geom_array_foreach_set_direct(blen_data, blen_attr, blen_dtype, fbx_layer_data, stride, + item_size, descr, xform) else: - blen_read_geom_array_setattr(blen_read_geom_array_gen_indextodirect(fbx_layer_index, stride), - blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform) + blen_read_geom_array_foreach_set_indexed(blen_data, blen_attr, blen_dtype, fbx_layer_data, + fbx_layer_index, stride, item_size, descr, xform) return True elif fbx_layer_ref == b'Direct': - blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data, stride), - blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform) + blen_read_geom_array_foreach_set_direct(blen_data, blen_attr, blen_dtype, fbx_layer_data, stride, item_size, + descr, xform) return True blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet) elif fbx_layer_mapping == b'ByVertice': if fbx_layer_ref == b'Direct': assert(fbx_layer_index is None) - blen_read_geom_array_setattr(blen_read_geom_array_gen_direct_looptovert(mesh, fbx_layer_data, stride), - blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform) + blen_read_geom_array_foreach_set_looptovert(mesh, blen_data, blen_attr, blen_dtype, fbx_layer_data, stride, + item_size, descr, xform) return True blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet) elif fbx_layer_mapping == b'AllSame': if fbx_layer_ref == b'IndexToDirect': assert(fbx_layer_index is None) - blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data)), - blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform) + blen_read_geom_array_foreach_set_allsame(blen_data, blen_attr, blen_dtype, fbx_layer_data, stride, + item_size, descr, xform) return True blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet) else: @@ -1029,7 +1200,7 @@ def blen_read_geom_layer_material(fbx_obj, mesh): blen_data = mesh.polygons blen_read_geom_array_mapped_polygon( - mesh, blen_data, "material_index", + mesh, blen_data, "material_index", np.uintc, fbx_layer_data, None, fbx_layer_mapping, fbx_layer_ref, 1, 1, layer_id, @@ -1063,7 +1234,7 @@ def blen_read_geom_layer_uv(fbx_obj, mesh): continue blen_read_geom_array_mapped_polyloop( - mesh, blen_data, "uv", + mesh, blen_data, "uv", np.single, fbx_layer_data, fbx_layer_index, fbx_layer_mapping, fbx_layer_ref, 2, 2, layer_id, @@ -1103,7 +1274,7 @@ def blen_read_geom_layer_color(fbx_obj, mesh, colors_type): continue blen_read_geom_array_mapped_polyloop( - mesh, blen_data, color_prop_name, + mesh, blen_data, color_prop_name, np.single, fbx_layer_data, fbx_layer_index, fbx_layer_mapping, fbx_layer_ref, 4, 4, layer_id, @@ -1137,11 +1308,11 @@ def blen_read_geom_layer_smooth(fbx_obj, mesh): blen_data = mesh.edges blen_read_geom_array_mapped_edge( - mesh, blen_data, "use_edge_sharp", + mesh, blen_data, "use_edge_sharp", bool, fbx_layer_data, None, fbx_layer_mapping, fbx_layer_ref, 1, 1, layer_id, - xform=lambda s: not s, + xform=np.logical_not, ) # We only set sharp edges here, not face smoothing itself... mesh.use_auto_smooth = True @@ -1149,7 +1320,7 @@ def blen_read_geom_layer_smooth(fbx_obj, mesh): elif fbx_layer_mapping == b'ByPolygon': blen_data = mesh.polygons return blen_read_geom_array_mapped_polygon( - mesh, blen_data, "use_smooth", + mesh, blen_data, "use_smooth", bool, fbx_layer_data, None, fbx_layer_mapping, fbx_layer_ref, 1, 1, layer_id, @@ -1160,8 +1331,6 @@ def blen_read_geom_layer_smooth(fbx_obj, mesh): return False def blen_read_geom_layer_edge_crease(fbx_obj, mesh): - from math import sqrt - fbx_layer = elem_find_first(fbx_obj, b'LayerElementEdgeCrease') if fbx_layer is None: @@ -1192,13 +1361,13 @@ def blen_read_geom_layer_edge_crease(fbx_obj, mesh): blen_data = mesh.edges return blen_read_geom_array_mapped_edge( - mesh, blen_data, "crease", + mesh, blen_data, "crease", np.single, fbx_layer_data, None, fbx_layer_mapping, fbx_layer_ref, 1, 1, layer_id, # Blender squares those values before sending them to OpenSubdiv, when other software don't, # so we need to compensate that to get similar results through FBX... - xform=sqrt, + xform=np.sqrt, ) else: print("warning layer %r mapping type unsupported: %r" % (fbx_layer.id, fbx_layer_mapping)) @@ -1223,22 +1392,28 @@ def blen_read_geom_layer_normal(fbx_obj, mesh, xform=None): print("warning %r %r missing data" % (layer_id, fbx_layer_name)) return False - # try loops, then vertices. + # Normals are temporarily set here so that they can be retrieved again after a call to Mesh.validate(). + bl_norm_dtype = np.single + item_size = 3 + # try loops, then polygons, then vertices. tries = ((mesh.loops, "Loops", False, blen_read_geom_array_mapped_polyloop), (mesh.polygons, "Polygons", True, blen_read_geom_array_mapped_polygon), (mesh.vertices, "Vertices", True, blen_read_geom_array_mapped_vert)) for blen_data, blen_data_type, is_fake, func in tries: - bdata = [None] * len(blen_data) if is_fake else blen_data - if func(mesh, bdata, "normal", - fbx_layer_data, fbx_layer_index, fbx_layer_mapping, fbx_layer_ref, 3, 3, layer_id, xform, True): + bdata = np.zeros((len(blen_data), item_size), dtype=bl_norm_dtype) if is_fake else blen_data + if func(mesh, bdata, "normal", bl_norm_dtype, + fbx_layer_data, fbx_layer_index, fbx_layer_mapping, fbx_layer_ref, 3, item_size, layer_id, xform, True): if blen_data_type == "Polygons": - for pidx, p in enumerate(mesh.polygons): - for lidx in range(p.loop_start, p.loop_start + p.loop_total): - mesh.loops[lidx].normal[:] = bdata[pidx] + # To expand to per-loop normals, repeat each per-polygon normal by the number of loops of each polygon. + poly_loop_totals = np.empty(len(mesh.polygons), dtype=np.uintc) + mesh.polygons.foreach_get("loop_total", poly_loop_totals) + loop_normals = np.repeat(bdata, poly_loop_totals, axis=0) + mesh.loops.foreach_set("normal", loop_normals.ravel()) elif blen_data_type == "Vertices": # We have to copy vnors to lnors! Far from elegant, but simple. - for l in mesh.loops: - l.normal[:] = bdata[l.vertex_index] + loop_vertex_indices = np.empty(len(mesh.loops), dtype=np.uintc) + mesh.loops.foreach_get("vertex_index", loop_vertex_indices) + mesh.loops.foreach_set("normal", bdata[loop_vertex_indices].ravel()) return True blen_read_geom_array_error_mapping("normal", fbx_layer_mapping) @@ -1348,9 +1523,8 @@ def blen_read_geom(fbx_tmpl, fbx_obj, settings): if geom_mat_no is None: ok_normals = blen_read_geom_layer_normal(fbx_obj, mesh) else: - def nortrans(v): - return geom_mat_no @ Vector(v) - ok_normals = blen_read_geom_layer_normal(fbx_obj, mesh, nortrans) + ok_normals = blen_read_geom_layer_normal(fbx_obj, mesh, + lambda v_array: nors_transformed(v_array, geom_mat_no)) mesh.validate(clean_customdata=False) # *Very* important to not remove lnors here! -- 2.30.2