Skip to content
Snippets Groups Projects
export_fbx_bin.py 140 KiB
Newer Older
  • Learn to ignore specific revisions
  •         elem_props_template_set(astack_tmpl, astack_props, "p_timestamp", b"ReferenceStop", end)
            elem_props_template_finalize(astack_tmpl, astack_props)
    
            # For now, only one layer for all animations.
            alayer = elem_data_single_int64(root, b"AnimationLayer", get_fbxuid_from_key(alayer_key))
            alayer.add_string(fbx_name_class(name, b"AnimLayer"))
            alayer.add_string(b"")
    
            for obj, (alayer_key, acurvenodes) in alayers.items():
                # Animation layer.
                # alayer = elem_data_single_int64(root, b"AnimationLayer", get_fbxuid_from_key(alayer_key))
                # alayer.add_string(fbx_name_class(obj.name.encode(), b"AnimLayer"))
                # alayer.add_string(b"")
    
                for fbx_prop, (acurvenode_key, acurves, acurvenode_name) in acurvenodes.items():
                    # Animation curve node.
                    acurvenode = elem_data_single_int64(root, b"AnimationCurveNode", get_fbxuid_from_key(acurvenode_key))
                    acurvenode.add_string(fbx_name_class(acurvenode_name.encode(), b"AnimCurveNode"))
                    acurvenode.add_string(b"")
    
                    acn_tmpl = elem_props_template_init(scene_data.templates, b"AnimationCurveNode")
                    acn_props = elem_properties(acurvenode)
    
                    for fbx_item, (acurve_key, def_value, keys, _acurve_valid) in acurves.items():
                        elem_props_template_set(acn_tmpl, acn_props, "p_number", fbx_item.encode(), def_value, animatable=True)
    
                        # Only create Animation curve if needed!
                        if keys:
                            acurve = elem_data_single_int64(root, b"AnimationCurve", get_fbxuid_from_key(acurve_key))
                            acurve.add_string(fbx_name_class(b"", b"AnimCurve"))
                            acurve.add_string(b"")
    
                            # key attributes...
                            nbr_keys = len(keys)
                            # flags...
                            keyattr_flags = (
                                1 << 3 |   # interpolation mode, 1 = constant, 2 = linear, 3 = cubic.
                                1 << 8 |   # tangent mode, 8 = auto, 9 = TCB, 10 = user, 11 = generic break,
                                1 << 13 |  # tangent mode, 12 = generic clamp, 13 = generic time independent,
                                1 << 14 |  # tangent mode, 13 + 14 = generic clamp progressive.
                                0,
                            )
                            # Maybe values controlling TCB & co???
                            keyattr_datafloat = (0.0, 0.0, 9.419963346924634e-30, 0.0)
    
                            # And now, the *real* data!
                            elem_data_single_float64(acurve, b"Default", def_value)
                            elem_data_single_int32(acurve, b"KeyVer", FBX_ANIM_KEY_VERSION)
                            elem_data_single_int64_array(acurve, b"KeyTime", keys_to_ktimes(keys))
                            elem_data_single_float32_array(acurve, b"KeyValueFloat", (v for _f, v in keys))
                            elem_data_single_int32_array(acurve, b"KeyAttrFlags", keyattr_flags)
                            elem_data_single_float32_array(acurve, b"KeyAttrDataFloat", keyattr_datafloat)
                            elem_data_single_int32_array(acurve, b"KeyAttrRefCount", (nbr_keys,))
    
                    elem_props_template_finalize(acn_tmpl, acn_props)
    
    ##### Top-level FBX data container. #####
    
    # Helper container gathering some data we need multiple times:
    #     * templates.
    #     * objects.
    #     * connections.
    #     * takes.
    FBXData = namedtuple("FBXData", (
        "templates", "templates_users", "connections",
    
        "settings", "scene", "objects", "animations", "frame_start", "frame_end",
    
        "data_empties", "data_lamps", "data_cameras", "data_meshes", "mesh_mat_indices",
    
        "bones_to_posebones", "data_bones", "data_deformers",
    
        "data_world", "data_materials", "data_textures", "data_videos",
    ))
    
    
    def fbx_mat_properties_from_texture(tex):
        """
        Returns a set of FBX metarial properties that are affected by the given texture.
        Quite obviously, this is a fuzzy and far-from-perfect mapping! Amounts of influence are completely lost, e.g.
        Note tex is actually expected to be a texture slot.
        """
        # Tex influence does not exists in FBX, so assume influence < 0.5 = no influence... :/
        INFLUENCE_THRESHOLD = 0.5
    
        # Mapping Blender -> FBX (blend_use_name, blend_fact_name, fbx_name).
        blend_to_fbx = (
            # Lambert & Phong...
            ("diffuse", "diffuse", b"DiffuseFactor"),
            ("color_diffuse", "diffuse_color", b"DiffuseColor"),
            ("alpha", "alpha", b"TransparencyFactor"),
            ("diffuse", "diffuse", b"TransparentColor"),  # Uses diffuse color in Blender!
            ("emit", "emit", b"EmissiveFactor"),
            ("diffuse", "diffuse", b"EmissiveColor"),  # Uses diffuse color in Blender!
            ("ambient", "ambient", b"AmbientFactor"),
            #("", "", b"AmbientColor"),  # World stuff in Blender, for now ignore...
    
            ("normal", "normal", b"NormalMap"),
            # Note: unsure about those... :/
    
            #("", "", b"Bump"),
            #("", "", b"BumpFactor"),
            #("", "", b"DisplacementColor"),
            #("", "", b"DisplacementFactor"),
            # Phong only.
            ("specular", "specular", b"SpecularFactor"),
            ("color_spec", "specular_color", b"SpecularColor"),
            # See Material template about those two!
            ("hardness", "hardness", b"Shininess"),
            ("hardness", "hardness", b"ShininessExponent"),
            ("mirror", "mirror", b"ReflectionColor"),
            ("raymir", "raymir", b"ReflectionFactor"),
        )
    
        tex_fbx_props = set()
        for use_map_name, name_factor, fbx_prop_name in blend_to_fbx:
            if getattr(tex, "use_map_" + use_map_name) and getattr(tex, name_factor + "_factor") >= INFLUENCE_THRESHOLD:
                tex_fbx_props.add(fbx_prop_name)
    
        return tex_fbx_props
    
    
    
    def fbx_skeleton_from_armature(scene, settings, armature, objects, data_meshes, bones_to_posebones,
    
                                   data_bones, data_deformers, arm_parents):
    
        """
        Create skeleton from armature/bones (NodeAttribute/LimbNode and Model/LimbNode), and for each deformed mesh,
        create Pose/BindPose(with sub PoseNode) and Deformer/Skin(with Deformer/SubDeformer/Cluster).
        Also supports "parent to bone" (simple parent to Model/LimbNode).
        arm_parents is a set of tuples (armature, object) for all successful armature bindings.
        """
        arm = armature.data
    
        for bo, pbo in zip(arm.bones, armature.pose.bones):
    
            key, data_key = get_blender_bone_key(armature, bo)
            objects[bo] = key
    
            bones_to_posebones[bo] = pbo
    
            data_bones[bo] = (key, data_key, armature)
            bones[bo.name] = bo
    
    
        for obj in objects.keys():
    
            if not isinstance(obj, Object):
    
                continue
            if obj.type not in {'MESH'}:
                continue
            if obj.parent != armature:
                continue
    
            # Always handled by an Armature modifier...
            found = False
            for mod in obj.modifiers:
                if mod.type not in {'ARMATURE'}:
                    continue
                # We only support vertex groups binding method, not bone envelopes one!
                if mod.object == armature and mod.use_vertex_groups:
                    found = True
                    break
    
            if not found:
                continue
    
    
            # Note: bindpose have no relations at all (no connections), so no need for any preprocess for them.
            # Create skin & clusters relations (note skins are connected to geometry, *not* model!).
    
            clusters = OrderedDict((bo, get_blender_bone_cluster_key(armature, me, bo)) for bo in bones.values())
    
            data_deformers.setdefault(armature, OrderedDict())[me] = (get_blender_armature_skin_key(armature, me),
                                                                      obj, clusters)
    
    
            # We don't want a regular parent relationship for those in FBX...
            arm_parents.add((armature, obj))
    
    
    
    def fbx_animations_simplify(scene_data, animdata):
        """
        Simplifies FCurves!
        """
        fac = scene_data.settings.bake_anim_simplify_factor
        step = scene_data.settings.bake_anim_step
        # So that, with default factor and step values (1), we get:
        max_frame_diff = step * fac * 10  # max step of 10 frames.
        value_diff_fac = fac / 1000  # min value evolution: 0.1% of whole range.
    
        for obj, keys in animdata.items():
            if not keys:
                continue
            extremums = [(min(values), max(values)) for values in zip(*(k[1] for k in keys))]
            min_diffs = [max((mx - mn) * value_diff_fac, 0.000001) for mx, mn in extremums]
            p_currframe, p_key, p_key_write = keys[0]
            p_keyed = [(p_currframe - max_frame_diff, val) for val in p_key]
            for currframe, key, key_write in keys:
                for idx, (val, p_val) in enumerate(zip(key, p_key)):
                    p_keyedframe, p_keyedval = p_keyed[idx]
                    if val == p_val:
                        # Never write keyframe when value is exactly the same as prev one!
                        continue
                    if abs(val - p_val) >= min_diffs[idx]:
                        # If enough difference from previous sampled value, key this value *and* the previous one!
                        key_write[idx] = True
                        p_key_write[idx] = True
                        p_keyed[idx] = (currframe, val)
    
                    elif (abs(val - p_keyedval) >= min_diffs[idx]) or (currframe - p_keyedframe >= max_frame_diff):
    
                        # Else, if enough difference from previous keyed value (or max gap between keys is reached),
                        # key this value only!
                        key_write[idx] = True
                        p_keyed[idx] = (currframe, val)
                p_currframe, p_key, p_key_write = currframe, key, key_write
    
            # Always key last sampled values (we ignore curves with a single valid key anyway).
            p_key_write[:] = [True] * len(p_key_write)
    
    def fbx_animations_objects_do(scene_data, ref_id, f_start, f_end, start_zero, objects=None):
    
        Generate animation data (a single AnimStack) from objects, for a given frame range.
    
        if objects is not None:
            # Add bones!
            objects |= {bo for vo in objects if (isinstance(vo, bpy.types.Object) and vo.type == 'ARMATURE')
                           for bo in vo.data.bones}
        else:
            objects = scene_data.objects.keys()
    
        bake_step = scene_data.settings.bake_anim_step
        scene = scene_data.scene
    
        bone_map = scene_data.bones_to_posebones
    
    
        # FBX mapping info: Property affected, and name of the "sub" property (to distinguish e.g. vector's channels).
        fbx_names = (
    
            ("Lcl Translation", "T", "d|X"), ("Lcl Translation", "T", "d|Y"), ("Lcl Translation", "T", "d|Z"),
            ("Lcl Rotation", "R", "d|X"), ("Lcl Rotation", "R", "d|Y"), ("Lcl Rotation", "R", "d|Z"),
            ("Lcl Scaling", "S", "d|X"), ("Lcl Scaling", "S", "d|Y"), ("Lcl Scaling", "S", "d|Z"),
    
        )
    
        back_currframe = scene.frame_current
    
        animdata = OrderedDict((obj, []) for obj in objects)
    
            real_currframe = currframe - f_start if start_zero else currframe
    
            scene.frame_set(int(currframe), currframe - int(currframe))
    
                # Get PoseBone from bone...
                tobj = bone_map[obj] if isinstance(obj, Bone) else obj
    
                # We compute baked loc/rot/scale for all objects (rot being euler-compat with previous value!).
                p_rot = p_rots.get(tobj, None)
                loc, rot, scale, _m, _mr = fbx_object_tx(scene_data, tobj, p_rot)
                p_rots[tobj] = rot
    
                tx = tuple(loc) + tuple(units_convert_iter(rot, "radian", "degree")) + tuple(scale)
    
                animdata[obj].append((real_currframe, tx, [False] * len(tx)))
    
            currframe += bake_step
    
        scene.frame_set(back_currframe, 0.0)
    
        fbx_animations_simplify(scene_data, animdata)
    
    
    
        # And now, produce final data (usable by FBX export code)...
        for obj, keys in animdata.items():
            if not keys:
                continue
            curves = [[] for k in keys[0][1]]
            for currframe, key, key_write in keys:
                for idx, (val, wrt) in enumerate(zip(key, key_write)):
                    if wrt:
                        curves[idx].append((currframe, val))
    
    
            # Get PoseBone from bone...
    
            #tobj = bone_map[obj] if isinstance(obj, Bone) else obj
            #loc, rot, scale, _m, _mr = fbx_object_tx(scene_data, tobj)
            #tx = tuple(loc) + tuple(units_convert_iter(rot, "radian", "degree")) + tuple(scale)
            dtx = (0.0, 0.0, 0.0) + (0.0, 0.0, 0.0) + (1.0, 1.0, 1.0)
    
            # If animation for a channel, (True, keyframes), else (False, current value).
    
            for idx, c in enumerate(curves):
    
                fbx_group, fbx_gname, fbx_item = fbx_names[idx]
    
                fbx_item_key = get_blender_anim_curve_key(scene, ref_id, obj, fbx_group, fbx_item)
    
                if fbx_group not in final_keys:
    
                    fbx_group_key = get_blender_anim_curve_node_key(scene, ref_id, obj, fbx_group)
                    final_keys[fbx_group] = (fbx_group_key, OrderedDict(), fbx_gname)
    
                final_keys[fbx_group][1][fbx_item] = (fbx_item_key, dtx[idx], c, True if len(c) > 1 else False)
    
            # And now, remove anim groups (i.e. groups of curves affecting a single FBX property) with no curve at all!
            del_groups = []
    
            for grp, (_k, data, _n) in final_keys.items():
                if True in (d[3] for d in data.values()):
    
                    continue
                del_groups.append(grp)
            for grp in del_groups:
                del final_keys[grp]
    
            if final_keys:
    
                animations[obj] = (get_blender_anim_layer_key(scene, obj), final_keys)
    
        astack_key = get_blender_anim_stack_key(scene, ref_id)
        alayer_key = get_blender_anim_layer_key(scene, ref_id)
    
        name = (get_blenderID_name(ref_id) if ref_id else scene.name).encode()
    
        if start_zero:
            f_end -= f_start
            f_start = 0.0
    
    
        return (astack_key, animations, alayer_key, name, f_start, f_end) if animations else None
    
    
    def fbx_animations_objects(scene_data):
        """
        Generate global animation data from objects.
        """
        scene = scene_data.scene
        animations = []
    
        frame_start = 1e100
        frame_end = -1e100
    
        def add_anim(animations, anim):
            nonlocal frame_start, frame_end
            if anim is not None:
                animations.append(anim)
                f_start, f_end = anim[4:6]
                if f_start < frame_start:
                    frame_start = f_start
                if f_end > frame_end:
                    frame_end = f_end
    
    
        # Per-NLA strip animstacks.
        if scene_data.settings.bake_anim_use_nla_strips:
            strips = []
            for obj in scene_data.objects:
                # NLA tracks only for objects, not bones!
                if not isinstance(obj, Object) or not obj.animation_data:
                    continue
                for track in obj.animation_data.nla_tracks:
                    if track.mute:
                        continue
                    for strip in track.strips:
                        if strip.mute:
                            continue
                        strips.append(strip)
                        strip.mute = True
    
            for strip in strips:
                strip.mute = False
    
                add_anim(animations, fbx_animations_objects_do(scene_data, strip, strip.frame_start, strip.frame_end, True))
    
        # All actions.
        if scene_data.settings.bake_anim_use_all_actions:
            def validate_actions(act, path_resolve):
                for fc in act.fcurves:
                    data_path = fc.data_path
                    if fc.array_index:
                        data_path = data_path + "[%d]" % fc.array_index
                    try:
                        path_resolve(data_path)
                    except ValueError:
                        return False  # Invalid.
                return True  # Valid.
    
            def restore_object(obj_to, obj_from):
                # Restore org state of object (ugh :/ ).
                props = (
                    'location', 'rotation_quaternion', 'rotation_axis_angle', 'rotation_euler', 'rotation_mode', 'scale',
                    'delta_location', 'delta_rotation_euler', 'delta_rotation_quaternion', 'delta_scale',
                    'lock_location', 'lock_rotation', 'lock_rotation_w', 'lock_rotations_4d', 'lock_scale',
                    'tag', 'layers', 'select', 'track_axis', 'up_axis', 'active_material', 'active_material_index',
                    'matrix_parent_inverse', 'empty_draw_type', 'empty_draw_size', 'empty_image_offset', 'pass_index',
                    'color', 'hide', 'hide_select', 'hide_render', 'use_slow_parent', 'slow_parent_offset',
                    'use_extra_recalc_object', 'use_extra_recalc_data', 'dupli_type', 'use_dupli_frames_speed',
                    'use_dupli_vertices_rotation', 'use_dupli_faces_scale', 'dupli_faces_scale', 'dupli_group',
                    'dupli_frames_start', 'dupli_frames_end', 'dupli_frames_on', 'dupli_frames_off',
                    'draw_type', 'show_bounds', 'draw_bounds_type', 'show_name', 'show_axis', 'show_texture_space',
                    'show_wire', 'show_all_edges', 'show_transparent', 'show_x_ray',
                    'show_only_shape_key', 'use_shape_key_edit_mode', 'active_shape_key_index',
                )
                for p in props:
                    setattr(obj_to, p, getattr(obj_from, p))
    
            for obj in scene_data.objects:
                # Actions only for objects, not bones!
                if not isinstance(obj, Object):
                    continue
    
                # We can't play with animdata and actions and get back to org state easily.
                # So we have to add a temp copy of the object to the scene, animate it, and remove it... :/
                obj_copy = obj.copy()
    
                if obj.animation_data:
                    org_act = obj.animation_data.action
                else:
                    org_act = ...
                    obj.animation_data_create()
                path_resolve = obj.path_resolve
    
                for act in bpy.data.actions:
                    # For now, *all* paths in the action must be valid for the object, to validate the action.
                    # Unless that action was already assigned to the object!
                    if act != org_act and not validate_actions(act, path_resolve):
                        continue
                    obj.animation_data.action = act
                    frame_start, frame_end = act.frame_range  # sic!
                    add_anim(animations,
                             fbx_animations_objects_do(scene_data, (obj, act), frame_start, frame_end, True, {obj}))
                    # Ugly! :/
                    obj.animation_data.action = None if org_act is ... else org_act
                    restore_object(obj, obj_copy)
    
                if org_act is ...:
                    obj.animation_data_clear()
                else:
                    obj_copy.animation_data.action = org_act
    
    
        # Global (containing everything) animstack.
        if not scene_data.settings.bake_anim_use_nla_strips or not animations:
    
            add_anim(animations, fbx_animations_objects_do(scene_data, None, scene.frame_start, scene.frame_end, False))
    
    
        return animations, frame_start, frame_end
    
    def fbx_data_from_scene(scene, settings):
        """
        Do some pre-processing over scene's data...
        """
        objtypes = settings.object_types
    
    
        ##### Gathering data...
    
        # This is rather simple for now, maybe we could end generating templates with most-used values
        # instead of default ones?
    
        objects = OrderedDict((obj, get_blenderID_key(obj)) for obj in objects if obj.type in objtypes)
    
        data_lamps = OrderedDict((obj.data, get_blenderID_key(obj.data)) for obj in objects if obj.type == 'LAMP')
    
        # Unfortunately, FBX camera data contains object-level data (like position, orientation, etc.)...
    
        data_cameras = OrderedDict((obj, get_blenderID_key(obj.data)) for obj in objects if obj.type == 'CAMERA')
    
        # Yep! Contains nothing, but needed!
        data_empties = OrderedDict((obj, get_blender_empty_key(obj)) for obj in objects if obj.type == 'EMPTY')
    
        data_meshes = OrderedDict()
        for obj in objects:
            if obj.type not in BLENDER_OBJECT_TYPES_MESHLIKE:
                continue
    
            use_org_data = True
    
            if settings.use_mesh_modifiers or obj.type in BLENDER_OTHER_OBJECT_TYPES:
    
                use_org_data = False
    
                if obj.type == 'MESH':
                    # No need to create a new mesh in this case, if no modifier is active!
                    use_org_data = True
    
                        # For meshes, when armature export is enabled, disable Armature modifiers here!
                        if mod.type == 'ARMATURE' and 'ARMATURE' in settings.object_types:
    
                            tmp_mods.append((mod, mod.show_render))
                            mod.show_render = False
    
                        if mod.show_render:
                            use_org_data = False
                if not use_org_data:
                    tmp_me = obj.to_mesh(scene, apply_modifiers=True, settings='RENDER')
                    data_meshes[obj] = (get_blenderID_key(tmp_me), tmp_me, True)
    
                # Re-enable temporary disabled modifiers.
                for mod, show_render in tmp_mods:
                    mod.show_render = show_render
    
                data_meshes[obj] = (get_blenderID_key(obj.data), obj.data, False)
    
    
        data_bones = OrderedDict()
        data_deformers = OrderedDict()
    
        bones_to_posebones = dict()
    
        arm_parents = set()
        for obj in tuple(objects.keys()):
            if obj.type not in {'ARMATURE'}:
                continue
    
            fbx_skeleton_from_armature(scene, settings, obj, objects, data_meshes, bones_to_posebones,
    
                                       data_bones, data_deformers, arm_parents)
    
    
        # Some world settings are embedded in FBX materials...
        if scene.world:
    
            data_world = OrderedDict(((scene.world, get_blenderID_key(scene.world)),))
    
    
        # TODO: Check all the mat stuff works even when mats are linked to Objects
        #       (we can then have the same mesh used with different materials...).
        #       *Should* work, as FBX always links its materials to Models (i.e. objects).
        #       XXX However, material indices would probably break...
    
        for obj in objects:
            # Only meshes for now!
    
            if not isinstance(obj, Object) or obj.type not in BLENDER_OBJECT_TYPES_MESHLIKE:
    
                continue
            for mat_s in obj.material_slots:
                mat = mat_s.material
                # Note theoretically, FBX supports any kind of materials, even GLSL shaders etc.
                # However, I doubt anything else than Lambert/Phong is really portable!
    
                # We support any kind of 'surface' shader though, better to have some kind of default Lambert than nothing.
    
                # TODO: Support nodes (*BIG* todo!).
    
                if mat.type in {'SURFACE'} and not mat.use_nodes:
    
                    if mat in data_materials:
                        data_materials[mat][1].append(obj)
                    else:
                        data_materials[mat] = (get_blenderID_key(mat), [obj])
    
        # Note FBX textures also hold their mapping info.
        # TODO: Support layers?
    
        # FbxVideo also used to store static images...
    
        # For now, do not use world textures, don't think they can be linked to anything FBX wise...
        for mat in data_materials.keys():
            for tex in mat.texture_slots:
                if tex is None:
                    continue
                # For now, only consider image textures.
                # Note FBX does has support for procedural, but this is not portable at all (opaque blob),
                # so not useful for us.
                # TODO I think ENVIRONMENT_MAP should be usable in FBX as well, but for now let it aside.
                #if tex.texture.type not in {'IMAGE', 'ENVIRONMENT_MAP'}:
                if tex.texture.type not in {'IMAGE'}:
                    continue
                img = tex.texture.image
                if img is None:
                    continue
                # Find out whether we can actually use this texture for this material, in FBX context.
                tex_fbx_props = fbx_mat_properties_from_texture(tex)
                if not tex_fbx_props:
                    continue
                if tex in data_textures:
                    data_textures[tex][1][mat] = tex_fbx_props
                else:
    
                    data_textures[tex] = (get_blenderID_key(tex), OrderedDict(((mat, tex_fbx_props),)))
    
                if img in data_videos:
                    data_videos[img][1].append(tex)
                else:
                    data_videos[img] = (get_blenderID_key(img), [tex])
    
    
        frame_start = scene.frame_start
        frame_end = scene.frame_end
    
        if settings.bake_anim:
            # From objects & bones only for a start.
            tmp_scdata = FBXData(  # Kind of hack, we need a temp scene_data for object's space handling to bake animations...
                None, None, None,
    
                settings, scene, objects, None, 0.0, 0.0,
    
                data_empties, data_lamps, data_cameras, data_meshes, None,
                bones_to_posebones, data_bones, data_deformers,
                data_world, data_materials, data_textures, data_videos,
            )
    
            animations, frame_start, frame_end = fbx_animations_objects(tmp_scdata)
    
        ##### Creation of templates...
    
        templates = OrderedDict()
        templates[b"GlobalSettings"] = fbx_template_def_globalsettings(scene, settings, nbr_users=1)
    
    
        if data_empties:
            templates[b"Null"] = fbx_template_def_null(scene, settings, nbr_users=len(data_empties))
    
    
        if data_lamps:
            templates[b"Light"] = fbx_template_def_light(scene, settings, nbr_users=len(data_lamps))
    
        if data_cameras:
            templates[b"Camera"] = fbx_template_def_camera(scene, settings, nbr_users=len(data_cameras))
    
        if data_bones:
            templates[b"Bone"] = fbx_template_def_bone(scene, settings, nbr_users=len(data_bones))
    
        if data_meshes:
            templates[b"Geometry"] = fbx_template_def_geometry(scene, settings, nbr_users=len(data_meshes))
    
        if objects:
            templates[b"Model"] = fbx_template_def_model(scene, settings, nbr_users=len(objects))
    
        if arm_parents:
            # Number of Pose|BindPose elements should be the same as number of meshes-parented-to-armatures
            templates[b"BindPose"] = fbx_template_def_pose(scene, settings, nbr_users=len(arm_parents))
    
        if data_deformers:
    
    Bastien Montagne's avatar
    Bastien Montagne committed
            nbr = len(data_deformers)
            nbr += sum(len(clusters) for def_me in data_deformers.values() for a, b, clusters in def_me.values())
    
            templates[b"Deformers"] = fbx_template_def_deformer(scene, settings, nbr_users=nbr)
    
        # No world support in FBX...
        """
        if data_world:
            templates[b"World"] = fbx_template_def_world(scene, settings, nbr_users=len(data_world))
        """
    
        if data_materials:
            templates[b"Material"] = fbx_template_def_material(scene, settings, nbr_users=len(data_materials))
    
        if data_textures:
            templates[b"TextureFile"] = fbx_template_def_texture_file(scene, settings, nbr_users=len(data_textures))
    
        if data_videos:
            templates[b"Video"] = fbx_template_def_video(scene, settings, nbr_users=len(data_videos))
    
    
            nbr_astacks = len(animations)
            nbr_acnodes = 0
            nbr_acurves = 0
            for _astack_key, astack, _al, _n, _fs, _fe in animations:
                for _alayer_key, alayer in astack.values():
                    for _acnode_key, acnode, _acnode_name in alayer.values():
                        nbr_acnodes += 1
                        for _acurve_key, _dval, acurve, acurve_valid in acnode.values():
                            if acurve:
                                nbr_acurves += 1
    
            templates[b"AnimationStack"] = fbx_template_def_animstack(scene, settings, nbr_users=nbr_astacks)
    
            # Would be nice to have one layer per animated object, but this seems tricky and not that well supported.
    
            # So for now, only one layer per anim stack.
            templates[b"AnimationLayer"] = fbx_template_def_animlayer(scene, settings, nbr_users=nbr_astacks)
            templates[b"AnimationCurveNode"] = fbx_template_def_animcurvenode(scene, settings, nbr_users=nbr_acnodes)
            templates[b"AnimationCurve"] = fbx_template_def_animcurve(scene, settings, nbr_users=nbr_acurves)
    
        templates_users = sum(tmpl.nbr_users for tmpl in templates.values())
    
        ##### Creation of connections...
    
        connections = []
    
        # Objects (with classical parenting).
        for obj, obj_key in objects.items():
            # Bones are handled later.
    
            if isinstance(obj, Object):
    
                par = obj.parent
                par_key = 0  # Convention, "root" node (never explicitly written).
                if par and par in objects:
                    par_type = obj.parent_type
                    if par_type in {'OBJECT', 'BONE'}:
                        # Meshes parented to armature also have 'OBJECT' par_type, in FBX this is handled separately,
                        # we do not want an extra object parenting!
                        if (par, obj) not in arm_parents:
                            par_key = objects[par]
                    else:
                        print("Sorry, “{}” parenting type is not supported".format(par_type))
                connections.append((b"OO", get_fbxuid_from_key(obj_key), get_fbxuid_from_key(par_key), None))
    
        # Armature & Bone chains.
        for bo, (bo_key, _bo_data_key, arm) in data_bones.items():
            par = bo.parent
            if not par:  # Root bone.
                par = arm
            if par not in objects:
                continue
            connections.append((b"OO", get_fbxuid_from_key(bo_key), get_fbxuid_from_key(objects[par]), None))
    
    
        # Empties
        for empty_obj, empty_key in data_empties.items():
            empty_obj_key = objects[empty_obj]
            connections.append((b"OO", get_fbxuid_from_key(empty_key), get_fbxuid_from_key(empty_obj_key), None))
    
    
        # Cameras
        for obj_cam, cam_key in data_cameras.items():
            cam_obj_key = objects[obj_cam]
            connections.append((b"OO", get_fbxuid_from_key(cam_key), get_fbxuid_from_key(cam_obj_key), None))
    
        # Object data.
        for obj, obj_key in objects.items():
    
            if isinstance(obj, Bone):
    
                _bo_key, bo_data_key, _arm = data_bones[obj]
                assert(_bo_key == obj_key)
                connections.append((b"OO", get_fbxuid_from_key(bo_data_key), get_fbxuid_from_key(obj_key), None))
            elif obj.type == 'LAMP':
                lamp_key = data_lamps[obj.data]
                connections.append((b"OO", get_fbxuid_from_key(lamp_key), get_fbxuid_from_key(obj_key), None))
    
            elif obj.type in BLENDER_OBJECT_TYPES_MESHLIKE:
                mesh_key, _me, _free = data_meshes[obj]
    
                connections.append((b"OO", get_fbxuid_from_key(mesh_key), get_fbxuid_from_key(obj_key), None))
    
        # Deformers (armature-to-geometry, only for meshes currently)...
        for arm, deformed_meshes in data_deformers.items():
    
            for me, (skin_key, obj, clusters) in deformed_meshes.items():
    
                mesh_key, _me, _free = data_meshes[obj]
                assert(me == _me)
    
                connections.append((b"OO", get_fbxuid_from_key(skin_key), get_fbxuid_from_key(mesh_key), None))
    
                for bo, clstr_key in clusters.items():
                    # cluster -> skin
                    connections.append((b"OO", get_fbxuid_from_key(clstr_key), get_fbxuid_from_key(skin_key), None))
                    # bone -> cluster
                    connections.append((b"OO", get_fbxuid_from_key(objects[bo]), get_fbxuid_from_key(clstr_key), None))
    
        # Materials
    
        _objs_indices = {}
        for mat, (mat_key, objs) in data_materials.items():
            for obj in objs:
                obj_key = objects[obj]
                connections.append((b"OO", get_fbxuid_from_key(mat_key), get_fbxuid_from_key(obj_key), None))
                # Get index of this mat for this object.
                # Mat indices for mesh faces are determined by their order in 'mat to ob' connections.
                # Only mats for meshes currently...
                me = obj.data
                idx = _objs_indices[obj] = _objs_indices.get(obj, -1) + 1
    
                mesh_mat_indices.setdefault(me, OrderedDict())[mat] = idx
    
        del _objs_indices
    
        # Textures
        for tex, (tex_key, mats) in data_textures.items():
            for mat, fbx_mat_props in mats.items():
                mat_key, _objs = data_materials[mat]
                for fbx_prop in fbx_mat_props:
                    # texture -> material properties
                    connections.append((b"OP", get_fbxuid_from_key(tex_key), get_fbxuid_from_key(mat_key), fbx_prop))
    
        # Images
        for vid, (vid_key, texs) in data_videos.items():
            for tex in texs:
                tex_key, _texs = data_textures[tex]
                connections.append((b"OO", get_fbxuid_from_key(vid_key), get_fbxuid_from_key(tex_key), None))
    
    
        for astack_key, astack, alayer_key, _name, _fstart, _fend in animations:
    
            # Animstack itself is linked nowhere!
    
            connections.append((b"OO", alayer_id, astack_id, None))
    
                obj_id = get_fbxuid_from_key(objects[obj])
                # Animlayer -> animstack.
    
                # alayer_id = get_fbxuid_from_key(alayer_key)
                # connections.append((b"OO", alayer_id, astack_id, None))
    
                for fbx_prop, (acurvenode_key, acurves, acurvenode_name) in acurvenodes.items():
    
                    # Animcurvenode -> animalayer.
                    acurvenode_id = get_fbxuid_from_key(acurvenode_key)
                    connections.append((b"OO", acurvenode_id, alayer_id, None))
                    # Animcurvenode -> object property.
    
                    connections.append((b"OP", acurvenode_id, obj_id, fbx_prop.encode()))
    
                    for fbx_item, (acurve_key, dafault_value, acurve, acurve_valid) in acurves.items():
    
                        if acurve:
                            # Animcurve -> Animcurvenode.
                            connections.append((b"OP", get_fbxuid_from_key(acurve_key), acurvenode_id, fbx_item.encode()))
    
    
        ##### And pack all this!
    
        return FBXData(
            templates, templates_users, connections,
    
            settings, scene, objects, animations, frame_start, frame_end,
    
            data_empties, data_lamps, data_cameras, data_meshes, mesh_mat_indices,
    
            bones_to_posebones, data_bones, data_deformers,
    
            data_world, data_materials, data_textures, data_videos,
        )
    
    
    
    def fbx_scene_data_cleanup(scene_data):
        """
        Some final cleanup...
        """
        # Delete temp meshes.
        for _key, me, free in scene_data.data_meshes.values():
            if free:
                bpy.data.meshes.remove(me)
    
    
    
    ##### Top-level FBX elements generators. #####
    
    def fbx_header_elements(root, scene_data, time=None):
        """
        Write boiling code of FBX root.
        time is expected to be a datetime.datetime object, or None (using now() in this case).
        """
        ##### Start of FBXHeaderExtension element.
        header_ext = elem_empty(root, b"FBXHeaderExtension")
    
        elem_data_single_int32(header_ext, b"FBXHeaderVersion", FBX_HEADER_VERSION)
    
        elem_data_single_int32(header_ext, b"FBXVersion", FBX_VERSION)
    
        # No encryption!
        elem_data_single_int32(header_ext, b"EncryptionType", 0)
    
        if time is None:
            time = datetime.datetime.now()
        elem = elem_empty(header_ext, b"CreationTimeStamp")
        elem_data_single_int32(elem, b"Version", 1000)
        elem_data_single_int32(elem, b"Year", time.year)
        elem_data_single_int32(elem, b"Month", time.month)
        elem_data_single_int32(elem, b"Day", time.day)
        elem_data_single_int32(elem, b"Hour", time.hour)
        elem_data_single_int32(elem, b"Minute", time.minute)
        elem_data_single_int32(elem, b"Second", time.second)
        elem_data_single_int32(elem, b"Millisecond", time.microsecond // 1000)
    
        elem_data_single_string_unicode(header_ext, b"Creator", "Blender version %s" % bpy.app.version_string)
    
        # 'SceneInfo' seems mandatory to get a valid FBX file...
        # TODO use real values!
        # XXX Should we use scene.name.encode() here?
        scene_info = elem_data_single_string(header_ext, b"SceneInfo", fbx_name_class(b"GlobalInfo", b"SceneInfo"))
        scene_info.add_string(b"UserData")
        elem_data_single_string(scene_info, b"Type", b"UserData")
        elem_data_single_int32(scene_info, b"Version", FBX_SCENEINFO_VERSION)
        meta_data = elem_empty(scene_info, b"MetaData")
        elem_data_single_int32(meta_data, b"Version", FBX_SCENEINFO_VERSION)
        elem_data_single_string(meta_data, b"Title", b"")
        elem_data_single_string(meta_data, b"Subject", b"")
        elem_data_single_string(meta_data, b"Author", b"")
        elem_data_single_string(meta_data, b"Keywords", b"")
        elem_data_single_string(meta_data, b"Revision", b"")
        elem_data_single_string(meta_data, b"Comment", b"")
    
        props = elem_properties(scene_info)
        elem_props_set(props, "p_string_url", b"DocumentUrl", "/foobar.fbx")
        elem_props_set(props, "p_string_url", b"SrcDocumentUrl", "/foobar.fbx")
        original = elem_props_compound(props, b"Original")
        original("p_string", b"ApplicationVendor", "Blender Foundation")
        original("p_string", b"ApplicationName", "Blender")
        original("p_string", b"ApplicationVersion", "2.70")
        original("p_datetime", b"DateTime_GMT", "01/01/1970 00:00:00.000")
        original("p_string", b"FileName", "/foobar.fbx")
        lastsaved = elem_props_compound(props, b"LastSaved")
        lastsaved("p_string", b"ApplicationVendor", "Blender Foundation")
        lastsaved("p_string", b"ApplicationName", "Blender")
        lastsaved("p_string", b"ApplicationVersion", "2.70")
        lastsaved("p_datetime", b"DateTime_GMT", "01/01/1970 00:00:00.000")
    
        ##### End of FBXHeaderExtension element.
    
        # FileID is replaced by dummy value currently...
        elem_data_single_bytes(root, b"FileId", b"FooBar")
    
        # CreationTime is replaced by dummy value currently, but anyway...
        elem_data_single_string_unicode(root, b"CreationTime",
                                        "{:04}-{:02}-{:02} {:02}:{:02}:{:02}:{:03}"
                                        "".format(time.year, time.month, time.day, time.hour, time.minute, time.second,
                                                  time.microsecond * 1000))
    
        elem_data_single_string_unicode(root, b"Creator", "Blender version %s" % bpy.app.version_string)
    
        ##### Start of GlobalSettings element.
        global_settings = elem_empty(root, b"GlobalSettings")
    
        elem_data_single_int32(global_settings, b"Version", 1000)
    
        props = elem_properties(global_settings)
        up_axis, front_axis, coord_axis = RIGHT_HAND_AXES[scene_data.settings.to_axes]
        elem_props_set(props, "p_integer", b"UpAxis", up_axis[0])
        elem_props_set(props, "p_integer", b"UpAxisSign", up_axis[1])
        elem_props_set(props, "p_integer", b"FrontAxis", front_axis[0])
        elem_props_set(props, "p_integer", b"FrontAxisSign", front_axis[1])
        elem_props_set(props, "p_integer", b"CoordAxis", coord_axis[0])
        elem_props_set(props, "p_integer", b"CoordAxisSign", coord_axis[1])
    
        elem_props_set(props, "p_integer", b"OriginalUpAxis", -1)
        elem_props_set(props, "p_integer", b"OriginalUpAxisSign", 1)
    
        elem_props_set(props, "p_double", b"UnitScaleFactor", 1.0)
    
        elem_props_set(props, "p_double", b"OriginalUnitScaleFactor", 1.0)
    
        elem_props_set(props, "p_color_rgb", b"AmbientColor", (0.0, 0.0, 0.0))
        elem_props_set(props, "p_string", b"DefaultCamera", "Producer Perspective")
    
    
        # Global timing data.
        r = scene_data.scene.render
        fps = r.fps / r.fps_base
    
        fbx_fps, fbx_fps_mode = FBX_FRAMERATES[0]  # Custom framerate.
        for ref_fps, fps_mode in FBX_FRAMERATES:
            if similar_values(fps, ref_fps):
                fbx_fps = ref_fps
                fbx_fps_mode = fps_mode
        elem_props_set(props, "p_enum", b"TimeMode", fbx_fps_mode)
    
        elem_props_set(props, "p_timestamp", b"TimeSpanStart", 0)
        elem_props_set(props, "p_timestamp", b"TimeSpanStop", FBX_KTIME)
    
        elem_props_set(props, "p_double", b"CustomFrameRate", fbx_fps)
    
    
        ##### End of GlobalSettings element.
    
    
    def fbx_documents_elements(root, scene_data):
        """
        Write 'Document' part of FBX root.
        Seems like FBX support multiple documents, but until I find examples of such, we'll stick to single doc!
        time is expected to be a datetime.datetime object, or None (using now() in this case).
        """
        name = scene_data.scene.name
    
        ##### Start of Documents element.
        docs = elem_empty(root, b"Documents")
    
        elem_data_single_int32(docs, b"Count", 1)
    
        doc_uid = get_fbxuid_from_key("__FBX_Document__" + name)
        doc = elem_data_single_int64(docs, b"Document", doc_uid)
    
        doc.add_string_unicode(name)
    
        props = elem_properties(doc)
        elem_props_set(props, "p_object", b"SourceObject")
        elem_props_set(props, "p_string", b"ActiveAnimStackName", "")
    
        # XXX Some kind of ID? Offset?
        #     Anyway, as long as we have only one doc, probably not an issue.
        elem_data_single_int64(doc, b"RootNode", 0)
    
    
    def fbx_references_elements(root, scene_data):
        """
        Have no idea what references are in FBX currently... Just writing empty element.
        """
        docs = elem_empty(root, b"References")
    
    
    def fbx_definitions_elements(root, scene_data):
        """
        Templates definitions. Only used by Objects data afaik (apart from dummy GlobalSettings one).
        """
        definitions = elem_empty(root, b"Definitions")
    
        elem_data_single_int32(definitions, b"Version", FBX_TEMPLATES_VERSION)
        elem_data_single_int32(definitions, b"Count", scene_data.templates_users)
    
        fbx_templates_generate(definitions, scene_data.templates)
    
    
    def fbx_objects_elements(root, scene_data):
        """
        Data (objects, geometry, material, textures, armatures, etc.
        """
        objects = elem_empty(root, b"Objects")
    
    
        for empty in scene_data.data_empties.keys():
            fbx_data_empty_elements(objects, empty, scene_data)
    
    
        for lamp in scene_data.data_lamps.keys():
            fbx_data_lamp_elements(objects, lamp, scene_data)
    
        for cam in scene_data.data_cameras.keys():
            fbx_data_camera_elements(objects, cam, scene_data)
    
    
        done_meshes = set()
    
        for me_obj in scene_data.data_meshes.keys():
    
            fbx_data_mesh_elements(objects, me_obj, scene_data, done_meshes)
        del done_meshes
    
    
        for obj in scene_data.objects.keys():
            fbx_data_object_elements(objects, obj, scene_data)
    
        for obj in scene_data.objects.keys():
    
            if not isinstance(obj, Object) or obj.type not in {'ARMATURE'}:
    
                continue
            fbx_data_armature_elements(objects, obj, scene_data)
    
        for mat in scene_data.data_materials.keys():
            fbx_data_material_elements(objects, mat, scene_data)
    
        for tex in scene_data.data_textures.keys():
            fbx_data_texture_file_elements(objects, tex, scene_data)
    
        for vid in scene_data.data_videos.keys():
            fbx_data_video_elements(objects, vid, scene_data)
    
    
        fbx_data_animation_elements(objects, scene_data)
    
    
    
    def fbx_connections_elements(root, scene_data):
        """
        Relations between Objects (which material uses which texture, and so on).
        """
        connections = elem_empty(root, b"Connections")
    
        for c in scene_data.connections:
            elem_connection(connections, *c)
    
    
    def fbx_takes_elements(root, scene_data):
        """
    
        # XXX Pretty sure takes are no more needed...
    
        takes = elem_empty(root, b"Takes")
        elem_data_single_string(takes, b"Current", b"")
    
    
        animations = scene_data.animations
    
        for astack_key, animations, alayer_key, name, f_start, f_end in animations:
            scene = scene_data.scene
            fps = scene.render.fps / scene.render.fps_base
            start_ktime = int(units_convert(f_start / fps, "second", "ktime"))
            end_ktime = int(units_convert(f_end / fps, "second", "ktime"))  # +1 is unity hack...
    
            take = elem_data_single_string(takes, b"Take", name)
            elem_data_single_string(take, b"FileName", name + b".tak")
            take_loc_time = elem_data_single_int64(take, b"LocalTime", start_ktime)
            take_loc_time.add_int64(end_ktime)
            take_ref_time = elem_data_single_int64(take, b"ReferenceTime", start_ktime)
            take_ref_time.add_int64(end_ktime)
    
    
    ##### "Main" functions. #####
    FBXSettingsMedia = namedtuple("FBXSettingsMedia", (
        "path_mode", "base_src", "base_dst", "subdir",
        "embed_textures", "copy_set",
    ))
    FBXSettings = namedtuple("FBXSettings", (
    
        "report", "to_axes", "global_matrix", "global_scale",
    
        "bake_space_transform", "global_matrix_inv", "global_matrix_inv_transposed",
    
        "context_objects", "object_types", "use_mesh_modifiers",
    
        "mesh_smooth_type", "use_mesh_edges", "use_tspace", "use_armature_deform_only",
    
        "bake_anim", "bake_anim_use_nla_strips", "bake_anim_use_all_actions", "bake_anim_step", "bake_anim_simplify_factor",
    
        "use_metadata", "media_settings", "use_custom_properties",
    ))