FBX Import: Speed up shape keys with numpy #104491
@ -461,8 +461,9 @@ def add_vgroup_to_objects(vg_indices, vg_weights, vg_name, objects):
|
|||||||
vg = obj.vertex_groups.get(vg_name)
|
vg = obj.vertex_groups.get(vg_name)
|
||||||
if vg is None:
|
if vg is None:
|
||||||
vg = obj.vertex_groups.new(name=vg_name)
|
vg = obj.vertex_groups.new(name=vg_name)
|
||||||
|
vg_add = vg.add
|
||||||
for i, w in zip(vg_indices, vg_weights):
|
for i, w in zip(vg_indices, vg_weights):
|
||||||
vg.add((i,), w, 'REPLACE')
|
vg_add((i,), w, 'REPLACE')
|
||||||
|
|
||||||
|
|
||||||
def blen_read_object_transform_preprocess(fbx_props, fbx_obj, rot_alt_mat, use_prepost_rot):
|
def blen_read_object_transform_preprocess(fbx_props, fbx_obj, rot_alt_mat, use_prepost_rot):
|
||||||
@ -1378,46 +1379,78 @@ def blen_read_geom(fbx_tmpl, fbx_obj, settings):
|
|||||||
return mesh
|
return mesh
|
||||||
|
|
||||||
|
|
||||||
def blen_read_shape(fbx_tmpl, fbx_sdata, fbx_bcdata, meshes, scene):
|
def blen_read_shapes(fbx_tmpl, fbx_data, objects, me, scene):
|
||||||
|
if not fbx_data:
|
||||||
|
# No shape key data. Nothing to do.
|
||||||
|
return
|
||||||
|
|
||||||
|
bl_vcos_dtype = np.single
|
||||||
|
me_vcos = np.empty(len(me.vertices) * 3, dtype=bl_vcos_dtype)
|
||||||
|
me.vertices.foreach_get("co", me_vcos)
|
||||||
|
me_vcos_vector_view = me_vcos.reshape(-1, 3)
|
||||||
|
|
||||||
|
objects = list({node.bl_obj for node in objects})
|
||||||
|
assert(objects)
|
||||||
|
|
||||||
|
bc_uuid_to_keyblocks = {}
|
||||||
|
for bc_uuid, fbx_sdata, fbx_bcdata in fbx_data:
|
||||||
elem_name_utf8 = elem_name_ensure_class(fbx_sdata, b'Geometry')
|
elem_name_utf8 = elem_name_ensure_class(fbx_sdata, b'Geometry')
|
||||||
indices = elem_prop_first(elem_find_first(fbx_sdata, b'Indexes'), default=())
|
indices = elem_prop_first(elem_find_first(fbx_sdata, b'Indexes'))
|
||||||
dvcos = tuple(co for co in zip(*[iter(elem_prop_first(elem_find_first(fbx_sdata, b'Vertices'), default=()))] * 3))
|
dvcos = elem_prop_first(elem_find_first(fbx_sdata, b'Vertices'))
|
||||||
|
|
||||||
|
indices = parray_as_ndarray(indices) if indices else np.empty(0, dtype=data_types.ARRAY_INT32)
|
||||||
|
dvcos = parray_as_ndarray(dvcos) if dvcos else np.empty(0, dtype=data_types.ARRAY_FLOAT64)
|
||||||
|
|
||||||
|
# If there's not a whole number of vectors, trim off the remainder.
|
||||||
|
# 3 components per vector.
|
||||||
|
remainder = len(dvcos) % 3
|
||||||
|
if remainder:
|
||||||
|
dvcos = dvcos[:-remainder]
|
||||||
|
dvcos = dvcos.reshape(-1, 3)
|
||||||
|
|
||||||
# We completely ignore normals here!
|
# We completely ignore normals here!
|
||||||
weight = elem_prop_first(elem_find_first(fbx_bcdata, b'DeformPercent'), default=100.0) / 100.0
|
weight = elem_prop_first(elem_find_first(fbx_bcdata, b'DeformPercent'), default=100.0) / 100.0
|
||||||
vgweights = tuple(vgw / 100.0 for vgw in elem_prop_first(elem_find_first(fbx_bcdata, b'FullWeights'), default=()))
|
|
||||||
|
vgweights = elem_prop_first(elem_find_first(fbx_bcdata, b'FullWeights'))
|
||||||
|
vgweights = parray_as_ndarray(vgweights) if vgweights else np.empty(0, dtype=data_types.ARRAY_FLOAT64)
|
||||||
|
# Not doing the division in-place in-case it's possible for FBX shape keys to be used by more than one mesh.
|
||||||
|
vgweights = vgweights / 100.0
|
||||||
|
|
||||||
|
create_vg = (vgweights != 1.0).any()
|
||||||
|
|
||||||
# Special case, in case all weights are the same, FullWeight can have only one element - *sigh!*
|
# Special case, in case all weights are the same, FullWeight can have only one element - *sigh!*
|
||||||
nbr_indices = len(indices)
|
nbr_indices = len(indices)
|
||||||
if len(vgweights) == 1 and nbr_indices > 1:
|
if len(vgweights) == 1 and nbr_indices > 1:
|
||||||
vgweights = (vgweights[0],) * nbr_indices
|
vgweights = np.full_like(indices, vgweights[0], dtype=vgweights.dtype)
|
||||||
|
|
||||||
assert(len(vgweights) == nbr_indices == len(dvcos))
|
assert(len(vgweights) == nbr_indices == len(dvcos))
|
||||||
create_vg = bool(set(vgweights) - {1.0})
|
|
||||||
|
|
||||||
keyblocks = []
|
|
||||||
|
|
||||||
for me, objects in meshes:
|
|
||||||
vcos = tuple((idx, me.vertices[idx].co + Vector(dvco)) for idx, dvco in zip(indices, dvcos))
|
|
||||||
objects = list({node.bl_obj for node in objects})
|
|
||||||
assert(objects)
|
|
||||||
|
|
||||||
|
# To add shape keys to the mesh, an Object using the mesh is needed.
|
||||||
if me.shape_keys is None:
|
if me.shape_keys is None:
|
||||||
|
|||||||
objects[0].shape_key_add(name="Basis", from_mix=False)
|
objects[0].shape_key_add(name="Basis", from_mix=False)
|
||||||
kb = objects[0].shape_key_add(name=elem_name_utf8, from_mix=False)
|
kb = objects[0].shape_key_add(name=elem_name_utf8, from_mix=False)
|
||||||
me.shape_keys.use_relative = True # Should already be set as such.
|
me.shape_keys.use_relative = True # Should already be set as such.
|
||||||
|
|
||||||
for idx, co in vcos:
|
# Only need to set the shape key co if there are any non-zero dvcos.
|
||||||
kb.data[idx].co[:] = co
|
if dvcos.any():
|
||||||
|
shape_cos = me_vcos_vector_view.copy()
|
||||||
|
shape_cos[indices] += dvcos
|
||||||
|
kb.data.foreach_set("co", shape_cos.ravel())
|
||||||
|
|
||||||
kb.value = weight
|
kb.value = weight
|
||||||
|
|
||||||
# Add vgroup if necessary.
|
# Add vgroup if necessary.
|
||||||
if create_vg:
|
if create_vg:
|
||||||
vgoups = add_vgroup_to_objects(indices, vgweights, kb.name, objects)
|
# VertexGroup.add only allows sequences of int indices, but iterating the indices array directly would
|
||||||
|
# produce numpy scalars of types such as np.int32. The underlying memoryview of the indices array, however,
|
||||||
|
# does produce standard Python ints when iterated, so pass indices.data to add_vgroup_to_objects instead of
|
||||||
|
# indices.
|
||||||
|
# memoryviews tend to be faster to iterate than numpy arrays anyway, so vgweights.data is passed too.
|
||||||
|
add_vgroup_to_objects(indices.data, vgweights.data, kb.name, objects)
|
||||||
kb.vertex_group = kb.name
|
kb.vertex_group = kb.name
|
||||||
|
|
||||||
keyblocks.append(kb)
|
bc_uuid_to_keyblocks.setdefault(bc_uuid, []).append(kb)
|
||||||
|
return bc_uuid_to_keyblocks
|
||||||
return keyblocks
|
|
||||||
|
|
||||||
|
|
||||||
# --------
|
# --------
|
||||||
@ -2868,6 +2901,7 @@ def load(operator, context, filepath="",
|
|||||||
def _():
|
def _():
|
||||||
fbx_tmpl = fbx_template_get((b'Geometry', b'KFbxShape'))
|
fbx_tmpl = fbx_template_get((b'Geometry', b'KFbxShape'))
|
||||||
|
|
||||||
|
mesh_to_shapes = {}
|
||||||
for s_uuid, s_item in fbx_table_nodes.items():
|
for s_uuid, s_item in fbx_table_nodes.items():
|
||||||
fbx_sdata, bl_sdata = s_item = fbx_table_nodes.get(s_uuid, (None, None))
|
fbx_sdata, bl_sdata = s_item = fbx_table_nodes.get(s_uuid, (None, None))
|
||||||
if fbx_sdata is None or fbx_sdata.id != b'Geometry' or fbx_sdata.props[2] != b'Shape':
|
if fbx_sdata is None or fbx_sdata.id != b'Geometry' or fbx_sdata.props[2] != b'Shape':
|
||||||
@ -2880,8 +2914,6 @@ def load(operator, context, filepath="",
|
|||||||
fbx_bcdata, _bl_bcdata = fbx_table_nodes.get(bc_uuid, (None, None))
|
fbx_bcdata, _bl_bcdata = fbx_table_nodes.get(bc_uuid, (None, None))
|
||||||
if fbx_bcdata is None or fbx_bcdata.id != b'Deformer' or fbx_bcdata.props[2] != b'BlendShapeChannel':
|
if fbx_bcdata is None or fbx_bcdata.id != b'Deformer' or fbx_bcdata.props[2] != b'BlendShapeChannel':
|
||||||
continue
|
continue
|
||||||
meshes = []
|
|
||||||
objects = []
|
|
||||||
for bs_uuid, bs_ctype in fbx_connection_map.get(bc_uuid, ()):
|
for bs_uuid, bs_ctype in fbx_connection_map.get(bc_uuid, ()):
|
||||||
if bs_ctype.props[0] != b'OO':
|
if bs_ctype.props[0] != b'OO':
|
||||||
continue
|
continue
|
||||||
@ -2896,6 +2928,9 @@ def load(operator, context, filepath="",
|
|||||||
continue
|
continue
|
||||||
# Blenmeshes are assumed already created at that time!
|
# Blenmeshes are assumed already created at that time!
|
||||||
assert(isinstance(bl_mdata, bpy.types.Mesh))
|
assert(isinstance(bl_mdata, bpy.types.Mesh))
|
||||||
|
# Group shapes by mesh so that each mesh only needs to be processed once for all of its shape
|
||||||
|
# keys.
|
||||||
|
if bl_mdata not in mesh_to_shapes:
|
||||||
# And we have to find all objects using this mesh!
|
# And we have to find all objects using this mesh!
|
||||||
objects = []
|
objects = []
|
||||||
for o_uuid, o_ctype in fbx_connection_map.get(m_uuid, ()):
|
for o_uuid, o_ctype in fbx_connection_map.get(m_uuid, ()):
|
||||||
@ -2904,12 +2939,18 @@ def load(operator, context, filepath="",
|
|||||||
node = fbx_helper_nodes[o_uuid]
|
node = fbx_helper_nodes[o_uuid]
|
||||||
if node:
|
if node:
|
||||||
objects.append(node)
|
objects.append(node)
|
||||||
meshes.append((bl_mdata, objects))
|
shapes_list = []
|
||||||
|
mesh_to_shapes[bl_mdata] = (objects, shapes_list)
|
||||||
|
else:
|
||||||
|
shapes_list = mesh_to_shapes[bl_mdata][1]
|
||||||
|
shapes_list.append((bc_uuid, fbx_sdata, fbx_bcdata))
|
||||||
# BlendShape deformers are only here to connect BlendShapeChannels to meshes, nothing else to do.
|
# BlendShape deformers are only here to connect BlendShapeChannels to meshes, nothing else to do.
|
||||||
|
|
||||||
|
# Iterate through each mesh and create its shape keys
|
||||||
|
for bl_mdata, (objects, shapes) in mesh_to_shapes.items():
|
||||||
|
for bc_uuid, keyblocks in blen_read_shapes(fbx_tmpl, shapes, objects, bl_mdata, scene).items():
|
||||||
# keyblocks is a list of tuples (mesh, keyblock) matching that shape/blendshapechannel, for animation.
|
# keyblocks is a list of tuples (mesh, keyblock) matching that shape/blendshapechannel, for animation.
|
||||||
keyblocks = blen_read_shape(fbx_tmpl, fbx_sdata, fbx_bcdata, meshes, scene)
|
blend_shape_channels.setdefault(bc_uuid, []).extend(keyblocks)
|
||||||
blend_shape_channels[bc_uuid] = keyblocks
|
|
||||||
_(); del _
|
_(); del _
|
||||||
|
|
||||||
if settings.use_subsurf:
|
if settings.use_subsurf:
|
||||||
|
Loading…
Reference in New Issue
Block a user
Am wondering about how much speed differences we are talking about here... because this double codepath handling for a same thing has a significant impact on readability and maintainability, so would only consider it necessary if we are talking about real use-cases where the difference would be in tens of seconds on edge-cases, or at least seconds in more common cases?
Edge-cases only in the low single digit seconds slower I would say.
A 500,000 vertex .fbx with 60 shape keys that move only a single vertex each is about 1.9s slower to import without the iteration for me.
If getting rid of the iteration then I would add in a quick
dvcos.any()
check to only actually do the main work when the shape key contains at least one non-zero dvcos. I probably should have done that to begin with.Thanks, then indeed I think keeping a single codepath here is better overall.