From f1dd37b8ac8f2db93f56002bae24e70b2cbf986f Mon Sep 17 00:00:00 2001 From: Samuli Raivio Date: Thu, 29 Aug 2019 12:01:39 +0200 Subject: Add optional subdivision surface support to the FBX exporter Add option 'Export Subdivision Surface' to the FBX exporter (disabled by default). When enabled the exporter will write the **last active Catmull-Clark subdivision surface modifier** as FBX properties instead of applying it. Edge crease data is also written to the FBX file if 'Use Creases' is enabled in the subsurf modifier. Reviewers: mont29 Tags: #add-ons Differential Revision: https://developer.blender.org/D4982 --- io_scene_fbx/__init__.py | 17 +++++++++- io_scene_fbx/export_fbx_bin.py | 75 +++++++++++++++++++++++++++++++++++++++--- io_scene_fbx/fbx_utils.py | 4 ++- io_scene_fbx/import_fbx.py | 75 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 164 insertions(+), 7 deletions(-) diff --git a/io_scene_fbx/__init__.py b/io_scene_fbx/__init__.py index bb9528af..ebbf2a19 100644 --- a/io_scene_fbx/__init__.py +++ b/io_scene_fbx/__init__.py @@ -21,7 +21,7 @@ bl_info = { "name": "FBX format", "author": "Campbell Barton, Bastien Montagne, Jens Restemeier", - "version": (4, 14, 15), + "version": (4, 15, 0), "blender": (2, 80, 0), "location": "File > Import-Export", "description": "FBX IO meshes, UV's, vertex colors, materials, textures, cameras, lamps and actions", @@ -131,6 +131,12 @@ class ImportFBX(bpy.types.Operator, ImportHelper): default=1.0, ) + use_subsurf: BoolProperty( + name="Import Subdivision Surface", + description="Import FBX subdivision information as subdivision surface modifiers", + default=False, + ) + use_custom_props: BoolProperty( name="Import User Properties", description="Import user properties as custom properties", @@ -205,6 +211,8 @@ class ImportFBX(bpy.types.Operator, ImportHelper): layout.prop(self, "use_anim") layout.prop(self, "anim_offset") + layout.prop(self, "use_subsurf") + layout.prop(self, "use_custom_props") sub = layout.row() sub.enabled = self.use_custom_props @@ -334,6 +342,12 @@ class ExportFBX(bpy.types.Operator, ExportHelper): "(prefer 'Normals Only' option if your target importer understand split normals)", default='OFF', ) + use_subsurf: BoolProperty( + name="Export Subdivision Surface", + description="Export the last Catmull-Rom subidivion modifier as FBX subdivision " + "(Does not apply the modifier even if 'Apply Modifiers' is enabled)", + default=False, + ) use_mesh_edges: BoolProperty( name="Loose Edges", description="Export loose edges (as two-vertices polygons)", @@ -507,6 +521,7 @@ class ExportFBX(bpy.types.Operator, ExportHelper): sub.enabled = self.use_mesh_modifiers and False # disabled in 2.8... sub.prop(self, "use_mesh_modifiers_render") layout.prop(self, "mesh_smooth_type") + layout.prop(self, "use_subsurf") layout.prop(self, "use_mesh_edges") sub = layout.row() #~ sub.enabled = self.mesh_smooth_type in {'OFF'} diff --git a/io_scene_fbx/export_fbx_bin.py b/io_scene_fbx/export_fbx_bin.py index 9292d656..5cba16e6 100644 --- a/io_scene_fbx/export_fbx_bin.py +++ b/io_scene_fbx/export_fbx_bin.py @@ -49,7 +49,7 @@ from .fbx_utils import ( FBX_VERSION, FBX_HEADER_VERSION, FBX_SCENEINFO_VERSION, FBX_TEMPLATES_VERSION, FBX_MODELS_VERSION, FBX_GEOMETRY_VERSION, FBX_GEOMETRY_NORMAL_VERSION, FBX_GEOMETRY_BINORMAL_VERSION, FBX_GEOMETRY_TANGENT_VERSION, - FBX_GEOMETRY_SMOOTHING_VERSION, FBX_GEOMETRY_VCOLOR_VERSION, FBX_GEOMETRY_UV_VERSION, + FBX_GEOMETRY_SMOOTHING_VERSION, FBX_GEOMETRY_CREASE_VERSION, FBX_GEOMETRY_VCOLOR_VERSION, FBX_GEOMETRY_UV_VERSION, FBX_GEOMETRY_MATERIAL_VERSION, FBX_GEOMETRY_LAYER_VERSION, FBX_GEOMETRY_SHAPE_VERSION, FBX_DEFORMER_SHAPE_VERSION, FBX_DEFORMER_SHAPECHANNEL_VERSION, FBX_POSE_BIND_VERSION, FBX_DEFORMER_SKIN_VERSION, FBX_DEFORMER_CLUSTER_VERSION, @@ -865,6 +865,30 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes): if scene_data.settings.use_custom_props: fbx_data_element_custom_properties(props, me) + # Subdivision levels. Take them from the first found subsurf modifier from the + # first object that has the mesh. Write crease information if the object has + # and subsurf modifier. + write_crease = False + if scene_data.settings.use_subsurf: + last_subsurf = None + for mod in me_obj.bdata.modifiers: + if not (mod.show_render or mod.show_viewport): + continue + if mod.type == 'SUBSURF' and mod.subdivision_type == 'CATMULL_CLARK': + last_subsurf = mod + + if last_subsurf: + elem_data_single_int32(geom, b"Smoothness", 2) # Display control mesh and smoothed + elem_data_single_int32(geom, b"BoundaryRule", 2) # Round edges like Blender + elem_data_single_int32(geom, b"PreviewDivisionLevels", last_subsurf.levels) + elem_data_single_int32(geom, b"RenderDivisionLevels", last_subsurf.render_levels) + + elem_data_single_int32(geom, b"PreserveBorders", 0) + elem_data_single_int32(geom, b"PreserveHardEdges", 0) + elem_data_single_int32(geom, b"PropagateEdgeHardness", 0) + + write_crease = mod.use_creases + elem_data_single_int32(geom, b"GeometryVersion", FBX_GEOMETRY_VERSION) # Vertex cos. @@ -980,7 +1004,21 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes): elem_data_single_int32_array(lay_smooth, b"Smoothing", t_ps) # Sight, int32 for bool... del t_ps - # TODO: Edge crease (LayerElementCrease). + # Edge crease for subdivision + if write_crease: + t_ec = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * edges_nbr + for e in me.edges: + if e.key not in edges_map: + continue # Only loose edges, in theory! + t_ec[edges_map[e.key]] = e.crease + + lay_crease = elem_data_single_int32(geom, b"LayerElementEdgeCrease", 0) + elem_data_single_int32(lay_crease, b"Version", FBX_GEOMETRY_CREASE_VERSION) + elem_data_single_string(lay_crease, b"Name", b"") + elem_data_single_string(lay_crease, b"MappingInformationType", b"ByEdge") + elem_data_single_string(lay_crease, b"ReferenceInformationType", b"Direct") + elem_data_single_float64_array(lay_crease, b"EdgeCrease", t_ec) + del t_ec # And we are done with edges! del edges_map @@ -1193,6 +1231,10 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes): lay_smooth = elem_empty(layer, b"LayerElement") elem_data_single_string(lay_smooth, b"Type", b"LayerElementSmoothing") elem_data_single_int32(lay_smooth, b"TypedIndex", 0) + if write_crease: + lay_smooth = elem_empty(layer, b"LayerElement") + elem_data_single_string(lay_smooth, b"Type", b"LayerElementEdgeCrease") + elem_data_single_int32(lay_smooth, b"TypedIndex", 0) if vcolnumber: lay_vcol = elem_empty(layer, b"LayerElement") elem_data_single_string(lay_vcol, b"Type", b"LayerElementColor") @@ -2217,8 +2259,10 @@ def fbx_data_from_scene(scene, depsgraph, settings): # 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) backup_pose_positions = [] + tmp_mods = [] if use_org_data and ob.type == 'MESH': # 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... @@ -2232,10 +2276,24 @@ def fbx_data_from_scene(scene, depsgraph, settings): backup_pose_positions.append((armature, armature.pose_position)) armature.pose_position = 'REST' elif mod.show_render or mod.show_viewport: - use_org_data = False + # 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 + last_subsurf = mod + else: + use_org_data = False + 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: + 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 @@ -2249,6 +2307,11 @@ def fbx_data_from_scene(scene, depsgraph, settings): 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() if use_org_data: data_meshes[ob_obj] = (get_blenderID_key(ob.data), ob.data, False) @@ -2913,6 +2976,7 @@ def save_single(operator, scene, depsgraph, filepath="", use_mesh_modifiers=True, use_mesh_modifiers_render=True, mesh_smooth_type='FACE', + use_subsurf=False, use_armature_deform_only=False, bake_anim=True, bake_anim_use_all_bones=True, @@ -2994,7 +3058,7 @@ def save_single(operator, scene, depsgraph, filepath="", operator.report, (axis_up, axis_forward), global_matrix, global_scale, apply_unit_scale, unit_scale, 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_mesh_edges, use_tspace, + mesh_smooth_type, use_subsurf, use_mesh_edges, use_tspace, 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, @@ -3066,6 +3130,7 @@ def defaults_unity3d(): "use_mesh_modifiers_render": True, "use_mesh_edges": False, "mesh_smooth_type": 'FACE', + "use_subsurf": False, "use_tspace": False, # XXX Why? Unity is expected to support tspace import... "use_armature_deform_only": True, diff --git a/io_scene_fbx/fbx_utils.py b/io_scene_fbx/fbx_utils.py index 19f32800..a496075d 100644 --- a/io_scene_fbx/fbx_utils.py +++ b/io_scene_fbx/fbx_utils.py @@ -51,6 +51,7 @@ FBX_GEOMETRY_NORMAL_VERSION = 101 FBX_GEOMETRY_BINORMAL_VERSION = 101 FBX_GEOMETRY_TANGENT_VERSION = 101 FBX_GEOMETRY_SMOOTHING_VERSION = 102 +FBX_GEOMETRY_CREASE_VERSION = 101 FBX_GEOMETRY_VCOLOR_VERSION = 101 FBX_GEOMETRY_UV_VERSION = 101 FBX_GEOMETRY_MATERIAL_VERSION = 101 @@ -1215,7 +1216,7 @@ FBXExportSettings = namedtuple("FBXExportSettings", ( "report", "to_axes", "global_matrix", "global_scale", "apply_unit_scale", "unit_scale", "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_mesh_edges", "use_tspace", + "mesh_smooth_type", "use_subsurf", "use_mesh_edges", "use_tspace", "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", @@ -1245,6 +1246,7 @@ FBXImportSettings = namedtuple("FBXImportSettings", ( "use_custom_normals", "use_image_search", "use_alpha_decals", "decal_offset", "use_anim", "anim_offset", + "use_subsurf", "use_custom_props", "use_custom_props_enum_as_string", "nodal_material_wrap_map", "image_cache", "ignore_leaf_bones", "force_connect_children", "automatic_bone_orientation", "bone_correction_matrix", diff --git a/io_scene_fbx/import_fbx.py b/io_scene_fbx/import_fbx.py index d94a237c..90c27e5b 100644 --- a/io_scene_fbx/import_fbx.py +++ b/io_scene_fbx/import_fbx.py @@ -1112,6 +1112,45 @@ def blen_read_geom_layer_smooth(fbx_obj, mesh): print("warning layer %r mapping type unsupported: %r" % (fbx_layer.id, fbx_layer_mapping)) return False +def blen_read_geom_layer_edge_crease(fbx_obj, mesh): + fbx_layer = elem_find_first(fbx_obj, b'LayerElementEdgeCrease') + + if fbx_layer is None: + return False + + # all should be valid + (fbx_layer_name, + fbx_layer_mapping, + fbx_layer_ref, + ) = blen_read_geom_layerinfo(fbx_layer) + + if fbx_layer_mapping != b'ByEdge': + return False + + layer_id = b'EdgeCrease' + fbx_layer_data = elem_prop_first(elem_find_first(fbx_layer, layer_id)) + + # some models have bad edge data, we cant use this info... + if not mesh.edges: + print("warning skipping edge crease data, no valid edges...") + return False + + if fbx_layer_mapping == b'ByEdge': + # some models have bad edge data, we cant use this info... + if not mesh.edges: + print("warning skipping edge crease data, no valid edges...") + return False + + blen_data = mesh.edges + return blen_read_geom_array_mapped_edge( + mesh, blen_data, "crease", + fbx_layer_data, None, + fbx_layer_mapping, fbx_layer_ref, + 1, 1, layer_id, + ) + else: + print("warning layer %r mapping type unsupported: %r" % (fbx_layer.id, fbx_layer_mapping)) + return False def blen_read_geom_layer_normal(fbx_obj, mesh, xform=None): fbx_layer = elem_find_first(fbx_obj, b'LayerElementNormal') @@ -1243,6 +1282,8 @@ def blen_read_geom(fbx_tmpl, fbx_obj, settings): # must be after edge, face loading. ok_smooth = blen_read_geom_layer_smooth(fbx_obj, mesh) + ok_crease = blen_read_geom_layer_edge_crease(fbx_obj, mesh) + ok_normals = False if settings.use_custom_normals: # Note: we store 'temp' normals in loops, since validate() may alter final mesh, @@ -1276,6 +1317,9 @@ def blen_read_geom(fbx_tmpl, fbx_obj, settings): if not ok_smooth: mesh.polygons.foreach_set("use_smooth", [True] * len(mesh.polygons)) + if ok_crease: + mesh.use_customdata_edge_crease = True + if settings.use_custom_props: blen_read_custom_properties(fbx_obj, mesh, settings) @@ -2270,6 +2314,7 @@ def load(operator, context, filepath="", decal_offset=0.0, use_anim=True, anim_offset=1.0, + use_subsurf=False, use_custom_props=True, use_custom_props_enum_as_string=True, ignore_leaf_bones=False, @@ -2398,6 +2443,7 @@ def load(operator, context, filepath="", use_custom_normals, use_image_search, use_alpha_decals, decal_offset, use_anim, anim_offset, + use_subsurf, use_custom_props, use_custom_props_enum_as_string, nodal_material_wrap_map, image_cache, ignore_leaf_bones, force_connect_children, automatic_bone_orientation, bone_correction_matrix, @@ -2799,6 +2845,35 @@ def load(operator, context, filepath="", blend_shape_channels[bc_uuid] = keyblocks _(); del _ + if settings.use_subsurf: + perfmon.step("FBX import: Subdivision surfaces") + + # Look through connections for subsurf in meshes and add it to the parent object + def _(): + for fbx_link in fbx_connections.elems: + if fbx_link.props[0] != b'OO': + continue + if fbx_link.props_type[1:3] == b'LL': + c_src, c_dst = fbx_link.props[1:3] + parent = fbx_helper_nodes.get(c_dst) + if parent is None: + continue + + child = fbx_helper_nodes.get(c_src) + if child is None: + fbx_sdata, bl_data = fbx_table_nodes.get(c_src, (None, None)) + if fbx_sdata.id != b'Geometry': + continue + + preview_levels = elem_prop_first(elem_find_first(fbx_sdata, b'PreviewDivisionLevels')) + render_levels = elem_prop_first(elem_find_first(fbx_sdata, b'RenderDivisionLevels')) + if isinstance(preview_levels, int) and isinstance(render_levels, int): + mod = parent.bl_obj.modifiers.new('subsurf', 'SUBSURF') + mod.levels = preview_levels + mod.render_levels = render_levels + + _(); del _ + if use_anim: perfmon.step("FBX import: Animations...") -- cgit v1.2.3