Skip to content
Snippets Groups Projects
export_fbx_bin.py 149 KiB
Newer Older
  • Learn to ignore specific revisions
  •     return FBXTemplate(b"AnimationCurveNode", b"FbxAnimCurveNode", props, nbr_users, [False])
    
    
    
    def fbx_template_def_animcurve(scene, settings, override_defaults=None, nbr_users=0):
        props = OrderedDict()
        if override_defaults is not None:
            props.update(override_defaults)
    
        return FBXTemplate(b"AnimationCurve", b"", props, nbr_users, [False])
    
    ##### FBX objects generators. #####
    
    # FBX Model-like data (i.e. Blender objects, dupliobjects and bones) are wrapped in ObjectWrapper.
    # This allows us to have a (nearly) same code FBX-wise for all those types.
    # The wrapper tries to stay as small as possible, by mostly using callbacks (property(get...))
    # to actual Blender data it contains.
    # Note it caches its instances, so that you may call several times ObjectWrapper(your_object)
    # with a minimal cost (just re-computing the key).
    
    class MetaObjectWrapper(type):
        def __call__(cls, bdata, armature=None):
            if bdata is None:
                return None
            dup_mat = None
            if isinstance(bdata, Object):
                key = get_blenderID_key(bdata)
            elif isinstance(bdata, DupliObject):
                key = "|".join((get_blenderID_key((bdata.id_data, bdata.object)), cls._get_dup_num_id(bdata)))
                dup_mat = bdata.matrix.copy()
            else:  # isinstance(bdata, (Bone, PoseBone)):
                if isinstance(bdata, PoseBone):
                    bdata = armature.data.bones[bdata.name]
                key = get_blenderID_key((armature, bdata))
    
            cache = getattr(cls, "_cache", None)
            if cache is None:
                cache = cls._cache = {}
            if key in cache:
                instance = cache[key]
                # Duplis hack: since duplis are not persistent in Blender (we have to re-create them to get updated
                # info like matrix...), we *always* need to reset that matrix when calling ObjectWrapper() (all
                # other data is supposed valid during whole cache live, so we can skip resetting it).
                instance._dupli_matrix = dup_mat
                return instance
    
            instance = cls.__new__(cls, bdata, armature)
            instance.__init__(bdata, armature)
            instance.key = key
            instance._dupli_matrix = dup_mat
            cache[key] = instance
            return instance
    
    
    class ObjectWrapper(metaclass=MetaObjectWrapper):
        """
        This class provides a same common interface for all (FBX-wise) object-like elements:
        * Blender Object
        * Blender Bone and PoseBone
        * Blender DupliObject
        Note since a same Blender object might be 'mapped' to several FBX models (esp. with duplis),
        we need to use a key to identify each.
        """
        __slots__ = ('name', 'key', 'bdata', '_tag', '_ref', '_dupli_matrix')
    
        @classmethod
        def cache_clear(cls):
            if hasattr(cls, "_cache"):
                del cls._cache
    
        @staticmethod
        def _get_dup_num_id(bdata):
            return ".".join(str(i) for i in bdata.persistent_id if i != 2147483647)
    
        def __init__(self, bdata, armature=None):
            """
            bdata might be an Object, DupliObject, Bone or PoseBone.
            If Bone or PoseBone, armature Object must be provided.
            """
            if isinstance(bdata, Object):
                self._tag = 'OB'
                self.name = get_blenderID_name(bdata)
                self.bdata = bdata
                self._ref = None
            elif isinstance(bdata, DupliObject):
                self._tag = 'DP'
                self.name = "|".join((get_blenderID_name((bdata.id_data, bdata.object)),
                                      "Dupli", self._get_dup_num_id(bdata)))
                self.bdata = bdata.object
                self._ref = bdata.id_data
            else:  # isinstance(bdata, (Bone, PoseBone)):
                if isinstance(bdata, PoseBone):
                    bdata = armature.data.bones[bdata.name]
                self._tag = 'BO'
                self.name = get_blenderID_name((armature, bdata))
                self.bdata = bdata
                self._ref = armature
    
        def __eq__(self, other):
            return isinstance(other, self.__class__) and self.key == other.key
    
        def __hash__(self):
            return hash(self.key)
    
        #### Common to all _tag values.
        def get_fbx_uuid(self):
            return get_fbxuid_from_key(self.key)
        fbx_uuid = property(get_fbx_uuid)
    
        def get_parent(self):
            if self._tag == 'OB':
                return ObjectWrapper(self.bdata.parent)
            elif self._tag == 'DP':
                return ObjectWrapper(self.bdata.parent or self._ref)
            else:  # self._tag == 'BO'
                return ObjectWrapper(self.bdata.parent, self._ref) or ObjectWrapper(self._ref)
        parent = property(get_parent)
    
        def get_matrix_local(self):
            if self._tag == 'OB':
                return self.bdata.matrix_local.copy()
            elif self._tag == 'DP':
                return self._ref.matrix_world.inverted() * self._dupli_matrix
            else:  # 'BO', current pose
                # PoseBone.matrix is in armature space, bring in back in real local one!
                par = self.bdata.parent
                par_mat_inv = self._ref.pose.bones[par.name].matrix.inverted() if par else Matrix()
                return par_mat_inv * self._ref.pose.bones[self.bdata.name].matrix
        matrix_local = property(get_matrix_local)
    
        def get_matrix_global(self):
            if self._tag == 'OB':
                return self.bdata.matrix_world.copy()
            elif self._tag == 'DP':
                return self._dupli_matrix
            else:  # 'BO', current pose
                return self._ref.matrix_world * self._ref.pose.bones[self.bdata.name].matrix
        matrix_global = property(get_matrix_global)
    
        def get_matrix_rest_local(self):
            if self._tag == 'BO':
                # Bone.matrix_local is in armature space, bring in back in real local one!
                par = self.bdata.parent
                par_mat_inv = par.matrix_local.inverted() if par else Matrix()
                return par_mat_inv * self.bdata.matrix_local
            else:
                return self.matrix_local
        matrix_rest_local = property(get_matrix_rest_local)
    
        def get_matrix_rest_global(self):
            if self._tag == 'BO':
                return self._ref.matrix_world * self.bdata.matrix_local
            else:
                return self.matrix_global
        matrix_rest_global = property(get_matrix_rest_global)
    
        #### Transform and helpers
        def has_valid_parent(self, objects):
            par = self.parent
            if par in objects:
                if self._tag == 'OB':
                    par_type = self.bdata.parent_type
                    if par_type in {'OBJECT', 'BONE'}:
                        return True
                    else:
                        print("Sorry, “{}” parenting type is not supported".format(par_type))
                        return False
                return True
            return False
    
        def use_bake_space_transform(self, scene_data):
            # NOTE: Only applies to object types supporting this!!! Currently, only meshes...
            #       Also, do not apply it to children objects.
            # TODO: Check whether this can work for bones too...
            return (scene_data.settings.bake_space_transform and self._tag == 'OB' and
                    self.bdata.type in BLENDER_OBJECT_TYPES_MESHLIKE and not self.has_valid_parent(scene_data.objects))
    
        def fbx_object_matrix(self, scene_data, rest=False, local_space=False, global_space=False):
            """
            Generate object transform matrix (*always* in matching *FBX* space!).
            If local_space is True, returned matrix is *always* in local space.
            Else if global_space is True, returned matrix is always in world space.
            If both local_space and global_space are False, returned matrix is in parent space if parent is valid,
            else in world space.
            Note local_space has precedence over global_space.
            If rest is True and object is a Bone, returns matching rest pose transform instead of current pose one.
            Applies specific rotation to bones, lamps and cameras (conversion Blender -> FBX).
            """
            # Objects which are not bones and do not have any parent are *always* in global space
            # (unless local_space is True!).
            is_global = (not local_space and
                         (global_space or not (self._tag in {'DP', 'BO'} or self.has_valid_parent(scene_data.objects))))
    
            if self._tag == 'BO':
                if rest:
                    matrix = self.matrix_rest_global if is_global else self.matrix_rest_local
                else:  # Current pose.
                    matrix = self.matrix_global if is_global else self.matrix_local
            else:
                # Since we have to apply corrections to some types of object, we always need local Blender space here...
                matrix = self.matrix_local
                parent = self.parent
    
                # Lamps and cameras need to be rotated (in local space!).
                if self.bdata.type == 'LAMP':
                    matrix = matrix * MAT_CONVERT_LAMP
                elif self.bdata.type == 'CAMERA':
                    matrix = matrix * MAT_CONVERT_CAMERA
    
                # Our matrix is in local space, time to bring it in its final desired space.
                if parent:
                    if is_global:
                        # Move matrix to global Blender space.
                        matrix = parent.matrix_global * matrix
                    elif parent.use_bake_space_transform(scene_data):
                        # Blender's and FBX's local space of parent may differ if we use bake_space_transform...
                        # Apply parent's *Blender* local space...
                        matrix = parent.matrix_local * matrix
                        # ...and move it back into parent's *FBX* local space.
                        par_mat = parent.fbx_object_matrix(scene_data, local_space=True)
                        matrix = par_mat.inverted() * matrix
    
            if self.use_bake_space_transform(scene_data):
                # If we bake the transforms we need to post-multiply inverse global transform.
                # This means that the global transform will not apply to children of this transform.
                matrix = matrix * scene_data.settings.global_matrix_inv
    
                # In any case, pre-multiply the global matrix to get it in FBX global space!
                matrix = scene_data.settings.global_matrix * matrix
    
            return matrix
    
        def fbx_object_tx(self, scene_data, rest=False, rot_euler_compat=None):
            """
            Generate object transform data (always in local space when possible).
            """
            matrix = self.fbx_object_matrix(scene_data, rest=rest)
            loc, rot, scale = matrix.decompose()
            matrix_rot = rot.to_matrix()
            # quat -> euler, we always use 'XYZ' order, use ref rotation if given.
            if rot_euler_compat is not None:
                rot = rot.to_euler('XYZ', rot_euler_compat)
    
                rot = rot.to_euler('XYZ')
            return loc, rot, scale, matrix, matrix_rot
    
        #### _tag dependent...
        def get_is_object(self):
            return self._tag == 'OB'
        is_object = property(get_is_object)
    
        def get_is_dupli(self):
            return self._tag == 'DP'
        is_dupli = property(get_is_dupli)
    
        def get_is_bone(self):
            return self._tag == 'BO'
        is_bone = property(get_is_bone)
    
        def get_type(self):
            if self._tag in {'OB', 'DP'}:
                return self.bdata.type
            return ...
        type = property(get_type)
    
        def get_armature(self):
            if self._tag == 'BO':
                return ObjectWrapper(self._ref)
            return None
        armature = property(get_armature)
    
        def get_bones(self):
            if self._tag == 'OB' and self.bdata.type == 'ARMATURE':
                return (ObjectWrapper(bo, self.bdata) for bo in self.bdata.data.bones)
            return ()
        bones = property(get_bones)
    
        def get_material_slots(self):
            if self._tag in {'OB', 'DP'}:
                return self.bdata.material_slots
            return ()
        material_slots = property(get_material_slots)
    
        #### Duplis...
        def dupli_list_create(self, scene, settings='PREVIEW'):
            if self._tag == 'OB':
                # Sigh, why raise exception here? :/
                try:
                    self.bdata.dupli_list_create(scene, settings)
                except:
                    pass
    
        def dupli_list_clear(self):
            if self._tag == 'OB':
                self.bdata.dupli_list_clear()
    
        def get_dupli_list(self):
            if self._tag == 'OB':
                return (ObjectWrapper(dup) for dup in self.bdata.dupli_list)
            return ()
        dupli_list = property(get_dupli_list)
    
    
    
    def fbx_name_class(name, cls):
        return FBX_NAME_CLASS_SEP.join((name, cls))
    
    
    
    def fbx_data_element_custom_properties(props, bid):
    
        """
        Store custom properties of blender ID bid (any mapping-like object, in fact) into FBX properties props.
        """
        for k, v in bid.items():
            if isinstance(v, str):
    
                elem_props_set(props, "p_string", k.encode(), v, custom=True)
    
            elif isinstance(v, int):
    
                elem_props_set(props, "p_integer", k.encode(), v, custom=True)
    
            if isinstance(v, float):
    
                elem_props_set(props, "p_double", k.encode(), v, custom=True)
    
    def fbx_data_empty_elements(root, empty, scene_data):
        """
        Write the Empty data block.
        """
        empty_key = scene_data.data_empties[empty]
    
        null = elem_data_single_int64(root, b"NodeAttribute", get_fbxuid_from_key(empty_key))
        null.add_string(fbx_name_class(empty.name.encode(), b"NodeAttribute"))
        null.add_string(b"Null")
    
        elem_data_single_string(null, b"TypeFlags", b"Null")
    
    
        tmpl = elem_props_template_init(scene_data.templates, b"Null")
    
    
        # No custom properties, already saved with object (Model).
    
    
    
    def fbx_data_lamp_elements(root, lamp, scene_data):
        """
        Write the Lamp data block.
        """
        gscale = scene_data.settings.global_scale
    
        lamp_key = scene_data.data_lamps[lamp]
        do_light = True
        decay_type = FBX_LIGHT_DECAY_TYPES['CONSTANT']
        do_shadow = False
        shadow_color = Vector((0.0, 0.0, 0.0))
        if lamp.type not in {'HEMI'}:
    
            if lamp.type not in {'SUN', 'AREA'}:
    
                decay_type = FBX_LIGHT_DECAY_TYPES[lamp.falloff_type]
            do_light = (not lamp.use_only_shadow) and (lamp.use_specular or lamp.use_diffuse)
            do_shadow = lamp.shadow_method not in {'NOSHADOW'}
            shadow_color = lamp.shadow_color
    
        light = elem_data_single_int64(root, b"NodeAttribute", get_fbxuid_from_key(lamp_key))
        light.add_string(fbx_name_class(lamp.name.encode(), b"NodeAttribute"))
        light.add_string(b"Light")
    
        elem_data_single_int32(light, b"GeometryVersion", FBX_GEOMETRY_VERSION)  # Sic...
    
    
        tmpl = elem_props_template_init(scene_data.templates, b"Light")
    
        props = elem_properties(light)
        elem_props_template_set(tmpl, props, "p_enum", b"LightType", FBX_LIGHT_TYPES[lamp.type])
        elem_props_template_set(tmpl, props, "p_bool", b"CastLight", do_light)
    
        elem_props_template_set(tmpl, props, "p_color", b"Color", lamp.color)
    
        elem_props_template_set(tmpl, props, "p_number", b"Intensity", lamp.energy * 100.0)
        elem_props_template_set(tmpl, props, "p_enum", b"DecayType", decay_type)
    
        elem_props_template_set(tmpl, props, "p_double", b"DecayStart", lamp.distance * gscale)
    
        elem_props_template_set(tmpl, props, "p_bool", b"CastShadows", do_shadow)
    
        elem_props_template_set(tmpl, props, "p_color", b"ShadowColor", shadow_color)
    
        if lamp.type in {'SPOT'}:
    
            elem_props_template_set(tmpl, props, "p_double", b"OuterAngle", math.degrees(lamp.spot_size))
            elem_props_template_set(tmpl, props, "p_double", b"InnerAngle",
    
                                    math.degrees(lamp.spot_size * (1.0 - lamp.spot_blend)))
    
    
        # Custom properties.
        if scene_data.settings.use_custom_properties:
    
            fbx_data_element_custom_properties(props, lamp)
    
    
    
    def fbx_data_camera_elements(root, cam_obj, scene_data):
        """
        Write the Camera data blocks.
        """
        gscale = scene_data.settings.global_scale
    
    
        cam_key = scene_data.data_cameras[cam_obj]
    
        # Real data now, good old camera!
        # Object transform info.
    
        loc, rot, scale, matrix, matrix_rot = cam_obj.fbx_object_tx(scene_data)
    
        up = matrix_rot * Vector((0.0, 1.0, 0.0))
        to = matrix_rot * Vector((0.0, 0.0, -1.0))
        # Render settings.
        # TODO We could export much more...
        render = scene_data.scene.render
        width = render.resolution_x
        height = render.resolution_y
        aspect = width / height
        # Film width & height from mm to inches
        filmwidth = units_convert(cam_data.sensor_width, "millimeter", "inch")
        filmheight = units_convert(cam_data.sensor_height, "millimeter", "inch")
        filmaspect = filmwidth / filmheight
        # Film offset
        offsetx = filmwidth * cam_data.shift_x
        offsety = filmaspect * filmheight * cam_data.shift_y
    
        cam = elem_data_single_int64(root, b"NodeAttribute", get_fbxuid_from_key(cam_key))
        cam.add_string(fbx_name_class(cam_data.name.encode(), b"NodeAttribute"))
        cam.add_string(b"Camera")
    
    
        tmpl = elem_props_template_init(scene_data.templates, b"Camera")
    
        props = elem_properties(cam)
    
        elem_props_template_set(tmpl, props, "p_vector", b"Position", loc)
        elem_props_template_set(tmpl, props, "p_vector", b"UpVector", up)
    
        elem_props_template_set(tmpl, props, "p_vector", b"InterestPosition", loc + to)  # Point, not vector!
    
        # Should we use world value?
    
        elem_props_template_set(tmpl, props, "p_color", b"BackgroundColor", (0.0, 0.0, 0.0))
    
        elem_props_template_set(tmpl, props, "p_bool", b"DisplayTurnTableIcon", True)
    
    
        elem_props_template_set(tmpl, props, "p_double", b"FilmWidth", filmwidth)
        elem_props_template_set(tmpl, props, "p_double", b"FilmHeight", filmheight)
        elem_props_template_set(tmpl, props, "p_double", b"FilmAspectRatio", filmaspect)
        elem_props_template_set(tmpl, props, "p_double", b"FilmOffsetX", offsetx)
        elem_props_template_set(tmpl, props, "p_double", b"FilmOffsetY", offsety)
    
    
        elem_props_template_set(tmpl, props, "p_enum", b"ApertureMode", 3)  # FocalLength.
        elem_props_template_set(tmpl, props, "p_enum", b"GateFit", 2)  # FitHorizontal.
        elem_props_template_set(tmpl, props, "p_fov", b"FieldOfView", math.degrees(cam_data.angle_x))
        elem_props_template_set(tmpl, props, "p_fov_x", b"FieldOfViewX", math.degrees(cam_data.angle_x))
        elem_props_template_set(tmpl, props, "p_fov_y", b"FieldOfViewY", math.degrees(cam_data.angle_y))
        # No need to convert to inches here...
    
        elem_props_template_set(tmpl, props, "p_double", b"FocalLength", cam_data.lens)
        elem_props_template_set(tmpl, props, "p_double", b"SafeAreaAspectRatio", aspect)
    
        elem_props_template_set(tmpl, props, "p_double", b"NearPlane", cam_data.clip_start * gscale)
        elem_props_template_set(tmpl, props, "p_double", b"FarPlane", cam_data.clip_end * gscale)
    
        elem_props_template_set(tmpl, props, "p_enum", b"BackPlaneDistanceMode", 1)  # RelativeToCamera.
    
        elem_props_template_set(tmpl, props, "p_double", b"BackPlaneDistance", cam_data.clip_end * gscale)
    
        # Custom properties.
        if scene_data.settings.use_custom_properties:
    
            fbx_data_element_custom_properties(props, cam_data)
    
    
        elem_data_single_string(cam, b"TypeFlags", b"Camera")
        elem_data_single_int32(cam, b"GeometryVersion", 124)  # Sic...
        elem_data_vec_float64(cam, b"Position", loc)
        elem_data_vec_float64(cam, b"Up", up)
        elem_data_vec_float64(cam, b"LookAt", to)
        elem_data_single_int32(cam, b"ShowInfoOnMoving", 1)
        elem_data_single_int32(cam, b"ShowAudio", 0)
        elem_data_vec_float64(cam, b"AudioColor", (0.0, 1.0, 0.0))
        elem_data_single_float64(cam, b"CameraOrthoZoom", 1.0)
    
    
    
    def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes):
    
        """
        Write the Mesh (Geometry) data block.
        """
    
        # Ugly helper... :/
        def _infinite_gen(val):
            while 1:
                yield val
    
    
        me_key, me, _free = scene_data.data_meshes[me_obj]
    
        # In case of multiple instances of same mesh, only write it once!
        if me_key in done_meshes:
            return
    
    
        # No gscale/gmat here, all data are supposed to be in object space.
        smooth_type = scene_data.settings.mesh_smooth_type
    
    
        do_bake_space_transform = ObjectWrapper(me_obj).use_bake_space_transform(scene_data)
    
    
        # Vertices are in object space, but we are post-multiplying all transforms with the inverse of the
        # global matrix, so we need to apply the global matrix to the vertices to get the correct result.
        geom_mat_co = scene_data.settings.global_matrix if do_bake_space_transform else None
        # We need to apply the inverse transpose of the global matrix when transforming normals.
    
        geom_mat_no = Matrix(scene_data.settings.global_matrix_inv_transposed) if do_bake_space_transform else None
        if geom_mat_no is not None:
            # Remove translation & scaling!
            geom_mat_no.translation = Vector()
            geom_mat_no.normalize()
    
        geom = elem_data_single_int64(root, b"Geometry", get_fbxuid_from_key(me_key))
        geom.add_string(fbx_name_class(me.name.encode(), b"Geometry"))
        geom.add_string(b"Mesh")
    
    
    Bastien Montagne's avatar
    Bastien Montagne committed
        tmpl = elem_props_template_init(scene_data.templates, b"Geometry")
    
        props = elem_properties(geom)
    
    
    Bastien Montagne's avatar
    Bastien Montagne committed
        elem_props_template_finalize(tmpl, props)
    
    
        # Custom properties.
        if scene_data.settings.use_custom_properties:
    
            fbx_data_element_custom_properties(props, me)
    
    
        elem_data_single_int32(geom, b"GeometryVersion", FBX_GEOMETRY_VERSION)
    
        # Vertex cos.
    
        t_co = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(me.vertices) * 3
    
        me.vertices.foreach_get("co", t_co)
    
        if geom_mat_co is not None:
            def _vcos_transformed_gen(raw_cos, m=None):
                # Note: we could most likely get much better performances with numpy, but will leave this as TODO for now.
                return chain(*(m * Vector(v) for v in zip(*(iter(raw_cos),) * 3)))
            t_co = _vcos_transformed_gen(t_co, geom_mat_co)
    
        elem_data_single_float64_array(geom, b"Vertices", t_co)
        del t_co
    
        # Polygon indices.
        #
        # We do loose edges as two-vertices faces, if enabled...
        #
    
    Bastien Montagne's avatar
    Bastien Montagne committed
        # Note we have to process Edges in the same time, as they are based on poly's loops...
    
        loop_nbr = len(me.loops)
    
        t_pvi = array.array(data_types.ARRAY_INT32, (0,)) * loop_nbr
    
        t_ls = [None] * len(me.polygons)
    
        me.loops.foreach_get("vertex_index", t_pvi)
        me.polygons.foreach_get("loop_start", t_ls)
    
        # Add "fake" faces for loose edges.
        if scene_data.settings.use_mesh_edges:
            t_le = tuple(e.vertices for e in me.edges if e.is_loose)
            t_pvi.extend(chain(*t_le))
            t_ls.extend(range(loop_nbr, loop_nbr + len(t_le), 2))
            del t_le
    
        # Edges...
        # Note: Edges are represented as a loop here: each edge uses a single index, which refers to the polygon array.
        #       The edge is made by the vertex indexed py this polygon's point and the next one on the same polygon.
        #       Advantage: Only one index per edge.
        #       Drawback: Only polygon's edges can be represented (that's why we have to add fake two-verts polygons
        #                 for loose edges).
        #       We also have to store a mapping from real edges to their indices in this array, for edge-mapped data
        #       (like e.g. crease).
        t_eli = array.array(data_types.ARRAY_INT32)
        edges_map = {}
        edges_nbr = 0
        if t_ls and t_pvi:
            t_ls = set(t_ls)
            todo_edges = [None] * len(me.edges) * 2
            me.edges.foreach_get("vertices", todo_edges)
            todo_edges = set((v1, v2) if v1 < v2 else (v2, v1) for v1, v2 in zip(*(iter(todo_edges),) * 2))
    
            li = 0
            vi = vi_start = t_pvi[0]
            for li_next, vi_next in enumerate(t_pvi[1:] + t_pvi[:1], start=1):
                if li_next in t_ls:  # End of a poly's loop.
                    vi2 = vi_start
                    vi_start = vi_next
                else:
                    vi2 = vi_next
    
                e_key = (vi, vi2) if vi < vi2 else (vi2, vi)
                if e_key in todo_edges:
                    t_eli.append(li)
                    todo_edges.remove(e_key)
                    edges_map[e_key] = edges_nbr
                    edges_nbr += 1
    
                vi = vi_next
                li = li_next
        # End of edges!
    
        # We have to ^-1 last index of each loop.
        for ls in t_ls:
            t_pvi[ls - 1] ^= -1
    
        # And finally we can write data!
        elem_data_single_int32_array(geom, b"PolygonVertexIndex", t_pvi)
        elem_data_single_int32_array(geom, b"Edges", t_eli)
        del t_pvi
        del t_ls
        del t_eli
    
        # And now, layers!
    
        # Smoothing.
        if smooth_type in {'FACE', 'EDGE'}:
            t_ps = None
            _map = b""
            if smooth_type == 'FACE':
    
                t_ps = array.array(data_types.ARRAY_INT32, (0,)) * len(me.polygons)
    
                me.polygons.foreach_get("use_smooth", t_ps)
                _map = b"ByPolygon"
            else:  # EDGE
                # Write Edge Smoothing.
    
                t_ps = array.array(data_types.ARRAY_INT32, (0,)) * edges_nbr
    
                for e in me.edges:
                    if e.key not in edges_map:
                        continue  # Only loose edges, in theory!
                    t_ps[edges_map[e.key]] = not e.use_edge_sharp
                _map = b"ByEdge"
            lay_smooth = elem_data_single_int32(geom, b"LayerElementSmoothing", 0)
            elem_data_single_int32(lay_smooth, b"Version", FBX_GEOMETRY_SMOOTHING_VERSION)
            elem_data_single_string(lay_smooth, b"Name", b"")
            elem_data_single_string(lay_smooth, b"MappingInformationType", _map)
            elem_data_single_string(lay_smooth, b"ReferenceInformationType", b"Direct")
    
    Bastien Montagne's avatar
    Bastien Montagne committed
            elem_data_single_int32_array(lay_smooth, b"Smoothing", t_ps)  # Sight, int32 for bool...
    
            del t_ps
    
        # TODO: Edge crease (LayerElementCrease).
    
        # And we are done with edges!
        del edges_map
    
        # Loop normals.
        # NOTE: this is not supported by importer currently.
        # XXX Official docs says normals should use IndexToDirect,
        #     but this does not seem well supported by apps currently...
        me.calc_normals_split()
    
            # Great, now normals are also expected 4D!
    
            # XXX Back to 3D normals for now!
            #gen = zip(*(iter(raw_nors),) * 3 + (_infinite_gen(1.0),))
            gen = zip(*(iter(raw_nors),) * 3)
    
            return gen if m is None else (m * Vector(v) for v in gen)
    
        t_ln = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(me.loops) * 3
        me.loops.foreach_get("normal", t_ln)
        t_ln = _nortuples_gen(t_ln, geom_mat_no)
    
            lay_nor = elem_data_single_int32(geom, b"LayerElementNormal", 0)
            elem_data_single_int32(lay_nor, b"Version", FBX_GEOMETRY_NORMAL_VERSION)
            elem_data_single_string(lay_nor, b"Name", b"")
            elem_data_single_string(lay_nor, b"MappingInformationType", b"ByPolygonVertex")
            elem_data_single_string(lay_nor, b"ReferenceInformationType", b"IndexToDirect")
    
    
            elem_data_single_float64_array(lay_nor, b"Normals", chain(*ln2idx))
            # Normal weights, no idea what it is.
    
            #t_lnw = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(ln2idx)
            #elem_data_single_float64_array(lay_nor, b"NormalsW", t_lnw)
    
    
            ln2idx = {nor: idx for idx, nor in enumerate(ln2idx)}
    
            elem_data_single_int32_array(lay_nor, b"NormalsIndex", (ln2idx[n] for n in t_ln))
    
        else:
            lay_nor = elem_data_single_int32(geom, b"LayerElementNormal", 0)
            elem_data_single_int32(lay_nor, b"Version", FBX_GEOMETRY_NORMAL_VERSION)
            elem_data_single_string(lay_nor, b"Name", b"")
            elem_data_single_string(lay_nor, b"MappingInformationType", b"ByPolygonVertex")
            elem_data_single_string(lay_nor, b"ReferenceInformationType", b"Direct")
    
            elem_data_single_float64_array(lay_nor, b"Normals", chain(*t_ln))
    
            # Normal weights, no idea what it is.
    
            #t_ln = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(me.loops)
            #elem_data_single_float64_array(lay_nor, b"NormalsW", t_ln)
    
    
        # tspace
        tspacenumber = 0
        if scene_data.settings.use_tspace:
            tspacenumber = len(me.uv_layers)
            if tspacenumber:
    
                t_ln = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(me.loops) * 3
    
                #t_lnw = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(me.loops)
    
                for idx, uvlayer in enumerate(me.uv_layers):
                    name = uvlayer.name
                    me.calc_tangents(name)
                    # Loop bitangents (aka binormals).
                    # NOTE: this is not supported by importer currently.
                    me.loops.foreach_get("bitangent", t_ln)
                    lay_nor = elem_data_single_int32(geom, b"LayerElementBinormal", idx)
                    elem_data_single_int32(lay_nor, b"Version", FBX_GEOMETRY_BINORMAL_VERSION)
                    elem_data_single_string_unicode(lay_nor, b"Name", name)
                    elem_data_single_string(lay_nor, b"MappingInformationType", b"ByPolygonVertex")
                    elem_data_single_string(lay_nor, b"ReferenceInformationType", b"Direct")
    
                    elem_data_single_float64_array(lay_nor, b"Binormals", chain(*_nortuples_gen(t_ln, geom_mat_no)))
    
                    # Binormal weights, no idea what it is.
    
                    #elem_data_single_float64_array(lay_nor, b"BinormalsW", t_lnw)
    
    
                    # Loop tangents.
                    # NOTE: this is not supported by importer currently.
                    me.loops.foreach_get("tangent", t_ln)
                    lay_nor = elem_data_single_int32(geom, b"LayerElementTangent", idx)
                    elem_data_single_int32(lay_nor, b"Version", FBX_GEOMETRY_TANGENT_VERSION)
                    elem_data_single_string_unicode(lay_nor, b"Name", name)
                    elem_data_single_string(lay_nor, b"MappingInformationType", b"ByPolygonVertex")
                    elem_data_single_string(lay_nor, b"ReferenceInformationType", b"Direct")
    
                    elem_data_single_float64_array(lay_nor, b"Tangents", chain(*_nortuples_gen(t_ln, geom_mat_no)))
    
                    # Tangent weights, no idea what it is.
    
                    #elem_data_single_float64_array(lay_nor, b"TangentsW", t_lnw)
    
                me.free_tangents()
    
        me.free_normals_split()
    
    
        # Write VertexColor Layers
        # note, no programs seem to use this info :/
        vcolnumber = len(me.vertex_colors)
        if vcolnumber:
            def _coltuples_gen(raw_cols):
                return zip(*(iter(raw_cols),) * 3 + (_infinite_gen(1.0),))  # We need a fake alpha...
    
    
            t_lc = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(me.loops) * 3
    
            for colindex, collayer in enumerate(me.vertex_colors):
                collayer.data.foreach_get("color", t_lc)
                lay_vcol = elem_data_single_int32(geom, b"LayerElementColor", colindex)
                elem_data_single_int32(lay_vcol, b"Version", FBX_GEOMETRY_VCOLOR_VERSION)
                elem_data_single_string_unicode(lay_vcol, b"Name", collayer.name)
                elem_data_single_string(lay_vcol, b"MappingInformationType", b"ByPolygonVertex")
                elem_data_single_string(lay_vcol, b"ReferenceInformationType", b"IndexToDirect")
    
                col2idx = tuple(set(_coltuples_gen(t_lc)))
                elem_data_single_float64_array(lay_vcol, b"Colors", chain(*col2idx))  # Flatten again...
    
                col2idx = {col: idx for idx, col in enumerate(col2idx)}
    
    Bastien Montagne's avatar
    Bastien Montagne committed
                elem_data_single_int32_array(lay_vcol, b"ColorIndex", (col2idx[c] for c in _coltuples_gen(t_lc)))
    
                del col2idx
            del t_lc
            del _coltuples_gen
    
        # Write UV layers.
        # Note: LayerElementTexture is deprecated since FBX 2011 - luckily!
        #       Textures are now only related to materials, in FBX!
        uvnumber = len(me.uv_layers)
        if uvnumber:
            def _uvtuples_gen(raw_uvs):
                return zip(*(iter(raw_uvs),) * 2)
    
    
            t_luv = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(me.loops) * 2
    
            for uvindex, uvlayer in enumerate(me.uv_layers):
                uvlayer.data.foreach_get("uv", t_luv)
                lay_uv = elem_data_single_int32(geom, b"LayerElementUV", uvindex)
                elem_data_single_int32(lay_uv, b"Version", FBX_GEOMETRY_UV_VERSION)
                elem_data_single_string_unicode(lay_uv, b"Name", uvlayer.name)
                elem_data_single_string(lay_uv, b"MappingInformationType", b"ByPolygonVertex")
                elem_data_single_string(lay_uv, b"ReferenceInformationType", b"IndexToDirect")
    
                uv2idx = tuple(set(_uvtuples_gen(t_luv)))
                elem_data_single_float64_array(lay_uv, b"UV", chain(*uv2idx))  # Flatten again...
    
                uv2idx = {uv: idx for idx, uv in enumerate(uv2idx)}
    
    Bastien Montagne's avatar
    Bastien Montagne committed
                elem_data_single_int32_array(lay_uv, b"UVIndex", (uv2idx[uv] for uv in _uvtuples_gen(t_luv)))
    
                del uv2idx
            del t_luv
            del _uvtuples_gen
    
        # Face's materials.
        me_fbxmats_idx = None
        if me in scene_data.mesh_mat_indices:
            me_fbxmats_idx = scene_data.mesh_mat_indices[me]
            me_blmats = me.materials
            if me_fbxmats_idx and me_blmats:
                lay_mat = elem_data_single_int32(geom, b"LayerElementMaterial", 0)
                elem_data_single_int32(lay_mat, b"Version", FBX_GEOMETRY_MATERIAL_VERSION)
                elem_data_single_string(lay_mat, b"Name", b"")
                nbr_mats = len(me_fbxmats_idx)
                if nbr_mats > 1:
    
                    t_pm = array.array(data_types.ARRAY_INT32, (0,)) * len(me.polygons)
    
                    me.polygons.foreach_get("material_index", t_pm)
    
                    # We have to validate mat indices, and map them to FBX indices.
                    blmats_to_fbxmats_idxs = [me_fbxmats_idx[m] for m in me_blmats]
                    mat_idx_limit = len(blmats_to_fbxmats_idxs)
                    def_mat = blmats_to_fbxmats_idxs[0]
    
                    _gen = (blmats_to_fbxmats_idxs[m] if m < mat_idx_limit else def_mat for m in t_pm)
    
                    t_pm = array.array(data_types.ARRAY_INT32, _gen)
    
                    elem_data_single_string(lay_mat, b"MappingInformationType", b"ByPolygon")
    
                    # XXX Logically, should be "Direct" reference type, since we do not have any index array, and have one
                    #     value per polygon...
                    #     But looks like FBX expects it to be IndexToDirect here (maybe because materials are already
                    #     indices??? *sigh*).
                    elem_data_single_string(lay_mat, b"ReferenceInformationType", b"IndexToDirect")
    
                    elem_data_single_int32_array(lay_mat, b"Materials", t_pm)
                    del t_pm
                else:
                    elem_data_single_string(lay_mat, b"MappingInformationType", b"AllSame")
                    elem_data_single_string(lay_mat, b"ReferenceInformationType", b"IndexToDirect")
                    elem_data_single_int32_array(lay_mat, b"Materials", [0])
    
        # And the "layer TOC"...
    
        layer = elem_data_single_int32(geom, b"Layer", 0)
        elem_data_single_int32(layer, b"Version", FBX_GEOMETRY_LAYER_VERSION)
        lay_nor = elem_empty(layer, b"LayerElement")
        elem_data_single_string(lay_nor, b"Type", b"LayerElementNormal")
        elem_data_single_int32(lay_nor, b"TypedIndex", 0)
    
        if tspacenumber:
            lay_binor = elem_empty(layer, b"LayerElement")
            elem_data_single_string(lay_binor, b"Type", b"LayerElementBinormal")
            elem_data_single_int32(lay_binor, b"TypedIndex", 0)
            lay_tan = elem_empty(layer, b"LayerElement")
            elem_data_single_string(lay_tan, b"Type", b"LayerElementTangent")
            elem_data_single_int32(lay_tan, b"TypedIndex", 0)
    
        if smooth_type in {'FACE', 'EDGE'}:
            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 vcolnumber:
            lay_vcol = elem_empty(layer, b"LayerElement")
            elem_data_single_string(lay_vcol, b"Type", b"LayerElementColor")
            elem_data_single_int32(lay_vcol, b"TypedIndex", 0)
        if uvnumber:
            lay_uv = elem_empty(layer, b"LayerElement")
            elem_data_single_string(lay_uv, b"Type", b"LayerElementUV")
            elem_data_single_int32(lay_uv, b"TypedIndex", 0)
        if me_fbxmats_idx is not None:
            lay_mat = elem_empty(layer, b"LayerElement")
            elem_data_single_string(lay_mat, b"Type", b"LayerElementMaterial")
            elem_data_single_int32(lay_mat, b"TypedIndex", 0)
    
        # Add other uv and/or vcol layers...
        for vcolidx, uvidx, tspaceidx in zip_longest(range(1, vcolnumber), range(1, uvnumber), range(1, tspacenumber),
                                                     fillvalue=0):
            layer = elem_data_single_int32(geom, b"Layer", max(vcolidx, uvidx))
            elem_data_single_int32(layer, b"Version", FBX_GEOMETRY_LAYER_VERSION)
            if vcolidx:
                lay_vcol = elem_empty(layer, b"LayerElement")
                elem_data_single_string(lay_vcol, b"Type", b"LayerElementColor")
                elem_data_single_int32(lay_vcol, b"TypedIndex", vcolidx)
            if uvidx:
                lay_uv = elem_empty(layer, b"LayerElement")
                elem_data_single_string(lay_uv, b"Type", b"LayerElementUV")
                elem_data_single_int32(lay_uv, b"TypedIndex", uvidx)
            if tspaceidx:
                lay_binor = elem_empty(layer, b"LayerElement")
                elem_data_single_string(lay_binor, b"Type", b"LayerElementBinormal")
                elem_data_single_int32(lay_binor, b"TypedIndex", tspaceidx)
                lay_tan = elem_empty(layer, b"LayerElement")
                elem_data_single_string(lay_tan, b"Type", b"LayerElementTangent")
                elem_data_single_int32(lay_tan, b"TypedIndex", tspaceidx)
    
    
        done_meshes.add(me_key)
    
    
    
    def fbx_data_material_elements(root, mat, scene_data):
        """
        Write the Material data block.
        """
        ambient_color = (0.0, 0.0, 0.0)
        if scene_data.data_world:
            ambient_color = next(iter(scene_data.data_world.keys())).ambient_color
    
        mat_key, _objs = scene_data.data_materials[mat]
        # Approximation...
    
        mat_type = b"Phong" if mat.specular_shader in {'COOKTORR', 'PHONG', 'BLINN'} else b"Lambert"
    
    
        fbx_mat = elem_data_single_int64(root, b"Material", get_fbxuid_from_key(mat_key))
        fbx_mat.add_string(fbx_name_class(mat.name.encode(), b"Material"))
        fbx_mat.add_string(b"")
    
        elem_data_single_int32(fbx_mat, b"Version", FBX_MATERIAL_VERSION)
        # those are not yet properties, it seems...
        elem_data_single_string(fbx_mat, b"ShadingModel", mat_type)
        elem_data_single_int32(fbx_mat, b"MultiLayer", 0)  # Should be bool...
    
    
        tmpl = elem_props_template_init(scene_data.templates, b"Material")
    
        props = elem_properties(fbx_mat)
    
        elem_props_template_set(tmpl, props, "p_string", b"ShadingModel", mat_type.decode())
    
        elem_props_template_set(tmpl, props, "p_color", b"EmissiveColor", mat.diffuse_color)
    
        elem_props_template_set(tmpl, props, "p_number", b"EmissiveFactor", mat.emit)
    
        elem_props_template_set(tmpl, props, "p_color", b"AmbientColor", ambient_color)
    
        elem_props_template_set(tmpl, props, "p_number", b"AmbientFactor", mat.ambient)
    
        elem_props_template_set(tmpl, props, "p_color", b"DiffuseColor", mat.diffuse_color)
    
        elem_props_template_set(tmpl, props, "p_number", b"DiffuseFactor", mat.diffuse_intensity)
    
        elem_props_template_set(tmpl, props, "p_color", b"TransparentColor",
    
                                mat.diffuse_color if mat.use_transparency else (1.0, 1.0, 1.0))
    
        elem_props_template_set(tmpl, props, "p_number", b"TransparencyFactor",
                                1.0 - mat.alpha if mat.use_transparency else 0.0)
        elem_props_template_set(tmpl, props, "p_number", b"Opacity", mat.alpha if mat.use_transparency else 1.0)
        elem_props_template_set(tmpl, props, "p_vector_3d", b"NormalMap", (0.0, 0.0, 0.0))
        # Not sure about those...
    
    Bastien Montagne's avatar
    Bastien Montagne committed
        """
    
        b"Bump": ((0.0, 0.0, 0.0), "p_vector_3d"),
    
        b"DisplacementColor": ((0.0, 0.0, 0.0), "p_color_rgb"),
    
            elem_props_template_set(tmpl, props, "p_color", b"SpecularColor", mat.specular_color)
    
            elem_props_template_set(tmpl, props, "p_number", b"SpecularFactor", mat.specular_intensity / 2.0)
            # See Material template about those two!
            elem_props_template_set(tmpl, props, "p_number", b"Shininess", (mat.specular_hardness - 1.0) / 5.10)
            elem_props_template_set(tmpl, props, "p_number", b"ShininessExponent", (mat.specular_hardness - 1.0) / 5.10)
    
            elem_props_template_set(tmpl, props, "p_color", b"ReflectionColor", mat.mirror_color)
    
            elem_props_template_set(tmpl, props, "p_number", b"ReflectionFactor",
                                    mat.raytrace_mirror.reflect_factor if mat.raytrace_mirror.use else 0.0)
    
    
        # Custom properties.
        if scene_data.settings.use_custom_properties:
    
            fbx_data_element_custom_properties(props, mat)
    
    
    
    def _gen_vid_path(img, scene_data):
        msetts = scene_data.settings.media_settings
        fname_rel = bpy_extras.io_utils.path_reference(img.filepath, msetts.base_src, msetts.base_dst, msetts.path_mode,
                                                       msetts.subdir, msetts.copy_set, img.library)
        fname_abs = os.path.normpath(os.path.abspath(os.path.join(msetts.base_dst, fname_rel)))
        return fname_abs, fname_rel
    
    
    def fbx_data_texture_file_elements(root, tex, scene_data):
        """
        Write the (file) Texture data block.
        """
        # XXX All this is very fuzzy to me currently...
        #     Textures do not seem to use properties as much as they could.
        #     For now assuming most logical and simple stuff.
    
        tex_key, _mats = scene_data.data_textures[tex]
        img = tex.texture.image
        fname_abs, fname_rel = _gen_vid_path(img, scene_data)
    
        fbx_tex = elem_data_single_int64(root, b"Texture", get_fbxuid_from_key(tex_key))
        fbx_tex.add_string(fbx_name_class(tex.name.encode(), b"Texture"))
        fbx_tex.add_string(b"")
    
        elem_data_single_string(fbx_tex, b"Type", b"TextureVideoClip")
        elem_data_single_int32(fbx_tex, b"Version", FBX_TEXTURE_VERSION)
        elem_data_single_string(fbx_tex, b"TextureName", fbx_name_class(tex.name.encode(), b"Texture"))
        elem_data_single_string(fbx_tex, b"Media", fbx_name_class(img.name.encode(), b"Video"))
        elem_data_single_string_unicode(fbx_tex, b"FileName", fname_abs)
        elem_data_single_string_unicode(fbx_tex, b"RelativeFilename", fname_rel)
    
        alpha_source = 0  # None
        if img.use_alpha:
            if tex.texture.use_calculate_alpha:
                alpha_source = 1  # RGBIntensity as alpha.
            else:
                alpha_source = 2  # Black, i.e. alpha channel.
        # BlendMode not useful for now, only affects layered textures afaics.
        mapping = 0  # None.
        if tex.texture_coords in {'ORCO'}:  # XXX Others?
            if tex.mapping in {'FLAT'}:
                mapping = 1  # Planar
            elif tex.mapping in {'CUBE'}:
                mapping = 4  # Box
            elif tex.mapping in {'TUBE'}:
                mapping = 3  # Cylindrical
            elif tex.mapping in {'SPHERE'}:
                mapping = 2  # Spherical
        elif tex.texture_coords in {'UV'}:
            # XXX *HOW* do we link to correct UVLayer???
            mapping = 6  # UV
        wrap_mode = 1  # Clamp
        if tex.texture.extension in {'REPEAT'}:
            wrap_mode = 0  # Repeat
    
    
        tmpl = elem_props_template_init(scene_data.templates, b"TextureFile")
    
        props = elem_properties(fbx_tex)
        elem_props_template_set(tmpl, props, "p_enum", b"AlphaSource", alpha_source)
        elem_props_template_set(tmpl, props, "p_bool", b"PremultiplyAlpha",
                                img.alpha_mode in {'STRAIGHT'})  # Or is it PREMUL?
        elem_props_template_set(tmpl, props, "p_enum", b"CurrentMappingType", mapping)
        elem_props_template_set(tmpl, props, "p_enum", b"WrapModeU", wrap_mode)
        elem_props_template_set(tmpl, props, "p_enum", b"WrapModeV", wrap_mode)
        elem_props_template_set(tmpl, props, "p_vector_3d", b"Translation", tex.offset)
        elem_props_template_set(tmpl, props, "p_vector_3d", b"Scaling", tex.scale)
        elem_props_template_set(tmpl, props, "p_bool", b"UseMipMap", tex.texture.use_mipmap)
    
    
        # Custom properties.
        if scene_data.settings.use_custom_properties:
    
            fbx_data_element_custom_properties(props, tex.texture)
    
    def fbx_data_video_elements(root, vid, scene_data):
        """
        Write the actual image data block.
        """
        vid_key, _texs = scene_data.data_videos[vid]
        fname_abs, fname_rel = _gen_vid_path(vid, scene_data)
    
        fbx_vid = elem_data_single_int64(root, b"Video", get_fbxuid_from_key(vid_key))
        fbx_vid.add_string(fbx_name_class(vid.name.encode(), b"Video"))
        fbx_vid.add_string(b"Clip")
    
        elem_data_single_string(fbx_vid, b"Type", b"Clip")
        # XXX No Version???
        elem_data_single_string_unicode(fbx_vid, b"FileName", fname_abs)
        elem_data_single_string_unicode(fbx_vid, b"RelativeFilename", fname_rel)
    
        if scene_data.settings.media_settings.embed_textures:
            try:
                with open(vid.filepath, 'br') as f:
                    elem_data_single_byte_array(fbx_vid, b"Content", f.read())