Speed up FBX export of shape keys with numpy #104452
@ -11,6 +11,7 @@ import os
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
from itertools import zip_longest, chain
|
from itertools import zip_longest, chain
|
||||||
|
from functools import cache
|
||||||
|
|
||||||
if "bpy" in locals():
|
if "bpy" in locals():
|
||||||
import importlib
|
import importlib
|
||||||
@ -47,7 +48,7 @@ from .fbx_utils import (
|
|||||||
# Miscellaneous utils.
|
# Miscellaneous utils.
|
||||||
PerfMon,
|
PerfMon,
|
||||||
units_blender_to_fbx_factor, units_convertor, units_convertor_iter,
|
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,
|
matrix4_to_array, similar_values, shape_difference_exclude_similar, astype_view_signedness, fast_first_axis_unique,
|
||||||
# Mesh transform helpers.
|
# Mesh transform helpers.
|
||||||
vcos_transformed_gen, vcos_transformed, nors_transformed,
|
vcos_transformed_gen, vcos_transformed, nors_transformed,
|
||||||
# UUID from key.
|
# UUID from key.
|
||||||
@ -760,15 +761,19 @@ def fbx_data_mesh_shapes_elements(root, me_obj, me, scene_data, fbx_me_tmpl, fbx
|
|||||||
for shape, (channel_key, geom_key, shape_verts_co, shape_verts_idx) in shapes.items():
|
for shape, (channel_key, geom_key, shape_verts_co, shape_verts_idx) in shapes.items():
|
||||||
# Use vgroups as weights, if defined.
|
# Use vgroups as weights, if defined.
|
||||||
if shape.vertex_group and shape.vertex_group in me_obj.bdata.vertex_groups:
|
if shape.vertex_group and shape.vertex_group in me_obj.bdata.vertex_groups:
|
||||||
shape_verts_weights = array.array(data_types.ARRAY_FLOAT64, [0.0]) * (len(shape_verts_co) // 3)
|
shape_verts_weights = np.zeros(len(shape_verts_idx), dtype=np.float64)
|
||||||
|
# It's slightly faster to iterate and index the underlying memoryview objects
|
||||||
|
mv_shape_verts_weights = shape_verts_weights.data
|
||||||
|
mv_shape_verts_idx = shape_verts_idx.data
|
||||||
vg_idx = me_obj.bdata.vertex_groups[shape.vertex_group].index
|
vg_idx = me_obj.bdata.vertex_groups[shape.vertex_group].index
|
||||||
for sk_idx, v_idx in enumerate(shape_verts_idx):
|
for sk_idx, v_idx in enumerate(mv_shape_verts_idx):
|
||||||
for vg in vertices[v_idx].groups:
|
for vg in vertices[v_idx].groups:
|
||||||
if vg.group == vg_idx:
|
if vg.group == vg_idx:
|
||||||
shape_verts_weights[sk_idx] = vg.weight * 100.0
|
mv_shape_verts_weights[sk_idx] = vg.weight
|
||||||
break
|
break
|
||||||
|
shape_verts_weights *= 100.0
|
||||||
else:
|
else:
|
||||||
shape_verts_weights = array.array(data_types.ARRAY_FLOAT64, [100.0]) * (len(shape_verts_co) // 3)
|
shape_verts_weights = np.full(len(shape_verts_idx), 100.0, dtype=np.float64)
|
||||||
channels.append((channel_key, shape, shape_verts_weights))
|
channels.append((channel_key, shape, shape_verts_weights))
|
||||||
|
|
||||||
geom = elem_data_single_int64(root, b"Geometry", get_fbx_uuid_from_key(geom_key))
|
geom = elem_data_single_int64(root, b"Geometry", get_fbx_uuid_from_key(geom_key))
|
||||||
@ -784,8 +789,7 @@ def fbx_data_mesh_shapes_elements(root, me_obj, me, scene_data, fbx_me_tmpl, fbx
|
|||||||
elem_data_single_int32_array(geom, b"Indexes", shape_verts_idx)
|
elem_data_single_int32_array(geom, b"Indexes", shape_verts_idx)
|
||||||
elem_data_single_float64_array(geom, b"Vertices", shape_verts_co)
|
elem_data_single_float64_array(geom, b"Vertices", shape_verts_co)
|
||||||
if write_normals:
|
if write_normals:
|
||||||
elem_data_single_float64_array(geom, b"Normals",
|
elem_data_single_float64_array(geom, b"Normals", np.zeros(len(shape_verts_idx) * 3, dtype=np.float64))
|
||||||
array.array(data_types.ARRAY_FLOAT64, [0.0]) * len(shape_verts_co))
|
|
||||||
|
|
||||||
# Yiha! BindPose for shapekeys too! Dodecasigh...
|
# Yiha! BindPose for shapekeys too! Dodecasigh...
|
||||||
# XXX Not sure yet whether several bindposes on same mesh are allowed, or not... :/
|
# XXX Not sure yet whether several bindposes on same mesh are allowed, or not... :/
|
||||||
@ -2501,6 +2505,18 @@ def fbx_data_from_scene(scene, depsgraph, settings):
|
|||||||
# ShapeKeys.
|
# ShapeKeys.
|
||||||
data_deformers_shape = {}
|
data_deformers_shape = {}
|
||||||
geom_mat_co = settings.global_matrix if settings.bake_space_transform else None
|
geom_mat_co = settings.global_matrix if settings.bake_space_transform else None
|
||||||
|
co_bl_dtype = np.single
|
||||||
|
co_fbx_dtype = np.float64
|
||||||
|
idx_fbx_dtype = np.int32
|
||||||
|
|
||||||
|
def empty_verts_fallbacks():
|
||||||
|
"""Create fallback arrays for when there are no verts"""
|
||||||
|
# FBX does not like empty shapes (makes Unity crash e.g.).
|
||||||
|
# To prevent this, we add a vertex that does nothing, but it keeps the shape key intact
|
||||||
|
single_vert_co = np.zeros((1, 3), dtype=co_fbx_dtype)
|
||||||
|
single_vert_idx = np.zeros(1, dtype=idx_fbx_dtype)
|
||||||
|
return single_vert_co, single_vert_idx
|
||||||
|
|
||||||
for me_key, me, _free in data_meshes.values():
|
for me_key, me, _free in data_meshes.values():
|
||||||
if not (me.shape_keys and len(me.shape_keys.key_blocks) > 1): # We do not want basis-only relative skeys...
|
if not (me.shape_keys and len(me.shape_keys.key_blocks) > 1): # We do not want basis-only relative skeys...
|
||||||
continue
|
continue
|
||||||
@ -2508,41 +2524,44 @@ def fbx_data_from_scene(scene, depsgraph, settings):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
shapes_key = get_blender_mesh_shape_key(me)
|
shapes_key = get_blender_mesh_shape_key(me)
|
||||||
# We gather all vcos first, since some skeys may be based on others...
|
|
||||||
_cos = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(me.vertices) * 3
|
|
||||||
me.vertices.foreach_get("co", _cos)
|
|
||||||
v_cos = tuple(vcos_transformed_gen(_cos, geom_mat_co))
|
|
||||||
sk_cos = {}
|
|
||||||
for shape in me.shape_keys.key_blocks[1:]:
|
|
||||||
shape.data.foreach_get("co", _cos)
|
|
||||||
sk_cos[shape] = tuple(vcos_transformed_gen(_cos, geom_mat_co))
|
|
||||||
sk_base = me.shape_keys.key_blocks[0]
|
sk_base = me.shape_keys.key_blocks[0]
|
||||||
|
|
||||||
|
# Get and cache only the cos that we need
|
||||||
|
@cache
|
||||||
|
def sk_cos(shape_key):
|
||||||
|
_cos = np.empty(len(me.vertices) * 3, dtype=co_bl_dtype)
|
||||||
|
if shape_key == sk_base:
|
||||||
|
me.vertices.foreach_get("co", _cos)
|
||||||
|
else:
|
||||||
|
shape_key.data.foreach_get("co", _cos)
|
||||||
|
return vcos_transformed(_cos, geom_mat_co, co_fbx_dtype)
|
||||||
|
|
||||||
for shape in me.shape_keys.key_blocks[1:]:
|
for shape in me.shape_keys.key_blocks[1:]:
|
||||||
# Only write vertices really different from org coordinates!
|
# Only write vertices really different from base coordinates!
|
||||||
shape_verts_co = []
|
relative_key = shape.relative_key
|
||||||
shape_verts_idx = []
|
if shape == relative_key:
|
||||||
|
# Shape is its own relative key, so it does nothing
|
||||||
|
shape_verts_co, shape_verts_idx = empty_verts_fallbacks()
|
||||||
|
else:
|
||||||
|
sv_cos = sk_cos(shape)
|
||||||
|
ref_cos = sk_cos(shape.relative_key)
|
||||||
|
|
||||||
sv_cos = sk_cos[shape]
|
# Exclude cos similar to ref_cos and get the indices of the cos that remain
|
||||||
ref_cos = v_cos if shape.relative_key == sk_base else sk_cos[shape.relative_key]
|
shape_verts_co, shape_verts_idx = shape_difference_exclude_similar(sv_cos, ref_cos)
|
||||||
for idx, (sv_co, ref_co) in enumerate(zip(sv_cos, ref_cos)):
|
|
||||||
if similar_values_iter(sv_co, ref_co):
|
|
||||||
# Note: Maybe this is a bit too simplistic, should we use real shape base here? Though FBX does not
|
|
||||||
# have this at all... Anyway, this should cover most common cases imho.
|
|
||||||
continue
|
|
||||||
shape_verts_co.extend(Vector(sv_co) - Vector(ref_co))
|
|
||||||
shape_verts_idx.append(idx)
|
|
||||||
|
|
||||||
# FBX does not like empty shapes (makes Unity crash e.g.).
|
if not shape_verts_co.size:
|
||||||
# To prevent this, we add a vertex that does nothing, but it keeps the shape key intact
|
shape_verts_co, shape_verts_idx = empty_verts_fallbacks()
|
||||||
if not shape_verts_co:
|
else:
|
||||||
shape_verts_co.extend((0, 0, 0))
|
# Ensure the indices are of the correct type
|
||||||
shape_verts_idx.append(0)
|
shape_verts_idx = astype_view_signedness(shape_verts_idx, idx_fbx_dtype)
|
||||||
|
|
||||||
channel_key, geom_key = get_blender_mesh_shape_channel_key(me, shape)
|
channel_key, geom_key = get_blender_mesh_shape_channel_key(me, shape)
|
||||||
data = (channel_key, geom_key, shape_verts_co, shape_verts_idx)
|
data = (channel_key, geom_key, shape_verts_co, shape_verts_idx)
|
||||||
data_deformers_shape.setdefault(me, (me_key, shapes_key, {}))[2][shape] = data
|
data_deformers_shape.setdefault(me, (me_key, shapes_key, {}))[2][shape] = data
|
||||||
|
|
||||||
|
del sk_cos
|
||||||
|
|
||||||
perfmon.step("FBX export prepare: Wrapping Armatures...")
|
perfmon.step("FBX export prepare: Wrapping Armatures...")
|
||||||
|
|
||||||
# Armatures!
|
# Armatures!
|
||||||
|
@ -260,6 +260,41 @@ def similar_values_iter(v1, v2, e=1e-6):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def shape_difference_exclude_similar(sv_cos, ref_cos, e=1e-6):
|
||||||
|
"""Return a tuple of:
|
||||||
|
the difference between the vertex cos in sv_cos and ref_cos, excluding any that are nearly the same,
|
||||||
|
and the indices of the vertices that are not nearly the same"""
|
||||||
|
assert(sv_cos.size == ref_cos.size)
|
||||||
|
|
||||||
|
# Create views of 1 co per row of the arrays, only making copies if needed.
|
||||||
|
sv_cos = sv_cos.reshape(-1, 3)
|
||||||
|
ref_cos = ref_cos.reshape(-1, 3)
|
||||||
|
|
||||||
|
# Quick check for equality
|
||||||
|
if np.array_equal(sv_cos, ref_cos):
|
||||||
|
# There's no difference between the two arrays.
|
||||||
|
empty_cos = np.empty((0, 3), dtype=sv_cos.dtype)
|
||||||
|
empty_indices = np.empty(0, dtype=np.int32)
|
||||||
|
return empty_cos, empty_indices
|
||||||
|
|
||||||
|
# Note that unlike math.isclose(a,b), np.isclose(a,b) is not symmetrical and the second argument 'b', is
|
||||||
|
# considered to be the reference value.
|
||||||
|
# Note that atol=0 will mean that if only one co component being compared is zero, they won't be considered close.
|
||||||
|
similar_mask = np.isclose(sv_cos, ref_cos, atol=0, rtol=e)
|
||||||
|
|
||||||
|
# A co is only similar if every component in it is similar.
|
||||||
|
co_similar_mask = np.all(similar_mask, axis=1)
|
||||||
|
|
||||||
|
# Get the indices of cos that are not similar.
|
||||||
|
not_similar_verts_idx = np.flatnonzero(~co_similar_mask)
|
||||||
|
|
||||||
|
# Subtracting first over the entire arrays and then indexing seems faster than indexing both arrays first and then
|
||||||
|
# subtracting, until less than about 3% of the cos are being indexed.
|
||||||
|
difference_cos = (sv_cos - ref_cos)[not_similar_verts_idx]
|
||||||
|
return difference_cos, not_similar_verts_idx
|
||||||
|
|
||||||
|
|
||||||
def vcos_transformed_gen(raw_cos, m=None):
|
def vcos_transformed_gen(raw_cos, m=None):
|
||||||
# Note: we could most likely get much better performances with numpy, but will leave this as TODO for now.
|
# Note: we could most likely get much better performances with numpy, but will leave this as TODO for now.
|
||||||
gen = zip(*(iter(raw_cos),) * 3)
|
gen = zip(*(iter(raw_cos),) * 3)
|
||||||
|
Loading…
Reference in New Issue
Block a user