Skip to content
Snippets Groups Projects
import_fbx.py 54.5 KiB
Newer Older
  • Learn to ignore specific revisions
  • 
        #### Get some info from GlobalSettings.
    
        fbx_settings = elem_find_first(elem_root, b'GlobalSettings')
        fbx_settings_props = elem_find_first(fbx_settings, b'Properties70')
        if fbx_settings is None or fbx_settings_props is None:
            operator.report({'ERROR'}, "No 'GlobalSettings' found in file %r" % filepath)
            return {'CANCELLED'}
    
        # Compute global matrix and scale.
        if not use_manual_orientation:
            axis_forward = (elem_props_get_integer(fbx_settings_props, b'FrontAxis', 1),
                            elem_props_get_integer(fbx_settings_props, b'FrontAxisSign', 1))
            axis_up = (elem_props_get_integer(fbx_settings_props, b'UpAxis', 2),
                       elem_props_get_integer(fbx_settings_props, b'UpAxisSign', 1))
            axis_coord = (elem_props_get_integer(fbx_settings_props, b'CoordAxis', 0),
                          elem_props_get_integer(fbx_settings_props, b'CoordAxisSign', 1))
    
            axis_key = (axis_up, axis_forward, axis_coord)
            axis_up, axis_forward = {v: k for k, v in RIGHT_HAND_AXES.items()}.get(axis_key, ('Z', 'Y'))
    
            # FBX base unit seems to be the centimeter, while raw Blender Unit is equivalent to the meter...
            global_scale = elem_props_get_number(fbx_settings_props, b'UnitScaleFactor', 100.0) / 100.0
        global_matrix = (Matrix.Scale(global_scale, 4) *
                         axis_conversion(from_forward=axis_forward, from_up=axis_up).to_4x4())
    
        # Compute framerate settings.
        custom_fps = elem_props_get_number(fbx_settings_props, b'CustomFrameRate', 25.0)
        time_mode = elem_props_get_enum(fbx_settings_props, b'TimeMode')
        real_fps = {eid: val for val, eid in FBX_FRAMERATES[1:]}.get(time_mode, custom_fps)
        if real_fps < 0.0:
            real_fps = 25.0
        scene.render.fps = round(real_fps)
        scene.render.fps_base = scene.render.fps / real_fps
    
    
        #### And now, the "real" data.
    
    
        fbx_defs = elem_find_first(elem_root, b'Definitions')  # can be None
    
        fbx_nodes = elem_find_first(elem_root, b'Objects')
        fbx_connections = elem_find_first(elem_root, b'Connections')
    
        if fbx_nodes is None:
    
            operator.report({'ERROR'}, "No 'Objects' found in file %r" % filepath)
            return {'CANCELLED'}
    
        if fbx_connections is None:
    
            operator.report({'ERROR'}, "No 'Connections' found in file %r" % filepath)
            return {'CANCELLED'}
    
        # ----
        # First load property templates
        # Load 'PropertyTemplate' values.
        # Key is a tuple, (ObjectType, FBXNodeType)
        # eg, (b'Texture', b'KFbxFileTexture')
        #     (b'Geometry', b'KFbxMesh')
        fbx_templates = {}
    
        def _():
            if fbx_defs is not None:
                for fbx_def in fbx_defs.elems:
                    if fbx_def.id == b'ObjectType':
                        for fbx_subdef in fbx_def.elems:
                            if fbx_subdef.id == b'PropertyTemplate':
                                assert(fbx_def.props_type == b'S')
                                assert(fbx_subdef.props_type == b'S')
                                # (b'Texture', b'KFbxFileTexture') - eg.
                                key = fbx_def.props[0], fbx_subdef.props[0]
                                fbx_templates[key] = fbx_subdef
        _(); del _
    
        def fbx_template_get(key):
    
            ret = fbx_templates.get(key, fbx_elem_nil)
            if ret is None:
                # Newest FBX (7.4 and above) use no more 'K' in their type names...
                key = (key[0], key[1][1:])
                return fbx_templates.get(key, fbx_elem_nil)
            return ret
    
        def _():
            for fbx_obj in fbx_nodes.elems:
    
                # TODO, investigate what other items after first 3 may be
                assert(fbx_obj.props_type[:3] == b'LSS')
    
                fbx_uuid = elem_uuid(fbx_obj)
                fbx_table_nodes[fbx_uuid] = [fbx_obj, None]
        _(); del _
    
        # ----
    
    Bastien Montagne's avatar
    Bastien Montagne committed
        # http://download.autodesk.com/us/fbx/20112/FBX_SDK_HELP/index.html?url=
        #        WS73099cc142f487551fea285e1221e4f9ff8-7fda.htm,topicNumber=d0e6388
    
    
        fbx_connection_map = {}
        fbx_connection_map_reverse = {}
    
        def _():
            for fbx_link in fbx_connections.elems:
                c_type = fbx_link.props[0]
    
                if fbx_link.props_type[1:3] == b'LL':
                    c_src, c_dst = fbx_link.props[1:3]
                    fbx_connection_map.setdefault(c_src, []).append((c_dst, fbx_link))
                    fbx_connection_map_reverse.setdefault(c_dst, []).append((c_src, fbx_link))
    
            fbx_tmpl = fbx_template_get((b'Geometry', b'KFbxMesh'))
    
    
            for fbx_uuid, fbx_item in fbx_table_nodes.items():
                fbx_obj, blen_data = fbx_item
                if fbx_obj.id != b'Geometry':
                    continue
                if fbx_obj.props[-1] == b'Mesh':
                    assert(blen_data is None)
    
                    fbx_item[1] = blen_read_geom(fbx_tmpl, fbx_obj)
    
        _(); del _
    
        # ----
        # Load material data
        def _():
    
            fbx_tmpl = fbx_template_get((b'Material', b'KFbxSurfacePhong'))
            # b'KFbxSurfaceLambert'
    
    
            for fbx_uuid, fbx_item in fbx_table_nodes.items():
                fbx_obj, blen_data = fbx_item
                if fbx_obj.id != b'Material':
                    continue
                assert(blen_data is None)
    
                fbx_item[1] = blen_read_material(fbx_tmpl, fbx_obj,
    
                                                 cycles_material_wrap_map, use_cycles)
        _(); del _
    
        # ----
        # Load image data
        def _():
    
            fbx_tmpl = fbx_template_get((b'Texture', b'KFbxFileTexture'))
    
    
            for fbx_uuid, fbx_item in fbx_table_nodes.items():
                fbx_obj, blen_data = fbx_item
                if fbx_obj.id != b'Texture':
                    continue
    
                fbx_item[1] = blen_read_texture(fbx_tmpl, fbx_obj, basedir, image_cache,
    
        # ----
        # Load camera data
        def _():
    
            fbx_tmpl = fbx_template_get((b'NodeAttribute', b'KFbxCamera'))
    
    
            for fbx_uuid, fbx_item in fbx_table_nodes.items():
                fbx_obj, blen_data = fbx_item
                if fbx_obj.id != b'NodeAttribute':
                    continue
                if fbx_obj.props[-1] == b'Camera':
                    assert(blen_data is None)
    
                    fbx_item[1] = blen_read_camera(fbx_tmpl, fbx_obj, global_scale)
    
        _(); del _
    
        # ----
        # Load lamp data
        def _():
    
            fbx_tmpl = fbx_template_get((b'NodeAttribute', b'KFbxLight'))
    
    
            for fbx_uuid, fbx_item in fbx_table_nodes.items():
                fbx_obj, blen_data = fbx_item
                if fbx_obj.id != b'NodeAttribute':
                    continue
                if fbx_obj.props[-1] == b'Light':
                    assert(blen_data is None)
    
                    fbx_item[1] = blen_read_light(fbx_tmpl, fbx_obj, global_scale)
    
        # ----
        # Connections
        def connection_filter_ex(fbx_uuid, fbx_id, dct):
            return [(c_found[0], c_found[1], c_type)
                    for (c_uuid, c_type) in dct.get(fbx_uuid, ())
    
                    # 0 is used for the root node, which isnt in fbx_table_nodes
                    for c_found in (() if c_uuid is 0 else (fbx_table_nodes[c_uuid],))
    
                    if (fbx_id is None) or (c_found[0].id == fbx_id)]
    
    
        def connection_filter_forward(fbx_uuid, fbx_id):
            return connection_filter_ex(fbx_uuid, fbx_id, fbx_connection_map)
    
        def connection_filter_reverse(fbx_uuid, fbx_id):
            return connection_filter_ex(fbx_uuid, fbx_id, fbx_connection_map_reverse)
    
        def _():
    
            fbx_tmpl = fbx_template_get((b'Model', b'KFbxNode'))
    
    
            # Link objects, keep first, this also creates objects
    
            for fbx_uuid, fbx_item in fbx_table_nodes.items():
                fbx_obj, blen_data = fbx_item
                if fbx_obj.id != b'Model':
                    continue
    
    
                # Create empty object or search for object data
                if fbx_obj.props[2] == b'Null':
                    fbx_lnk_item = None
                    ok = True
                else:
                    ok = False
    
                    for (fbx_lnk,
                         fbx_lnk_item,
                         fbx_lnk_type) in connection_filter_reverse(fbx_uuid, None):
    
    
                        if fbx_lnk_type.props[0] != b'OO':
                            continue
                        if not isinstance(fbx_lnk_item, bpy.types.ID):
                            continue
                        if isinstance(fbx_lnk_item, (bpy.types.Material, bpy.types.Image)):
                            continue
    
                        # Need to check why this happens, Bird_Leg.fbx
    
                        # This is basic object parenting, also used by "bones".
    
                        if isinstance(fbx_lnk_item, (bpy.types.Object)):
                            continue
    
                        ok = True
                        break
                if ok:
    
                    # create when linking since we need object data
    
                    obj = blen_read_object(fbx_tmpl, fbx_obj, fbx_lnk_item)
    
                    assert(fbx_item[1] is None)
                    fbx_item[1] = obj
    
    
                    # instance in scene
                    obj_base = scene.objects.link(obj)
                    obj_base.select = True
    
        def _():
    
            # Parent objects, after we created them...
    
            for fbx_uuid, fbx_item in fbx_table_nodes.items():
                fbx_obj, blen_data = fbx_item
                if fbx_obj.id != b'Model':
                    continue
                if fbx_item[1] is None:
                    continue  # no object loaded.. ignore
    
    
                for (fbx_lnk,
                     fbx_lnk_item,
                     fbx_lnk_type) in connection_filter_forward(fbx_uuid, b'Model'):
    
    
                    fbx_item[1].parent = fbx_lnk_item
        _(); del _
    
        def _():
            if global_matrix is not None:
                # Apply global matrix last (after parenting)
                for fbx_uuid, fbx_item in fbx_table_nodes.items():
                    fbx_obj, blen_data = fbx_item
                    if fbx_obj.id != b'Model':
                        continue
                    if fbx_item[1] is None:
                        continue  # no object loaded.. ignore
    
                    if fbx_item[1].parent is None:
                        fbx_item[1].matrix_basis = global_matrix * fbx_item[1].matrix_basis
    
        def _():
            # link Material's to Geometry (via Model's)
            for fbx_uuid, fbx_item in fbx_table_nodes.items():
                fbx_obj, blen_data = fbx_item
                if fbx_obj.id != b'Geometry':
                    continue
    
                mesh = fbx_table_nodes[fbx_uuid][1]
    
    
                # can happen in rare cases
                if mesh is None:
                    continue
    
    
                for (fbx_lnk,
                     fbx_lnk_item,
                     fbx_lnk_type) in connection_filter_forward(fbx_uuid, b'Model'):
    
    
                    # link materials
                    fbx_lnk_uuid = elem_uuid(fbx_lnk)
    
                    for (fbx_lnk_material,
                         material,
                         fbx_lnk_material_type) in connection_filter_reverse(fbx_lnk_uuid, b'Material'):
    
    
                        mesh.materials.append(material)
    
            fbx_tmpl = fbx_template_get((b'Material', b'KFbxSurfacePhong'))
            # b'KFbxSurfaceLambert'
    
    
            # textures that use this material
            def texture_bumpfac_get(fbx_obj):
    
                assert(fbx_obj.id == b'Material')
    
                fbx_props = (elem_find_first(fbx_obj, b'Properties70'),
                             elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil))
                assert(fbx_props[0] is not None)
    
                # (x / 7.142) is only a guess, cycles usable range is (0.0 -> 0.5)
                return elem_props_get_number(fbx_props, b'BumpFactor', 2.5) / 7.142
    
            def texture_mapping_get(fbx_obj):
                assert(fbx_obj.id == b'Texture')
    
                fbx_props = (elem_find_first(fbx_obj, b'Properties70'),
                             elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil))
                assert(fbx_props[0] is not None)
                return (elem_props_get_vector_3d(fbx_props, b'Translation', (0.0, 0.0, 0.0)),
                        elem_props_get_vector_3d(fbx_props, b'Rotation', (0.0, 0.0, 0.0)),
                        elem_props_get_vector_3d(fbx_props, b'Scaling', (1.0, 1.0, 1.0)),
    
                        (bool(elem_props_get_enum(fbx_props, b'WrapModeU', 0)),
                         bool(elem_props_get_enum(fbx_props, b'WrapModeV', 0))))
    
            if not use_cycles:
                # Simple function to make a new mtex and set defaults
    
                def material_mtex_new(material, image, tex_map):
    
                    tex = texture_cache.get(image)
                    if tex is None:
                        tex = bpy.data.textures.new(name=image.name, type='IMAGE')
                        tex.image = image
                        texture_cache[image] = tex
    
                    mtex = material.texture_slots.add()
                    mtex.texture = tex
                    mtex.texture_coords = 'UV'
                    mtex.use_map_color_diffuse = False
    
    
                    # No rotation here...
                    mtex.offset[:] = tex_map[0]
                    mtex.scale[:] = tex_map[2]
    
            for fbx_uuid, fbx_item in fbx_table_nodes.items():
                fbx_obj, blen_data = fbx_item
                if fbx_obj.id != b'Material':
                    continue
    
                material = fbx_table_nodes[fbx_uuid][1]
    
                for (fbx_lnk,
                     image,
                     fbx_lnk_type) in connection_filter_reverse(fbx_uuid, b'Texture'):
    
    
                    if use_cycles:
                        if fbx_lnk_type.props[0] == b'OP':
                            lnk_type = fbx_lnk_type.props[3]
    
                            ma_wrap = cycles_material_wrap_map[material]
    
    
                            # tx/rot/scale
                            tex_map = texture_mapping_get(fbx_lnk)
                            if (tex_map[0] == (0.0, 0.0, 0.0) and
    
    Bastien Montagne's avatar
    Bastien Montagne committed
                                    tex_map[1] == (0.0, 0.0, 0.0) and
                                    tex_map[2] == (1.0, 1.0, 1.0) and
                                    tex_map[3] == (False, False)):
    
                                use_mapping = False
                            else:
                                use_mapping = True
                                tex_map_kw = {
                                    "translation": tex_map[0],
    
                                    "rotation": [-i for i in tex_map[1]],
                                    "scale": [((1.0 / i) if i != 0.0 else 1.0) for i in tex_map[2]],
    
                            if lnk_type == b'DiffuseColor':
                                ma_wrap.diffuse_image_set(image)
    
                                if use_mapping:
                                    ma_wrap.diffuse_mapping_set(**tex_map_kw)
    
                            elif lnk_type == b'SpecularColor':
                                ma_wrap.specular_image_set(image)
    
                                if use_mapping:
                                    ma_wrap.specular_mapping_set(**tex_map_kw)
    
                            elif lnk_type == b'ReflectionColor':
                                ma_wrap.reflect_image_set(image)
    
                                if use_mapping:
                                    ma_wrap.reflect_mapping_set(**tex_map_kw)
    
                            elif lnk_type == b'TransparentColor':  # alpha
    
                                ma_wrap.alpha_image_set(image)
    
                                if use_mapping:
                                    ma_wrap.alpha_mapping_set(**tex_map_kw)
    
                                if use_alpha_decals:
                                    material_decals.add(material)
    
                            elif lnk_type == b'DiffuseFactor':
                                pass  # TODO
                            elif lnk_type == b'ShininessExponent':
                                ma_wrap.hardness_image_set(image)
    
                                if use_mapping:
                                    ma_wrap.hardness_mapping_set(**tex_map_kw)
                            elif lnk_type == b'NormalMap' or lnk_type == b'Bump':  # XXX, applications abuse bump!
    
                                ma_wrap.normal_image_set(image)
                                ma_wrap.normal_factor_set(texture_bumpfac_get(fbx_obj))
    
                                if use_mapping:
                                    ma_wrap.normal_mapping_set(**tex_map_kw)
                                """
    
                            elif lnk_type == b'Bump':
                                ma_wrap.bump_image_set(image)
                                ma_wrap.bump_factor_set(texture_bumpfac_get(fbx_obj))
    
                                if use_mapping:
                                    ma_wrap.bump_mapping_set(**tex_map_kw)
                                """
    
                            else:
                                print("WARNING: material link %r ignored" % lnk_type)
    
                            material_images.setdefault(material, {})[lnk_type] = image
    
                    else:
                        if fbx_lnk_type.props[0] == b'OP':
                            lnk_type = fbx_lnk_type.props[3]
    
    
                            # tx/rot/scale (rot is ignored here!).
                            tex_map = texture_mapping_get(fbx_lnk)
    
                            mtex = material_mtex_new(material, image, tex_map)
    
    
                            if lnk_type == b'DiffuseColor':
                                mtex.use_map_color_diffuse = True
                                mtex.blend_type = 'MULTIPLY'
                            elif lnk_type == b'SpecularColor':
                                mtex.use_map_color_spec = True
                                mtex.blend_type = 'MULTIPLY'
                            elif lnk_type == b'ReflectionColor':
                                mtex.use_map_raymir = True
    
                            elif lnk_type == b'TransparentColor':  # alpha
                                material.use_transparency = True
                                material.transparency_method = 'RAYTRACE'
                                material.alpha = 0.0
                                mtex.use_map_alpha = True
                                mtex.alpha_factor = 1.0
                                if use_alpha_decals:
                                    material_decals.add(material)
    
                            elif lnk_type == b'DiffuseFactor':
                                mtex.use_map_diffuse = True
                            elif lnk_type == b'ShininessExponent':
                                mtex.use_map_hardness = True
    
                            elif lnk_type == b'NormalMap' or lnk_type == b'Bump':  # XXX, applications abuse bump!
    
                                mtex.texture.use_normal_map = True  # not ideal!
    
                                mtex.use_map_normal = True
                                mtex.normal_factor = texture_bumpfac_get(fbx_obj)
    
                            elif lnk_type == b'Bump':
                                mtex.use_map_normal = True
                                mtex.normal_factor = texture_bumpfac_get(fbx_obj)
    
                            else:
                                print("WARNING: material link %r ignored" % lnk_type)
    
    
                            material_images.setdefault(material, {})[lnk_type] = image
    
            # Check if the diffuse image has an alpha channel,
            # if so, use the alpha channel.
    
            # Note: this could be made optional since images may have alpha but be entirely opaque
            for fbx_uuid, fbx_item in fbx_table_nodes.items():
                fbx_obj, blen_data = fbx_item
                if fbx_obj.id != b'Material':
                    continue
                material = fbx_table_nodes[fbx_uuid][1]
                image = material_images.get(material, {}).get(b'DiffuseColor')
                # do we have alpha?
                if image and image.depth == 32:
                    if use_alpha_decals:
                        material_decals.add(material)
    
                    if use_cycles:
                        ma_wrap = cycles_material_wrap_map[material]
                        if ma_wrap.node_bsdf_alpha.mute:
                            ma_wrap.alpha_image_set_from_diffuse()
                    else:
                        if not any((True for mtex in material.texture_slots if mtex and mtex.use_map_alpha)):
                            mtex = material_mtex_new(material, image)
    
                            material.use_transparency = True
                            material.transparency_method = 'RAYTRACE'
                            material.alpha = 0.0
                            mtex.use_map_alpha = True
                            mtex.alpha_factor = 1.0
    
    
                # propagate mapping from diffuse to all other channels which have none defined.
                if use_cycles:
                    ma_wrap = cycles_material_wrap_map[material]
                    ma_wrap.mapping_set_from_diffuse()
    
    
        def _():
            # Annoying workaround for cycles having no z-offset
            if material_decals and use_alpha_decals:
                for fbx_uuid, fbx_item in fbx_table_nodes.items():
                    fbx_obj, blen_data = fbx_item
                    if fbx_obj.id != b'Geometry':
                        continue
                    if fbx_obj.props[-1] == b'Mesh':
                        mesh = fbx_item[1]
    
                        if decal_offset != 0.0:
                            for material in mesh.materials:
                                if material in material_decals:
                                    for v in mesh.vertices:
                                        v.co += v.normal * decal_offset
                                    break
    
                        if use_cycles:
                            for obj in (obj for obj in bpy.data.objects if obj.data == mesh):
                                obj.cycles_visibility.shadow = False
                        else:
                            for material in mesh.materials:
                                if material in material_decals:
                                    # recieve but dont cast shadows
                                    material.use_raytrace = False
        _(); del _
    
    
    Bastien Montagne's avatar
    Bastien Montagne committed
        print('Import finished in %.4f sec.' % (time.process_time() - start_time))