From f70bb98a617b736e5e00326791a95760abdcce6f Mon Sep 17 00:00:00 2001 From: Thomas Barlow Date: Sun, 25 Jun 2023 00:43:19 +0100 Subject: [PATCH 1/6] Fix triangulation removing shape keys --- io_scene_fbx/export_fbx_bin.py | 46 ++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/io_scene_fbx/export_fbx_bin.py b/io_scene_fbx/export_fbx_bin.py index 2b2a393f5..07ba2a6aa 100644 --- a/io_scene_fbx/export_fbx_bin.py +++ b/io_scene_fbx/export_fbx_bin.py @@ -2594,12 +2594,30 @@ def fbx_data_from_scene(scene, depsgraph, settings): # If modifiers has been altered need to update dependency graph. if backup_pose_positions or tmp_mods: depsgraph.update() - ob_to_convert = ob.evaluated_get(depsgraph) if settings.use_mesh_modifiers else ob - # NOTE: The dependency graph might be re-evaluating multiple times, which could - # potentially free the mesh created early on. So we put those meshes to bmain and - # free them afterwards. Not ideal but ensures correct ownerwhip. - tmp_me = bpy.data.meshes.new_from_object( - ob_to_convert, preserve_all_data_layers=True, depsgraph=depsgraph) + if settings.use_mesh_modifiers: + ob_to_convert = ob.evaluated_get(depsgraph) + # NOTE: The dependency graph might be re-evaluating multiple times, which could + # potentially free the mesh created early on. So we put those meshes to bmain and + # free them afterwards. Not ideal but ensures correct ownerwhip. + tmp_me = bpy.data.meshes.new_from_object( + ob_to_convert, preserve_all_data_layers=True, depsgraph=depsgraph) + free = True + + # Usually the materials of the evaluated object will be the same, but modifiers, such as Geometry + # Nodes, can change the materials. + orig_mats = tuple(slot.material for slot in ob.material_slots) + eval_mats = tuple(slot.material.original if slot.material else None + for slot in ob_to_convert.material_slots) + if orig_mats != eval_mats: + # Override the default behaviour of getting materials from ob_obj.bdata.material_slots. + ob_obj.override_materials = eval_mats + else: + # Creates a temporary mesh owned by the Object without adding it to bpy.data.meshes. Most + # importantly, compared to bpy.data.meshes.new_from_object, it does not remove shape keys from the + # new mesh. We'll forcefully free these temporary meshes at the end of the export. + tmp_me = ob.to_mesh(preserve_all_data_layers=True, depsgraph=depsgraph) + free = False + # Triangulate the mesh if requested if settings.use_triangles: import bmesh @@ -2608,15 +2626,8 @@ def fbx_data_from_scene(scene, depsgraph, settings): bmesh.ops.triangulate(bm, faces=bm.faces) bm.to_mesh(tmp_me) bm.free() - # Usually the materials of the evaluated object will be the same, but modifiers, such as Geometry Nodes, - # can change the materials. - orig_mats = tuple(slot.material for slot in ob.material_slots) - eval_mats = tuple(slot.material.original if slot.material else None - for slot in ob_to_convert.material_slots) - if orig_mats != eval_mats: - # Override the default behaviour of getting materials from ob_obj.bdata.material_slots. - ob_obj.override_materials = eval_mats - data_meshes[ob_obj] = (get_blenderID_key(tmp_me), tmp_me, True) + + data_meshes[ob_obj] = (get_blenderID_key(tmp_me), tmp_me, free) # Change armatures back. for armature, pose_position in backup_pose_positions: print((armature, pose_position)) @@ -3007,7 +3018,10 @@ def fbx_scene_data_cleanup(scene_data): """ # Delete temp meshes. done_meshes = set() - for me_key, me, free in scene_data.data_meshes.values(): + for ob_obj, (me_key, me, free) in scene_data.data_meshes.items(): + # Clear temporary meshes created by Object.to_mesh(). + ob_obj.bdata.to_mesh_clear() + # Clear temporary meshes created when applying modifiers. if free and me_key not in done_meshes: bpy.data.meshes.remove(me) done_meshes.add(me_key) -- 2.30.2 From 733598d5c1b81a44a8852259fe28c2513cacd59c Mon Sep 17 00:00:00 2001 From: Thomas Barlow Date: Fri, 15 Sep 2023 01:07:34 +0100 Subject: [PATCH 2/6] Use Mesh.copy() instead and fix export of non-mesh Objects that are converted to meshes on export --- io_scene_fbx/export_fbx_bin.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/io_scene_fbx/export_fbx_bin.py b/io_scene_fbx/export_fbx_bin.py index 07ba2a6aa..b7b7a2e68 100644 --- a/io_scene_fbx/export_fbx_bin.py +++ b/io_scene_fbx/export_fbx_bin.py @@ -2594,14 +2594,13 @@ def fbx_data_from_scene(scene, depsgraph, settings): # If modifiers has been altered need to update dependency graph. if backup_pose_positions or tmp_mods: depsgraph.update() - if settings.use_mesh_modifiers: + if settings.use_mesh_modifiers or ob.type in BLENDER_OTHER_OBJECT_TYPES: ob_to_convert = ob.evaluated_get(depsgraph) # NOTE: The dependency graph might be re-evaluating multiple times, which could # potentially free the mesh created early on. So we put those meshes to bmain and # free them afterwards. Not ideal but ensures correct ownerwhip. tmp_me = bpy.data.meshes.new_from_object( ob_to_convert, preserve_all_data_layers=True, depsgraph=depsgraph) - free = True # Usually the materials of the evaluated object will be the same, but modifiers, such as Geometry # Nodes, can change the materials. @@ -2612,11 +2611,9 @@ def fbx_data_from_scene(scene, depsgraph, settings): # Override the default behaviour of getting materials from ob_obj.bdata.material_slots. ob_obj.override_materials = eval_mats else: - # Creates a temporary mesh owned by the Object without adding it to bpy.data.meshes. Most - # importantly, compared to bpy.data.meshes.new_from_object, it does not remove shape keys from the - # new mesh. We'll forcefully free these temporary meshes at the end of the export. - tmp_me = ob.to_mesh(preserve_all_data_layers=True, depsgraph=depsgraph) - free = False + # bpy.data.meshes.new_from_object always removes shape keys (see #104714), so create a copy of the + # mesh instead. + tmp_me = ob.data.copy() # Triangulate the mesh if requested if settings.use_triangles: @@ -2627,7 +2624,7 @@ def fbx_data_from_scene(scene, depsgraph, settings): bm.to_mesh(tmp_me) bm.free() - data_meshes[ob_obj] = (get_blenderID_key(tmp_me), tmp_me, free) + data_meshes[ob_obj] = (get_blenderID_key(tmp_me), tmp_me, True) # Change armatures back. for armature, pose_position in backup_pose_positions: print((armature, pose_position)) @@ -3018,10 +3015,7 @@ def fbx_scene_data_cleanup(scene_data): """ # Delete temp meshes. done_meshes = set() - for ob_obj, (me_key, me, free) in scene_data.data_meshes.items(): - # Clear temporary meshes created by Object.to_mesh(). - ob_obj.bdata.to_mesh_clear() - # Clear temporary meshes created when applying modifiers. + for me_key, me, free in scene_data.data_meshes.values(): if free and me_key not in done_meshes: bpy.data.meshes.remove(me) done_meshes.add(me_key) -- 2.30.2 From 531e7be110cbd92ea90fe372fc2dccafb11fb13a Mon Sep 17 00:00:00 2001 From: Thomas Barlow Date: Fri, 15 Sep 2023 01:34:20 +0100 Subject: [PATCH 3/6] Fix "Apply Modifiers" removing shape keys when original data cannot be used for other reasons Having "Triangulate Faces" enabled or having meshes with Object-linked materials would still not export shape keys if "Apply Modifiers" was enabled and the mesh did not have any modifiers to apply. --- io_scene_fbx/export_fbx_bin.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/io_scene_fbx/export_fbx_bin.py b/io_scene_fbx/export_fbx_bin.py index b7b7a2e68..d03d4b449 100644 --- a/io_scene_fbx/export_fbx_bin.py +++ b/io_scene_fbx/export_fbx_bin.py @@ -2551,6 +2551,7 @@ def fbx_data_from_scene(scene, depsgraph, settings): if settings.use_mesh_modifiers or settings.use_triangles or ob.type in BLENDER_OTHER_OBJECT_TYPES or is_ob_material: # We cannot use default mesh in that case, or material would not be the right ones... use_org_data = not (is_ob_material or ob.type in BLENDER_OTHER_OBJECT_TYPES) + apply_modifiers = False backup_pose_positions = [] tmp_mods = [] if use_org_data and ob.type == 'MESH': @@ -2581,9 +2582,11 @@ def fbx_data_from_scene(scene, depsgraph, settings): if settings.use_subsurf and mod.type == 'SUBSURF' and mod.subdivision_type == 'CATMULL_CLARK': if last_subsurf: use_org_data = False + apply_modifiers = True last_subsurf = mod else: use_org_data = False + apply_modifiers = True if settings.use_subsurf and last_subsurf: # XXX: When exporting with subsurf information temporarily disable # the last subsurf modifier. @@ -2594,7 +2597,7 @@ def fbx_data_from_scene(scene, depsgraph, settings): # If modifiers has been altered need to update dependency graph. if backup_pose_positions or tmp_mods: depsgraph.update() - if settings.use_mesh_modifiers or ob.type in BLENDER_OTHER_OBJECT_TYPES: + if apply_modifiers or ob.type in BLENDER_OTHER_OBJECT_TYPES: ob_to_convert = ob.evaluated_get(depsgraph) # NOTE: The dependency graph might be re-evaluating multiple times, which could # potentially free the mesh created early on. So we put those meshes to bmain and -- 2.30.2 From 8b02121ae9f80741d3cb7d80e16024837639f533 Mon Sep 17 00:00:00 2001 From: Thomas Barlow Date: Fri, 15 Sep 2023 22:06:21 +0100 Subject: [PATCH 4/6] Restructure code to make it clearer what needs to be done to an Object's data and in what cases --- io_scene_fbx/export_fbx_bin.py | 192 +++++++++++++++++---------------- 1 file changed, 101 insertions(+), 91 deletions(-) diff --git a/io_scene_fbx/export_fbx_bin.py b/io_scene_fbx/export_fbx_bin.py index d03d4b449..e46ccd366 100644 --- a/io_scene_fbx/export_fbx_bin.py +++ b/io_scene_fbx/export_fbx_bin.py @@ -2536,7 +2536,6 @@ def fbx_data_from_scene(scene, depsgraph, settings): if ob_obj.type not in BLENDER_OBJECT_TYPES_MESHLIKE: continue ob = ob_obj.bdata - use_org_data = True org_ob_obj = None # Do not want to systematically recreate a new mesh for dupliobject instances, kind of break purpose of those. @@ -2546,101 +2545,112 @@ def fbx_data_from_scene(scene, depsgraph, settings): data_meshes[ob_obj] = data_meshes[org_ob_obj] continue - is_ob_material = any(ms.link == 'OBJECT' for ms in ob.material_slots) + # There are 4 different cases for what we need to do with the original data of each Object: + # 1) The original data can be used without changes. + # 2) A copy of the original data needs to be made. + # - If a mesh needs to be modified upon export, e.g. it needs triangulating. + # - If a mesh has Object-linked materials. This is to do with how materials are currently mapped to FBX. + # 3) A non-mesh needs to be converted to a mesh. + # 4) The Object needs to be evaluated and then converted to a mesh. + # - Whenever use_mesh_modifiers is enabled and either there are modifiers to apply or the Object is not a mesh. + # If multiple cases apply to an Object, then only the last applicable case is relevant. + do_copy = any(ms.link == 'OBJECT' for ms in ob.material_slots) or (ob.type == 'MESH' and settings.use_triangles) + do_convert = ob.type in BLENDER_OTHER_OBJECT_TYPES + do_evaluate = do_convert and settings.use_mesh_modifiers - if settings.use_mesh_modifiers or settings.use_triangles or ob.type in BLENDER_OTHER_OBJECT_TYPES or is_ob_material: - # We cannot use default mesh in that case, or material would not be the right ones... - use_org_data = not (is_ob_material or ob.type in BLENDER_OTHER_OBJECT_TYPES) - apply_modifiers = False - backup_pose_positions = [] - tmp_mods = [] - if use_org_data and ob.type == 'MESH': - if settings.use_triangles: - use_org_data = False - # No need to create a new mesh in this case, if no modifier is active! - last_subsurf = None - for mod in ob.modifiers: - # For meshes, when armature export is enabled, disable Armature modifiers here! - # XXX Temp hacks here since currently we only have access to a viewport depsgraph... - # - # NOTE: We put armature to the rest pose instead of disabling it so we still - # have vertex groups in the evaluated mesh. - if mod.type == 'ARMATURE' and 'ARMATURE' in settings.object_types: - object = mod.object - if object and object.type == 'ARMATURE': - armature = object.data - # If armature is already in REST position, there's nothing to back-up - # This cuts down on export time dramatically, if all armatures are already in REST position - # by not triggering dependency graph update - if armature.pose_position != 'REST': - backup_pose_positions.append((armature, armature.pose_position)) - armature.pose_position = 'REST' - elif mod.show_render or mod.show_viewport: - # If exporting with subsurf collect the last Catmull-Clark subsurf modifier - # and disable it. We can use the original data as long as this is the first - # found applicable subsurf modifier. - if settings.use_subsurf and mod.type == 'SUBSURF' and mod.subdivision_type == 'CATMULL_CLARK': - if last_subsurf: - use_org_data = False - apply_modifiers = True - last_subsurf = mod - else: - use_org_data = False - apply_modifiers = True - if settings.use_subsurf and last_subsurf: - # XXX: When exporting with subsurf information temporarily disable - # the last subsurf modifier. - tmp_mods.append((last_subsurf, last_subsurf.show_render, last_subsurf.show_viewport)) - last_subsurf.show_render = False - last_subsurf.show_viewport = False - if not use_org_data: - # If modifiers has been altered need to update dependency graph. - if backup_pose_positions or tmp_mods: - depsgraph.update() - if apply_modifiers or ob.type in BLENDER_OTHER_OBJECT_TYPES: - ob_to_convert = ob.evaluated_get(depsgraph) - # NOTE: The dependency graph might be re-evaluating multiple times, which could - # potentially free the mesh created early on. So we put those meshes to bmain and - # free them afterwards. Not ideal but ensures correct ownerwhip. - tmp_me = bpy.data.meshes.new_from_object( - ob_to_convert, preserve_all_data_layers=True, depsgraph=depsgraph) + # If the Object is a mesh, and we're applying modifiers, check if there are actually any modifiers to apply. + # If there are then the mesh will need to be evaluated, and we may need to make some temporary changes before + # evaluating the mesh. + backup_pose_positions = [] + tmp_mods = [] + if ob.type == 'MESH' and settings.use_mesh_modifiers: + # No need to create a new mesh in this case, if no modifier is active! + last_subsurf = None + for mod in ob.modifiers: + # For meshes, when armature export is enabled, disable Armature modifiers here! + # XXX Temp hacks here since currently we only have access to a viewport depsgraph... + # + # NOTE: We put armature to the rest pose instead of disabling it so we still + # have vertex groups in the evaluated mesh. + if mod.type == 'ARMATURE' and 'ARMATURE' in settings.object_types: + object = mod.object + if object and object.type == 'ARMATURE': + armature = object.data + # If armature is already in REST position, there's nothing to back-up + # This cuts down on export time dramatically, if all armatures are already in REST position + # by not triggering dependency graph update + if armature.pose_position != 'REST': + backup_pose_positions.append((armature, armature.pose_position)) + armature.pose_position = 'REST' + elif mod.show_render or mod.show_viewport: + # If exporting with subsurf collect the last Catmull-Clark subsurf modifier + # and disable it. We can use the original data as long as this is the first + # found applicable subsurf modifier. + if settings.use_subsurf and mod.type == 'SUBSURF' and mod.subdivision_type == 'CATMULL_CLARK': + if last_subsurf: + do_evaluate = True + last_subsurf = mod + else: + do_evaluate = True + if settings.use_subsurf and last_subsurf: + # XXX: When exporting with subsurf information temporarily disable + # the last subsurf modifier. + tmp_mods.append((last_subsurf, last_subsurf.show_render, last_subsurf.show_viewport)) - # Usually the materials of the evaluated object will be the same, but modifiers, such as Geometry - # Nodes, can change the materials. - orig_mats = tuple(slot.material for slot in ob.material_slots) - eval_mats = tuple(slot.material.original if slot.material else None - for slot in ob_to_convert.material_slots) - if orig_mats != eval_mats: - # Override the default behaviour of getting materials from ob_obj.bdata.material_slots. - ob_obj.override_materials = eval_mats - else: - # bpy.data.meshes.new_from_object always removes shape keys (see #104714), so create a copy of the - # mesh instead. - tmp_me = ob.data.copy() - - # Triangulate the mesh if requested - if settings.use_triangles: - import bmesh - bm = bmesh.new() - bm.from_mesh(tmp_me) - bmesh.ops.triangulate(bm, faces=bm.faces) - bm.to_mesh(tmp_me) - bm.free() - - data_meshes[ob_obj] = (get_blenderID_key(tmp_me), tmp_me, True) - # Change armatures back. - for armature, pose_position in backup_pose_positions: - print((armature, pose_position)) - armature.pose_position = pose_position - # Update now, so we don't leave modified state after last object was exported. - # Re-enable temporary disabled modifiers. - for mod, show_render, show_viewport in tmp_mods: - mod.show_render = show_render - mod.show_viewport = show_viewport + if do_evaluate: + # If modifiers has been altered need to update dependency graph. if backup_pose_positions or tmp_mods: depsgraph.update() - if use_org_data: + ob_to_convert = ob.evaluated_get(depsgraph) + # NOTE: The dependency graph might be re-evaluating multiple times, which could + # potentially free the mesh created early on. So we put those meshes to bmain and + # free them afterwards. Not ideal but ensures correct ownership. + tmp_me = bpy.data.meshes.new_from_object( + ob_to_convert, preserve_all_data_layers=True, depsgraph=depsgraph) + + # Usually the materials of the evaluated object will be the same, but modifiers, such as Geometry + # Nodes, can change the materials. + orig_mats = tuple(slot.material for slot in ob.material_slots) + eval_mats = tuple(slot.material.original if slot.material else None + for slot in ob_to_convert.material_slots) + if orig_mats != eval_mats: + # Override the default behaviour of getting materials from ob_obj.bdata.material_slots. + ob_obj.override_materials = eval_mats + elif do_convert: + tmp_me = bpy.data.meshes.new_from_object(ob, preserve_all_data_layers=True, depsgraph=depsgraph) + elif do_copy: + # bpy.data.meshes.new_from_object always removes shape keys (see #104714), so create a direct copy of the + # mesh instead. + tmp_me = ob.data.copy() + else: + tmp_me = None + + if tmp_me is None: + # Use the original data of this Object. data_meshes[ob_obj] = (get_blenderID_key(ob.data), ob.data, False) + else: + # Triangulate the mesh if requested + if settings.use_triangles: + import bmesh + bm = bmesh.new() + bm.from_mesh(tmp_me) + bmesh.ops.triangulate(bm, faces=bm.faces) + bm.to_mesh(tmp_me) + bm.free() + # A temporary mesh was created for this Object, which should be deleted once the export is complete. + data_meshes[ob_obj] = (get_blenderID_key(tmp_me), tmp_me, True) + + # Change armatures back. + for armature, pose_position in backup_pose_positions: + print((armature, pose_position)) + armature.pose_position = pose_position + # Update now, so we don't leave modified state after last object was exported. + # Re-enable temporary disabled modifiers. + for mod, show_render, show_viewport in tmp_mods: + mod.show_render = show_render + mod.show_viewport = show_viewport + if backup_pose_positions or tmp_mods: + depsgraph.update() # In case "real" source object of that dupli did not yet still existed in data_meshes, create it now! if org_ob_obj is not None: -- 2.30.2 From 49e661786138c15c8901da073986bdec7f8b483c Mon Sep 17 00:00:00 2001 From: Thomas Barlow Date: Fri, 15 Sep 2023 22:28:14 +0100 Subject: [PATCH 5/6] Tidy up --- io_scene_fbx/export_fbx_bin.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/io_scene_fbx/export_fbx_bin.py b/io_scene_fbx/export_fbx_bin.py index e46ccd366..824a3bbb7 100644 --- a/io_scene_fbx/export_fbx_bin.py +++ b/io_scene_fbx/export_fbx_bin.py @@ -2548,19 +2548,22 @@ def fbx_data_from_scene(scene, depsgraph, settings): # There are 4 different cases for what we need to do with the original data of each Object: # 1) The original data can be used without changes. # 2) A copy of the original data needs to be made. - # - If a mesh needs to be modified upon export, e.g. it needs triangulating. - # - If a mesh has Object-linked materials. This is to do with how materials are currently mapped to FBX. - # 3) A non-mesh needs to be converted to a mesh. + # - If an export option modifies the data, e.g. Triangulate Faces is enabled. + # - If the Object has Object-linked materials. This is because our current mapping of materials to FBX requires + # that multiple Objects sharing a single mesh must have the same materials. + # 3) The Object needs to be converted to a mesh. + # - All mesh-like Objects that are not meshes need to be converted to a mesh in order to be exported. # 4) The Object needs to be evaluated and then converted to a mesh. - # - Whenever use_mesh_modifiers is enabled and either there are modifiers to apply or the Object is not a mesh. + # - Whenever use_mesh_modifiers is enabled and either there are modifiers to apply or the Object needs to be + # converted to a mesh. # If multiple cases apply to an Object, then only the last applicable case is relevant. - do_copy = any(ms.link == 'OBJECT' for ms in ob.material_slots) or (ob.type == 'MESH' and settings.use_triangles) + do_copy = any(ms.link == 'OBJECT' for ms in ob.material_slots) or settings.use_triangles do_convert = ob.type in BLENDER_OTHER_OBJECT_TYPES do_evaluate = do_convert and settings.use_mesh_modifiers # If the Object is a mesh, and we're applying modifiers, check if there are actually any modifiers to apply. - # If there are then the mesh will need to be evaluated, and we may need to make some temporary changes before - # evaluating the mesh. + # If there are then the mesh will need to be evaluated, and we may need to make some temporary changes to the + # modifiers or scene before the mesh is evaluated. backup_pose_positions = [] tmp_mods = [] if ob.type == 'MESH' and settings.use_mesh_modifiers: @@ -2608,8 +2611,8 @@ def fbx_data_from_scene(scene, depsgraph, settings): tmp_me = bpy.data.meshes.new_from_object( ob_to_convert, preserve_all_data_layers=True, depsgraph=depsgraph) - # Usually the materials of the evaluated object will be the same, but modifiers, such as Geometry - # Nodes, can change the materials. + # Usually the materials of the evaluated object will be the same, but modifiers, such as Geometry Nodes, + # can change the materials. orig_mats = tuple(slot.material for slot in ob.material_slots) eval_mats = tuple(slot.material.original if slot.material else None for slot in ob_to_convert.material_slots) @@ -2619,8 +2622,7 @@ def fbx_data_from_scene(scene, depsgraph, settings): elif do_convert: tmp_me = bpy.data.meshes.new_from_object(ob, preserve_all_data_layers=True, depsgraph=depsgraph) elif do_copy: - # bpy.data.meshes.new_from_object always removes shape keys (see #104714), so create a direct copy of the - # mesh instead. + # bpy.data.meshes.new_from_object removes shape keys (see #104714), so create a copy of the mesh instead. tmp_me = ob.data.copy() else: tmp_me = None -- 2.30.2 From e6814688e2727ed5c5aaa6a718e2310671f3d516 Mon Sep 17 00:00:00 2001 From: Thomas Barlow Date: Tue, 19 Sep 2023 01:10:42 +0100 Subject: [PATCH 6/6] Increase FBX IO 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 9ac5187ab..19d765fcb 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, 0), + "version": (5, 8, 1), "blender": (3, 6, 0), "location": "File > Import-Export", "description": "FBX IO meshes, UVs, vertex colors, materials, textures, cameras, lamps and actions", -- 2.30.2