diff --git a/scripts/addons_core/io_scene_fbx/__init__.py b/scripts/addons_core/io_scene_fbx/__init__.py index 4bfcfe4c965..4ba3166e381 100644 --- a/scripts/addons_core/io_scene_fbx/__init__.py +++ b/scripts/addons_core/io_scene_fbx/__init__.py @@ -433,6 +433,12 @@ class ExportFBX(bpy.types.Operator, ExportHelper): description="Convert all faces to triangles", default=False, ) + optimize_normals: BoolProperty( + name="Optimize Normals", + description="Use IndexToDirect mode to deduplicate normals. May not be well supported by " + "some external software.", + default=True, + ) use_custom_props: BoolProperty( name="Custom Properties", description="Export custom properties", @@ -653,6 +659,7 @@ def export_panel_geometry(layout, operator): #sub.prop(operator, "use_mesh_modifiers_render") body.prop(operator, "use_mesh_edges") body.prop(operator, "use_triangles") + body.prop(operator, "optimize_normals") sub = body.row() # ~ sub.enabled = operator.mesh_smooth_type in {'OFF'} sub.prop(operator, "use_tspace") diff --git a/scripts/addons_core/io_scene_fbx/export_fbx_bin.py b/scripts/addons_core/io_scene_fbx/export_fbx_bin.py index e1dc3c2f17d..1420c31ad23 100644 --- a/scripts/addons_core/io_scene_fbx/export_fbx_bin.py +++ b/scripts/addons_core/io_scene_fbx/export_fbx_bin.py @@ -767,7 +767,7 @@ def fbx_data_mesh_shapes_elements(root, me_obj, me, scene_data, fbx_me_tmpl, fbx channels = [] vertices = me.vertices - 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_nors, shape_verts_idx) in shapes.items(): # Use vgroups as weights, if defined. if shape.vertex_group and shape.vertex_group in me_obj.bdata.vertex_groups: shape_verts_weights = np.zeros(len(shape_verts_idx), dtype=np.float64) @@ -798,7 +798,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_float64_array(geom, b"Vertices", shape_verts_co) if write_normals: - elem_data_single_float64_array(geom, b"Normals", np.zeros(len(shape_verts_idx) * 3, dtype=np.float64)) + elem_data_single_float64_array(geom, b"Normals", shape_verts_nors) # Yiha! BindPose for shapekeys too! Dodecasigh... # XXX Not sure yet whether several bindposes on same mesh are allowed, or not... :/ @@ -1153,6 +1153,7 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes): del t_pvi_edge_indices # Loop normals. + do_optimize_normals = scene_data.settings.optimize_normals tspacenumber = 0 if write_normals: normal_bl_dtype = np.single @@ -1188,10 +1189,15 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes): # FBX SDK documentation says that normals should use IndexToDirect. 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_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_normal_idx = fast_first_axis_unique(t_normal.reshape(-1, 3), return_inverse=True) + if do_optimize_normals: + # 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_normal_idx = fast_first_axis_unique(t_normal.reshape(-1, 3), return_inverse=True) + else: + # Export normals in a linear fashion + t_normal = t_normal.reshape(-1, 3) + t_normal_idx = np.arange(len(t_normal), dtype=normal_idx_fbx_dtype) # Convert to the type for fbx t_normal_idx = astype_view_signedness(t_normal_idx, normal_idx_fbx_dtype) @@ -2246,7 +2252,7 @@ def fbx_animations_do(scene_data, ref_id, f_start, f_end, start_zero, objects=No # Ignore absolute shape keys for now! if not me.shape_keys.use_relative: continue - 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_nors, _shape_verts_idx) in shapes.items(): acnode = AnimationCurveNodeWrapper(channel_key, 'SHAPE_KEY', force_key, force_sek, (0.0,)) # Sooooo happy to have to twist again like a mad snake... Yes, we need to write those curves twice. :/ acnode.add_group(me_key, shape.name, shape.name, (shape.name,)) @@ -2732,14 +2738,22 @@ def fbx_data_from_scene(scene, depsgraph, settings): co_bl_dtype = np.single co_fbx_dtype = np.float64 idx_fbx_dtype = np.int32 + normal_bl_dtype = np.single + normal_fbx_dtype = np.float64 + geom_mat_no = Matrix(settings.global_matrix_inv_transposed) if settings.bake_space_transform else None + if geom_mat_no is not None: + # Remove translation & scaling! + geom_mat_no.translation = Vector() + geom_mat_no.normalize() 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_nor = 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 + return single_vert_co, single_vert_nor, single_vert_idx 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... @@ -2753,38 +2767,43 @@ def fbx_data_from_scene(scene, depsgraph, settings): # Get and cache only the cos that we need @cache - def sk_cos(shape_key): + def sk_cos_nors(shape_key): if shape_key == sk_base: _cos = MESH_ATTRIBUTE_POSITION.to_ndarray(me.attributes) else: _cos = np.empty(len(me.vertices) * 3, dtype=co_bl_dtype) shape_key.points.foreach_get("co", _cos) - return vcos_transformed(_cos, geom_mat_co, co_fbx_dtype) + _nors = np.array(shape_key.normals_vertex_get(), dtype=normal_bl_dtype) + return ( + vcos_transformed(_cos, geom_mat_co, co_fbx_dtype), + nors_transformed(_nors, geom_mat_no, normal_fbx_dtype) + ) for shape in me.shape_keys.key_blocks[1:]: # Only write vertices really different from base coordinates! relative_key = shape.relative_key if shape == relative_key: # Shape is its own relative key, so it does nothing - shape_verts_co, shape_verts_idx = empty_verts_fallbacks() + shape_verts_co, shape_verts_nors, shape_verts_idx = empty_verts_fallbacks() else: - sv_cos = sk_cos(shape) - ref_cos = sk_cos(shape.relative_key) + sv_cos_nors = sk_cos_nors(shape) + ref_cos_nors = sk_cos_nors(shape.relative_key) # Exclude cos similar to ref_cos and get the indices of the cos that remain - shape_verts_co, shape_verts_idx = shape_difference_exclude_similar(sv_cos, ref_cos) + shape_verts_co, shape_verts_nors, shape_verts_idx = shape_difference_exclude_similar( + sv_cos_nors, ref_cos_nors) if not shape_verts_co.size: - shape_verts_co, shape_verts_idx = empty_verts_fallbacks() + shape_verts_co, shape_verts_nors, shape_verts_idx = empty_verts_fallbacks() else: # Ensure the indices are of the correct type shape_verts_idx = astype_view_signedness(shape_verts_idx, idx_fbx_dtype) 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_nors, shape_verts_idx) data_deformers_shape.setdefault(me, (me_key, shapes_key, {}))[2][shape] = data - del sk_cos + del sk_cos_nors perfmon.step("FBX export prepare: Wrapping Armatures...") @@ -3002,7 +3021,7 @@ def fbx_data_from_scene(scene, depsgraph, settings): for me_key, shapes_key, shapes in data_deformers_shape.values(): # shape -> geometry connections.append((b"OO", get_fbx_uuid_from_key(shapes_key), get_fbx_uuid_from_key(me_key), None)) - for channel_key, geom_key, _shape_verts_co, _shape_verts_idx in shapes.values(): + for channel_key, geom_key, _shape_verts_co, _shape_verts_nors, _shape_verts_idx in shapes.values(): # shape channel -> shape connections.append((b"OO", get_fbx_uuid_from_key(channel_key), get_fbx_uuid_from_key(shapes_key), None)) # geometry (keys) -> shape channel @@ -3416,6 +3435,7 @@ def save_single(operator, scene, depsgraph, filepath="", use_mesh_edges=True, use_tspace=True, use_triangles=False, + optimize_normals=False, embed_textures=False, use_custom_props=False, bake_space_transform=False, @@ -3484,7 +3504,7 @@ def save_single(operator, scene, depsgraph, filepath="", bake_space_transform, global_matrix_inv, global_matrix_inv_transposed, context_objects, object_types, use_mesh_modifiers, use_mesh_modifiers_render, mesh_smooth_type, use_subsurf, use_mesh_edges, use_tspace, use_triangles, - armature_nodetype, use_armature_deform_only, + optimize_normals, armature_nodetype, use_armature_deform_only, add_leaf_bones, bone_correction_matrix, bone_correction_matrix_inv, bake_anim, bake_anim_use_all_bones, bake_anim_use_nla_strips, bake_anim_use_all_actions, bake_anim_step, bake_anim_simplify_factor, bake_anim_force_startend_keying, @@ -3563,6 +3583,7 @@ def defaults_unity3d(): "use_subsurf": False, "use_tspace": False, # XXX Why? Unity is expected to support tspace import... "use_triangles": False, + "optimize_normals": False, # Appears to cause problems when using imported blend shape normals "use_armature_deform_only": True, diff --git a/scripts/addons_core/io_scene_fbx/fbx_utils.py b/scripts/addons_core/io_scene_fbx/fbx_utils.py index 6296f9513a0..46d06a26584 100644 --- a/scripts/addons_core/io_scene_fbx/fbx_utils.py +++ b/scripts/addons_core/io_scene_fbx/fbx_utils.py @@ -284,22 +284,28 @@ def similar_values_iter(v1, v2, e=1e-6): return True -def shape_difference_exclude_similar(sv_cos, ref_cos, e=1e-6): +def shape_difference_exclude_similar(sv_cos_nors, ref_cos_nors, 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, + the corresponding vertex normal differences, and the indices of the vertices that are not nearly the same""" - assert(sv_cos.size == ref_cos.size) + sv_cos, sv_nors = sv_cos_nors + ref_cos, ref_nors = ref_cos_nors + assert(sv_cos.size == ref_cos.size == sv_nors.size == ref_nors.size) # Create views of 1 co per row of the arrays, only making copies if needed. sv_cos = sv_cos.reshape(-1, 3) + sv_nors = sv_nors.reshape(-1, 3) ref_cos = ref_cos.reshape(-1, 3) + ref_nors = ref_nors.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_nors = np.empty((0, 3), dtype=sv_nors.dtype) empty_indices = np.empty(0, dtype=np.int32) - return empty_cos, empty_indices + return empty_cos, empty_nors, 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. @@ -315,7 +321,8 @@ def shape_difference_exclude_similar(sv_cos, ref_cos, e=1e-6): # 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 + difference_nors = (sv_nors - ref_nors)[not_similar_verts_idx] + return difference_cos, difference_nors, not_similar_verts_idx def _mat4_vec3_array_multiply(mat4, vec3_array, dtype=None, return_4d=False): @@ -1894,7 +1901,7 @@ FBXExportSettings = namedtuple("FBXExportSettings", ( "bake_space_transform", "global_matrix_inv", "global_matrix_inv_transposed", "context_objects", "object_types", "use_mesh_modifiers", "use_mesh_modifiers_render", "mesh_smooth_type", "use_subsurf", "use_mesh_edges", "use_tspace", "use_triangles", - "armature_nodetype", "use_armature_deform_only", "add_leaf_bones", + "optimize_normals", "armature_nodetype", "use_armature_deform_only", "add_leaf_bones", "bone_correction_matrix", "bone_correction_matrix_inv", "bake_anim", "bake_anim_use_all_bones", "bake_anim_use_nla_strips", "bake_anim_use_all_actions", "bake_anim_step", "bake_anim_simplify_factor", "bake_anim_force_startend_keying",