From b2cfaa146d25fccdcc5a89534b28f5c33536e15a Mon Sep 17 00:00:00 2001 From: Thomas Barlow Date: Tue, 10 Oct 2023 00:51:46 +0100 Subject: [PATCH 1/3] Fix duplicate shape key import when FBX connections are duplicated Added some comments explaining the different FBX types and their relationships. Removed an unnecessary re-get, of the Shape being iterated, from fbx_table_nodes. --- io_scene_fbx/import_fbx.py | 42 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/io_scene_fbx/import_fbx.py b/io_scene_fbx/import_fbx.py index b84d3d005..0190faa74 100644 --- a/io_scene_fbx/import_fbx.py +++ b/io_scene_fbx/import_fbx.py @@ -3470,12 +3470,36 @@ def load(operator, context, filepath="", def _(): fbx_tmpl = fbx_template_get((b'Geometry', b'KFbxShape')) + # - FBX | - Blender equivalent + # Mesh | `Mesh` + # BlendShape | `Key` + # BlendShapeChannel | `ShapeKey`, but without its `.data`. + # Shape | `ShapeKey.data`, but also includes normals and the values are relative to the base Mesh + # | instead of being absolute. The data is sparse, so each Shape has an "Indexes" array too. + # | FBX 2020 introduced 'Modern Style' Shapes that also support tangents, binormals, vertex + # | colors and UVs, and can be absolute values instead of relative, but 'Modern Style' Shapes + # | are not currently supported. + # + # The FBX connections between Shapes and Meshes form multiple many-many relationships: + # Mesh >-< BlendShape >-< BlendShapeChannel >-< Shape + # In practice, the relationships are almost never many-many and are more typically 1-many or 1-1: + # Mesh --- BlendShape: + # usually 1-1 and the FBX SDK might enforce that each BlendShape is connected to at most one Mesh. + # BlendShape --< BlendShapeChannel: + # usually 1-many. + # BlendShapeChannel --- or uncommonly --< Shape: + # usually 1-1, but 1-many is a documented feature. mesh_to_shapes = {} - for s_uuid, s_item in fbx_table_nodes.items(): - fbx_sdata, bl_sdata = s_item = fbx_table_nodes.get(s_uuid, (None, None)) + for s_uuid, (fbx_sdata, _bl_sdata) in fbx_table_nodes.items(): if fbx_sdata is None or fbx_sdata.id != b'Geometry' or fbx_sdata.props[2] != b'Shape': continue + # Rarely, an imported FBX file will have duplicate connections. For Shape Key related connections, FBX + # appears to ignore the duplicates, or overwrite the existing duplicates such that the end result is the + # same as ignoring them, so we keep sets of the seen connections for each FBX type and ignore any duplicates + # we find. + seen_s2bc_connections = set() + # shape -> blendshapechannel -> blendshape -> mesh. for bc_uuid, bc_ctype in fbx_connection_map.get(s_uuid, ()): if bc_ctype.props[0] != b'OO': @@ -3483,18 +3507,32 @@ def load(operator, context, filepath="", 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': continue + connection_key = (s_uuid, bc_uuid) + if connection_key in seen_s2bc_connections: + continue + seen_s2bc_connections.add(connection_key) + seen_bc2bs_connections = set() for bs_uuid, bs_ctype in fbx_connection_map.get(bc_uuid, ()): if bs_ctype.props[0] != b'OO': continue fbx_bsdata, _bl_bsdata = fbx_table_nodes.get(bs_uuid, (None, None)) if fbx_bsdata is None or fbx_bsdata.id != b'Deformer' or fbx_bsdata.props[2] != b'BlendShape': continue + connection_key = (bc_uuid, bs_uuid) + if connection_key in seen_bc2bs_connections: + continue + seen_bc2bs_connections.add(connection_key) + seen_bs2m_connections = set() for m_uuid, m_ctype in fbx_connection_map.get(bs_uuid, ()): if m_ctype.props[0] != b'OO': continue fbx_mdata, bl_mdata = fbx_table_nodes.get(m_uuid, (None, None)) if fbx_mdata is None or fbx_mdata.id != b'Geometry' or fbx_mdata.props[2] != b'Mesh': continue + connection_key = (bs_uuid, m_uuid) + if connection_key in seen_bs2m_connections: + continue + seen_bs2m_connections.add(connection_key) # Blenmeshes are assumed already created at that time! 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 -- 2.30.2 From 31157d409d2cd4e0c35c0d843ab6bd1a660e8ea5 Mon Sep 17 00:00:00 2001 From: Thomas Barlow Date: Sat, 14 Oct 2023 12:09:29 +0100 Subject: [PATCH 2/3] Deduplicate shape key connection iteration code The main loop was getting rather long so its inner loops have been replaced with a helper generator function. --- io_scene_fbx/import_fbx.py | 63 +++++++++++++++----------------------- 1 file changed, 25 insertions(+), 38 deletions(-) diff --git a/io_scene_fbx/import_fbx.py b/io_scene_fbx/import_fbx.py index 0190faa74..524713955 100644 --- a/io_scene_fbx/import_fbx.py +++ b/io_scene_fbx/import_fbx.py @@ -3489,50 +3489,37 @@ def load(operator, context, filepath="", # usually 1-many. # BlendShapeChannel --- or uncommonly --< Shape: # usually 1-1, but 1-many is a documented feature. + + def connections_gen(c_src_uuid, fbx_id, fbx_type): + """Helper to reduce duplicate code""" + # Rarely, an imported FBX file will have duplicate connections. For Shape Key related connections, FBX + # appears to ignore the duplicates, or overwrite the existing duplicates such that the end result is the + # same as ignoring them, so keep a set of the seen connections and ignore any duplicates. + seen_connections = set() + for c_dst_uuid, ctype in fbx_connection_map.get(c_src_uuid, ()): + if ctype.props[0] != b'OO': + # 'Object-Object' connections only. + continue + fbx_data, bl_data = fbx_table_nodes.get(c_dst_uuid, (None, None)) + if fbx_data is None or fbx_data.id != fbx_id or fbx_data.props[2] != fbx_type: + # Either `c_dst_uuid` doesn't exist, or it has a different id or type. + continue + connection_key = (c_src_uuid, c_dst_uuid) + if connection_key in seen_connections: + # The connection is a duplicate, skip it. + continue + seen_connections.add(connection_key) + yield c_dst_uuid, fbx_data, bl_data + mesh_to_shapes = {} for s_uuid, (fbx_sdata, _bl_sdata) in fbx_table_nodes.items(): if fbx_sdata is None or fbx_sdata.id != b'Geometry' or fbx_sdata.props[2] != b'Shape': continue - # Rarely, an imported FBX file will have duplicate connections. For Shape Key related connections, FBX - # appears to ignore the duplicates, or overwrite the existing duplicates such that the end result is the - # same as ignoring them, so we keep sets of the seen connections for each FBX type and ignore any duplicates - # we find. - seen_s2bc_connections = set() - # shape -> blendshapechannel -> blendshape -> mesh. - for bc_uuid, bc_ctype in fbx_connection_map.get(s_uuid, ()): - if bc_ctype.props[0] != b'OO': - continue - 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': - continue - connection_key = (s_uuid, bc_uuid) - if connection_key in seen_s2bc_connections: - continue - seen_s2bc_connections.add(connection_key) - seen_bc2bs_connections = set() - for bs_uuid, bs_ctype in fbx_connection_map.get(bc_uuid, ()): - if bs_ctype.props[0] != b'OO': - continue - fbx_bsdata, _bl_bsdata = fbx_table_nodes.get(bs_uuid, (None, None)) - if fbx_bsdata is None or fbx_bsdata.id != b'Deformer' or fbx_bsdata.props[2] != b'BlendShape': - continue - connection_key = (bc_uuid, bs_uuid) - if connection_key in seen_bc2bs_connections: - continue - seen_bc2bs_connections.add(connection_key) - seen_bs2m_connections = set() - for m_uuid, m_ctype in fbx_connection_map.get(bs_uuid, ()): - if m_ctype.props[0] != b'OO': - continue - fbx_mdata, bl_mdata = fbx_table_nodes.get(m_uuid, (None, None)) - if fbx_mdata is None or fbx_mdata.id != b'Geometry' or fbx_mdata.props[2] != b'Mesh': - continue - connection_key = (bs_uuid, m_uuid) - if connection_key in seen_bs2m_connections: - continue - seen_bs2m_connections.add(connection_key) + for bc_uuid, fbx_bcdata, _bl_bcdata in connections_gen(s_uuid, b'Deformer', b'BlendShapeChannel'): + for bs_uuid, _fbx_bsdata, _bl_bsdata in connections_gen(bc_uuid, b'Deformer', b'BlendShape'): + for m_uuid, _fbx_mdata, bl_mdata in connections_gen(bs_uuid, b'Geometry', b'Mesh'): # Blenmeshes are assumed already created at that time! 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 -- 2.30.2 From 54ba8103bd3771746c34973fecfd4148c674a2d1 Mon Sep 17 00:00:00 2001 From: Thomas Barlow Date: Wed, 18 Oct 2023 16:14:44 +0100 Subject: [PATCH 3/3] Increase FBX version --- io_scene_fbx/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/io_scene_fbx/__init__.py b/io_scene_fbx/__init__.py index 72ae5c995..2d5e1f221 100644 --- a/io_scene_fbx/__init__.py +++ b/io_scene_fbx/__init__.py @@ -5,7 +5,7 @@ bl_info = { "name": "FBX format", "author": "Campbell Barton, Bastien Montagne, Jens Restemeier, @Mysteryem", - "version": (5, 8, 7), + "version": (5, 8, 8), "blender": (3, 6, 0), "location": "File > Import-Export", "description": "FBX IO meshes, UVs, vertex colors, materials, textures, cameras, lamps and actions", -- 2.30.2