From BlenderWiki

Jump to: navigation, search

FBX File Structure

Since we have the binary file format itself, this doc attempt to gather knowledge about FBX structure (typical content of a file, but also usage of properties and templates, etc.).

Warning: Heavily WIP, as we have to guess what does what here (no official doc about how things work)!

We try to focus on latest version (7.3, or even 7.4)!

Note: Once we have something more or less complete here, we could move it to the blog page as well…

Generalities

FBX is a nodal format, with a root element (which is never explicitly written) and a tree of children.

Each element has an id (byte string), and can have data and children elements.

Data are a set of (values, type) tuples, available types are: bool, short, int, long, float, double, bytes, string, and arrays of those types.

Here is a (JSON) representation of an element:

["Element_ID",                         # ID
     ["data_string", 13],              # Data
     "SI",                             # Data types, as single-char codes (S for String, I for Integer, etc.)
     [__other_children_elements__]
]

Everything is based on this simple schema.

Properties

Properties are a way to add heavily typed data.

Properties are represented by elements children of a "Properties70" element, which does not contain any data.

Each property is an element. Its ID does not seem to be important (usually, it's "P" or "PS" for all of them). Their data layout follow that schema:

["PropName", "PropType", "Label(?)", "Flags", __values__, …]

In other words, the four first data of a property are always strings (its name, its type, presumably its label (often empty), and some flags (optional)), they are the "metadata" of the property. The other data are the property's value (usually only one, but e.g. for colors or 3D vectors they are three - and some property types have no value :/), and their type depends on the data type!

Note that the three metadata defining the property's type looks rather fuzzy, they may change between versions.

Flags are still a bit fuzzy, here is what is known currently:

  • 'U' means the property is user-defined (it does not exist by default in FBX).
  • 'A' means the property is animatable.
  • '+' also found in some files, mysterious role for now.

Further about animatable properties: Looks like the first two metadata depend whether the property is animatable or not, finally. E.g. a non-animatable float64 will be ("double", "Number", ""), while an animatable float64 will be ("Number", "", "") - at least, that what’s infereable from reading "official" FBX files.

Bellow are some basic examples of properties:

Integer: ["P", ["some_name", "int", "Integer", "", 1], "SSSSI", []]
Double:  ["P", ["some_name", "double", "Number", "", 1.0], "SSSSD", []]
Color:   ["P", ["some_name", "ColorRGB", "Color", "", 0.0, 0.0, 0.0], "SSSSDDD", []]

It seems properties can also be grouped, using the "Compound" type (which has no value), e.g.:

["P", ["Original", "Compound", "", ""], "SSSS", []],
["P", ["Original|ApplicationVendor", "KString", "", "", ""], "SSSSS", []],
["P", ["Original|ApplicationName", "KString", "", "", ""], "SSSSS", []],
["P", ["Original|ApplicationVersion", "KString", "", "", ""], "SSSSS", []],
["P", ["Original|DateTime_GMT", "DateTime", "", "", ""], "SSSSS", []],
["P", ["Original|FileName", "KString", "", "", ""], "SSSSS", []],

Groups are defined by the properties’ names starting by the same value as their “parent” property’s name (using '|' as separator).

Some Specific Property Types

  • KTime: FBX timestamp, a signed int64, unit being 1/46186158000 seconds…

Templates

Each template is defined by an "ObjectType" element, which takes a single property, the name of the template, which is also the name of the Object they “define” (e.g. "Model", "Geometry", "Material", etc.).

Each template has two children: "Count" is the number of Objects using this template, and "PropertyTemplate", which simply contains some properties.

To summarize, here is the generic structure of a template definition:

["ObjectType", ["Geometry"], "S", [            # Start of template definition
    ["Count", [1], "I", []],                   # Number of Objects using this template
    ["PropertyTemplate", ["KFbxMesh"], "S", [  # Start of template's properties
        ["Properties70", [], "", [
            ["P", ["Color", "ColorRGB", "Color", "", 0.8, 0.8, 0.8], "SSSSDDD", []],
            ["P", ["BBoxMin", "Vector3D", "Vector", "", 0.0, 0.0, 0.0], "SSSSDDD", []],
            ["P", ["BBoxMax", "Vector3D", "Vector", "", 0.0, 0.0, 0.0], "SSSSDDD", []],
            ...
        ]]
    ]]
]],

Note GlobalSettings also seems to have its template definition, though it is not an Object…

The name data of the "PropertyTemplate" element are some kind of sub-types. Usually, they directly use classes’ names from the SDK.

Sub-type Name
In FBX 7.3, sub-type names seems to use SDK class names prefixed with a 'K' (like 'KFbxFileTexture'), while in 7.4 the 'K' seems to have disappeared…


Templates define some kind of “default values”, which can be overridden by each Object if needed.

There is another issue with templates: e.g. lights, cameras, nulls, etc. share the same "global" element type, while they do have separated sub-types, which their own set of properties. Unfortunately, templates are defined for “global” type, and do not look to handle several set of property definitions, so you have to chose one subtype in this case, and do without template at all for the other subtypes (though to counter of the template must remain the total number of users of the “global” type…).

Top Structure

A valid FBX file must contain a set of standard elements:

  • FBXHeaderExtension: Mandatory? Contains the metadata of the file.
  • FileId: Mandatory. Some kind of (currently) obscure hash, apparently based on the CreationTime data.
  • CreationTime: Mandatory. The date/time of creation of that file, as a single string (e.g. "2013-08-05 10:27:44:000").
  • Creator: Mandatory? A string identifying the tool used to create that FBX.
  • GlobalSettings: Mandatory? A set of properties defining general data, like the orientation of the scene…
  • Documents: Optional. Not sure what it is used for actually, so far only saw FBX files with a single doc definition here… Maybe it allows for several scenes in a single FBX?
  • References: ???
  • Definitions: Optional. Here templates are defined.
  • Objects: Optional. Here real, useful, static (?) data are stored (objects, geometries, textures, materials, etc.).
  • Connections: Optional. Here are defined links between data defined in Objects (e.g. which object uses which material, etc.).
  • Takes: Optional. Here animations are defined?

FBXHeaderExtension

FileId and CreationTime

Those two fields are used as some kind of obscure “signing” of the file. It seems to use the CreationTime to generate the FileId, but we still do not know how, so currently we use a dummy timestamp (0 since epoch) and fixed fileid… Investigation needed here!

GlobalSettings

Documents

Not sure what it is used for currently - looks like FBX could support multiple documents (like in, multiple scenes?), but only found single-doc examples so far... To be investigated.

Contains a "Count" element (number of documents), and a set of "Document" elements.

Each Document contains three data, an UID (as int64), an always empty string, and a name. Its children are a set of properties, and a "RootNode" with a unique int64 value (always 0, most likely an offset of some kind, have no more hints here).

["Documents", [], "", [
    ["Count", [1], "I", []],
    ["Document", [151925416, "", "Scene"], "LSS", [
        ["Properties70", [], "", [
            ["P", ["SourceObject", "object", "", ""], "SSSS", []],
            ["P", ["ActiveAnimStackName", "KString", "", "", ""], "SSSSS", []]
        ]],
        ["RootNode", [0], "L", []]
    ]]
]]

References

Currently have absolutely no idea what this is. Usually empty in examples I found.

Definitions

Contains a Version element, a Count one (which gives the total number of data elements using those template definitions), and a set of template definitions:

["Definitions", [], "", [
    ["Version", [100], "I", []],
    ["Count", [12], "I", []],
    ["ObjectType", ["GlobalSettings"], "S", [
        ...
    ]],
    ["ObjectType", ["Model"], "S", [
        ...
    ]],
    ...
]]

Objects

Objects are a set of elements of various kind (light data, camera data, geometry data, object data, material, texture, etc.), which later get linked together in the Connections section, using their UIDs.

All data have a similar top-level element, which seems to use a standard name depending on its content type, has three data (the first one being its UID, the second a 'name::class' pair, and the third a sort of sub-class), and a set of children, including properties:

 ["NodeAttribute", [                 # Main class
         151952504,                  # The UID.
         "::NodeAttribute",          # Name::Class
         "CameraSwitcher"            # Sub-class
     ], "LSS", [
     ["Properties70", [], "", [
         ["P", ["Camera Index", "Integer", "", "A+", 100], "SSSSI", []]
     ]],
     ["Version", [101], "I", []],
     ...
 ]]

The name::class separator (represented as '::' here) in binary format is in fact the chain b"\x00\x01".

Note it looks like some children elements sometimes repeat content from properties (e.g. for cameras, "Position", "UpVector" and "InterestPosition" properties have same content as "Position", "Up" and "LookAt" children elements respectively). Probably some compatibility matters.

Connections

Connections simply defines links between objects (like a given material is used by a given object, etc.):

 ["Connections", [], "", [
     ["C", ["OO", 152171968, 0], "SLL", []],
     ["C", ["OO", 152161640, 0], "SLL", []],
     ["C", ["OP", 152102088, 987654321, "Lcl Translation"], "SLLS", []],
     …
 ]]

Each connection is an element of name "C", with following data:

  • A code string defining the nature of the relation (so far, found "OO" for object linked to object, and "OP" for object linked to property.
  • A first UID (the "child").
  • A second UID (the "parent", if 0 it is the (implicit) root node of the FBX doc).
  • In case of object-to-property connection, a string which is the name of the parent’s property the child is linked to. This is used e.g. to assign a texture to the diffuse, specular, ambient, etc. property of a material. Note that property may be an “Properties70” one, or a direct parent's element one.

I also saw in a few files "PO" relations (property children of element), and I guess we could also imagine "PP" ones - but we do not have to bother about those currently, did not hit any use case yet.

Takes

This is the old (deprecated even?) animation system, looks like letting it empty in 7.4 files does not hurt…

Main Data - Objects

Model

  • Class: FbxObject::FbxNode
Connected to root element (0) or another Model in case of parenting.

These elements are roughly equivalent to Blender’s Objects. They define basic location, rotation and scale, as well as a bunch of advanced settings related to the same topics.

Cameras…
When linked to camera data, Models also contain some camera data (like width/eight, etc.). Not sure though this is not a remnant from older versions of the format…


Transformations Constraints

Any FBX Model can have translation/rotation/scaling limits, enabled by a "...Active" bool property, and with global or per-axis min/max values.

It can also have some "track to"-like relations, using another object of the scene as target (and optionally another one as up-target). However, how this works is still unclear (relevant properties are "object" type, which holds no value…).

Spaces & Parenting

Transformations of a Model (mostly "Lcl Translation", "Lcl Rotation" [Euler, in degrees] and "Lcl Scaling") are always in parent space, or world space in no parent is present ("Lcl" stands for "Local"). This is also correct for bones, since in FBX they are roughly represented as chains of parented Models (see #Armature_and_Bones Armature and Bones below).

However, there are three ways to apply child transformations in parent space, controlled by the "InheritType" enum property (FbxTransform::EInheritType):

  • eInheritRrSs (0).
  • eInheritRSrs (1).
  • eInheritRrs (2).

Uppercase are for parent (rotation/scaling), lowercase are for child.

With Blender parenting and in bone chains, we should use RSrs, which matches usual 3×3 matrix multiplications (as far as I understand!). And we could also maybe use Rrs to match our disabled "Inherit Scale" bone option?

Light Data

  • Class: FbxObject::FbxNodeAttribute::FbxLight
Connected to a Model.

Roughly equivalent to Blender’s Lamp (type of light source, energy, direction, etc.).

Camera Data

  • Class: FbxObject::FbxNodeAttribute::FbxCamera
Connected to a Model (plus another, “virtual” Model for CameraSwitcher?).
  • Class: FbxObject::FbxNodeAttribute::FbxCameraSwitcher

Camera data are divided in two entities, camera itself, and “camera switcher”, which I have not much hints about yet.

Camera embeds object-level data (position and orientation). Camera expects values for its sensor’s size to be in inches! Camera expects values for its angles to be in degrees.

Mesh Data

  • Class: FbxObject::FbxNodeAttribute::FbxLayerContainer::FbxGeometryBase::FbxGeometry::FbxMesh
Connected to a Model.

Roughly equivalent to Blender’s Mesh.

This is in the “Layer” element family, i.e. it can contains several data of the same type (one per layer), e.g. for UV maps or VCol layers. The general structure is:

 ["Geometry", [152167664, "Name::Geometry", "Mesh"], "LSS", [
     ["Vertices", [[<array_of_floats>]], "d", []],
     ["PolygonVertexIndex", [[<array_of_integers>]], "i", []],
     ["Edges", [[<array_of_integers>]], "i", []],
     ["GeometryVersion", [124], "I", []],
     ["LayerElementNormal", [0], "I", [
         ["Version", [101], "I", []],
         ["Name", [""], "S", []],
         ["MappingInformationType", ["ByVertice"], "S", []],
         ["ReferenceInformationType", ["Direct"], "S", []],
         ["Normals", [[<array_of_floats>]], "d", []]
     ]],
     ["LayerElementSmoothing", [0], "I", [
         ["Version", [102], "I", []],
         ["Name", [""], "S", []],
         ["MappingInformationType", ["ByPolygon"], "S", []],
         ["ReferenceInformationType", ["Direct"], "S", []],
         ["Smoothing", [[<array_of_integers>]], "i", []]  # Yep, int32 for bool values...
     ]],
     ["LayerElementUV", [0], "I", [
         ["Version", [101], "I", []],
         ["Name", ["UVMap"], "S", []],
         ["MappingInformationType", ["ByPolygonVertex"], "S", []],
         ["ReferenceInformationType", ["IndexToDirect"], "S", []],
         ["UV", [[<array_of_floats>]], "d", []],
         ["UVIndex", [[<array_of_integers>]], "i", []],
     ]],
     ["LayerElementUV", [1], "I", [
         ["Version", [101], "I", []],
         ["Name", ["UVMap.001"], "S", []],
         ["MappingInformationType", ["ByPolygonVertex"], "S", []],
         ["ReferenceInformationType", ["IndexToDirect"], "S", []],
         ["UV", [[<array_of_floats>]], "d", []],
         ["UVIndex", [[<array_of_integers>]], "i", []],
     ]],
     ["LayerElementMaterial", [0], "I", [
         ["Version", [101], "I", []],
         ["Name", ["gold"], "S", []],
         ["MappingInformationType", ["AllSame"], "S", []],
         ["ReferenceInformationType", ["IndexToDirect"], "S", []],
         ["Materials", [[0]], "i", []]
     ]],
     ["Layer", [0], "I", [
         ["Version", [100], "I", []],
         ["LayerElement", [], "", [
             ["Type", ["LayerElementNormal"], "S", []],
             ["TypedIndex", [0], "I", []]
         ]],
         ["LayerElement", [], "", [
             ["Type", ["LayerElementMaterial"], "S", []],
             ["TypedIndex", [0], "I", []]
         ]],
         ["LayerElement", [], "", [
             ["Type", ["LayerElementSmoothing"], "S", []],
             ["TypedIndex", [0], "I", []]
         ]]
     ]]
     ["Layer", [1], "I", [
         ["Version", [100], "I", []],
         ["LayerElement", [], "", [
             ["Type", ["LayerElementUV"], "S", []],
             ["TypedIndex", [1], "I", []]
         ]],
     ]]
 ]]

Here again, the '::' is just a convention, in binary format is in fact the chain b"\x00\x01".

So, we have first the basic data:

  • Vertices: a flatten array of coords (so if verts[0] == (1.0, 2.0, 0.5), array starts with [1.0, 2.0, 0.5, ...]).
  • Polygons (defined by vertices indices, the last index of a polygon is negated).
  • Edges (optional). Edges are defined in quite an odd way: each index here refers to a polygon’s array index, so that the edge is defined by this polygon’s vertex and the next one in the same polygon. Why make things simple when you can make them complex?

Then, you have the layer elements (uvs, materials, vcol, smooth, etc.). Those layer elements can affect vertices, edges, faces, faces’ vertices (what we call “loops” in Blender code) or everything. Except for the latest case, you can map layer data to affected geometry elements in two different ways:

  • Direct: item n in layer data array affects (vertice|edge|face|loop) n.
  • IndexToDirect: layer data only contains singular values, and an additional integer array maps each geometry element to a value in layer data array. So e.g. if UVs of loop 5 and loop 8 are the same, the fifth and eighth items of layer index array will both have the same value, which points to the correct UV triplet in layer data.

And once all layer element have been defined, you have to define the layers themselves, which contain them. There can only be one layer element of each type in a layer.

Null Data

  • Class: FbxObject::FbxNodeAttribute::FbxNull
Connected to a Model.

Roughly equivalent to Blender’s Empty.

Armature and Bones

In FBX, there is no real armature concept, you rather have chains of bones, which are nearly only defined by "Model" elements (FbxNode), i.e. loc/rot/scale from parent bone. Root bones are children of an empty object (Model), which plays the role of an armature.

In other words, armatures are presented more like a set of chains of parented-hooks.

Each bone/hook is represented by a "LimbNode", which also contains a "Size" parameter (its length), not sure this is used/understood by all importers though.

Bones and mesh are "linked" first by a BindPose element, which stores the (transform) matrix of the mesh and all bones in global (world) space, at the time of binding.

Second "link" is the Deformer (Skin) element, which is kind of similar to Blender's vertex group system for armatures. This element stores SubDeformers (a.k.a. Clusters), one per bone, with 'binding' weights for each vertex/control point, and some transform matrices.

Those transform matrices are not yet much clear to me, as far as I understand, they refer to transformations of mesh/armature/bone at binding time:

  • Transform is the transformation of the mesh, in the bone's space!.
  • TransformLink is the transformation of the bone, in global space.
  • TransformAssociateModel would be transformation of the armature object??? Not quite sure why it’b be needed…

To summarize connections:

  • LimbNode (bone data) -> Model (bone transformations, mere Empty object linked parent bone's one as any other Model parent relationship).
  • LimbNode (bone data) -> Cluster (kind of 'vgroup') -> Skin -> Geometry (usually mesh data).

BindPose has its own 'linking' system, that does not use connections (yes, FBX…). All children PoseNode elements have a Node property that contains the numeric ID of the element (mesh or bone) they represent. I would assume this is an old, somewhat deprecated system, in favor to the cluster/skin one…

Shape Keys

Shape keys handling are very similar to armatures one in FBX 7.x.

First, you define your shapes, as special Geometry elements ('Shape'). They only store a list of vertex indices, and matching coordinates and normals (normals often stored as NULL vector, not sure yet what it means). Note that the shaped mesh element gets one animatable num prop for each of its shape keys, looks like two different anim systems of the shape's influence coexist here… :/

It also generates a BindPose in this case, but against the Model element, not the Geometry one of the mesh… And I really do not see what’s the use of the bindnode matrix here - will ignore completely that for now, sigh.

Main (and probably future-proof ?) way to link a shape to its mesh is done through the Deformer system - here main element is a “BlendShape” element, and subdeformers are “BlendShapeChannel”. Those have a global animatable DeformPercent property, and an FullWeights array of factors, that I assume to be some kind of per-vertex weighting, we can probably ignore that in Blender for now.

  • Shape -> BlendShapeChannel -> BlendShape -> Geometry (usually mesh data).

Material Data

  • Class: FbxObject::FbxSurfaceMaterial
Connected to a Model.

FBX supports two kind of materials, basic Lambert/Phong shading, and custom shaders. Unfortunately, the laters only support closed shaders (HLSL and CGSL), no GLSL, so we won’t see those here (for now).

Basic shading will hence make you use either FbxSurfaceMaterialLambert (subclass of surfacematerial, for diffuse only) or FbxSurfaceMaterialPhong (subclass of lambert one, for diffuse + specular shading).

Note the data node itself is always simply called "Material", and you specify either "phong" or "lambert" in the "ShadingModel" element:

 ["Material", [1055568016, "default::Material", ""], "LSS", [
     ["Version", [102], "I", []],
     ["ShadingModel", ["phong"], "S", []],
     ["MultiLayer", [0], "I", []],
     ["Properties70", [], "", [
         ["P", ["ShadingModel", "KString", "", "", "phong"], "SSSSS", []],
         ["P", ["EmissiveFactor", "Number", "", "A", 0.0], "SSSSD", []],
         ["P", ["AmbientColor", "Color", "", "A", 0.5879999995231628, 0.5879999995231628, 0.5879999995231628], "SSSSDDD", []],
         ["P", ["DiffuseColor", "Color", "", "A", 0.5879999995231628, 0.5879999995231628, 0.5879999995231628], "SSSSDDD", []],
         ...
     ]]
 ]]

Multilayer is actually a bool, I think, not sure yet what it means/controls exactly.

Depending on the app, in theory most properties of materials can be controlled by textures (linking a texture e.g. to the "DiffuseColor" property is how apply a texture to a material's color!), see below for more details.

Note that meshes can have a LayerElementMaterial layer, which assigns a given material to a given face. Materials here are referenced by a mere index (instead of UIDs as everywhere else in modern FBX), which is the order in which they are linked to the object (FBX node)!

Texture Data

  • Class: FbxObject::FbxTexture::FbxFileTexture
Connected to a Material.
  • Class: FbxObject::FbxVideo
Connected to a FileTexture.

Textures are divided in two elements in FBX:

  • The texture itself, which contains mapping data, and is linked to materials’ properties ("OP" connections).
  • The “Video” (or “Clip”), which references pictures, and is also used for static images (liked to Texture elements).

The Texture (TextureVideoClip type) contains all mapping data (can be UV of course, but also simple cube/sphere/tube projection, etc.). It also contains file path, even though Video also has those data. Note there seems to be no way to choose a specific UVmap for a given texture! At least, found none…

The Video (Clip type) contains the actual image/video path, together with some info like its name. It can also embed the binary data of the image/video, in "Content" byte array element.

Note in theory, there is also a support of procedural textures, but these are mere opaque blob of data, unusable for us.

We may also use the layered textures to match situations where several textures affect a same property of a same material. Would first check whether it’s supported enough well across applications, though!

Animation

  • Class: FbxObject::FbxCollection::FbxAnimStack
Connected to noting!
  • Class: FbxObject::FbxCollection::FbxAnimLayer
Connected to an AnimStack.
  • Class: FbxObject::FbxAnimCurveNode
Connected to an AnimLayer and a Node's property.
  • Class: FbxObject::FbxAnimCurveBase::FbxAnimCurve
Connected to an AnimCurveNode's property.

(New) animation in FBX is actually fairly simple: you have a Stack (usually only one per FBX file, else you may have compatibility issues), to which a set of Layers are linked.

AnimLayers are similar to Blender’s Actions. You may have several Layers affecting a same object’s property, with blending effects, however we won’t seek for such complexity in our exporter!

A set of CurveNodes is linked to each AnimLayer, which define which properties are animated:

 ["AnimationCurveNode", [2045776, "T::AnimCurveNode", ""], "LSS", [
     ["Properties70", [], "", [
         ["P", ["d", "Compound", "", ""], "SSSS", []]]]]]]],
         ["P", ["d|X", "Number", "", "A", 2.227995], "SSSSD", []],
         ["P", ["d|Y", "Number", "", "A", 3.0238389999999997], "SSSSD", []],
         ["P", ["d|Z", "Number", "", "A", -1.49012e-08], "SSSSD", []]]]]]

This CurveNode, once linked to a Model’s Lcl Translation property, will control its location. Note that in theory, a same CurveNode can control several properties of several objects (but we won’t mess with that either in Blender!).

Note how it uses a unique Compound property, with one item per channel of the target property. So for translation & co, you’ll have three, for “factor” properties you’ll only have one, etc. Those compound properties must be of the same type as the target ones, their values are presumed to be “default” ones… The naming scheme of those props remains a bit fuzzy currently.

Finally, an AnimCurve is linked to each prop of the CurveNode:

 ["AnimationCurve", [924545958, "::AnimCurve", ""], "LSS", [
     ["Default", [-120.30426094607161], "D", []],
     ["KeyVer", [4008], "I", []],
     ["KeyTime", [[1847446320, 3694892640, 5542338960, 7389785280, 9237231600, 11084677920, 12932124240, 14779570560, 16627016880, 18474463200, 20321909520, 22169355840, 24016802160, 25864248480, 27711694800, 29559141120, 31406587440, 33254033760, 35101480080, 36948926400, 38796372720, 90524869680]], "l", []],
     ["KeyValueFloat", [[-90.00000762939453, -90.16700744628906, -90.67182159423828, -91.51591491699219, -92.69371032714844, -94.19062042236328, -95.98118591308594, -98.02799224853516, -100.28105926513672, -102.67914581298828, -105.1521224975586, -107.62510681152344, -110.02317810058594, -112.27626037597656, -114.32304382324219, -116.11365509033203, -117.61054229736328, -118.78833770751953, -119.63243103027344, -120.13725280761719, -120.30426025390625, -120.30426025390625]], "f", []],
     ["KeyAttrFlags", [[24840]], "i", []],
     ["KeyAttrDataFloat", [[0.0, 0.0, 9.419963346924634e-30, 0.0]], "f", []],
     ["KeyAttrRefCount", [[22]], "i", []]]]

An anim curve has a default value (presumably always a double?), a version, like most of other "Data" elements in FBX, an array of time indices (in KTime format), an array of values (float ones, sigh), both defining the basis of keyframes. Then you have three KeyAttr… elements. First one is an array of flags, gathering a whole bunch of behavior for the keyframes (types of interpolation, etc.). Second one is an array of float values, four per defined flags, which probably control things like TBC and other advanced keyframe features. Finally, the RefCount array contains, for each flag (and matching float data), the number of keys using it. So if you use the same flag for all keys, you can only write it one, and set here the number of keys in your curve. I you use (flag/floatdata) A for first ten keys, then (flag/floatdata) B for five keys, then again (flag/floatdata) A for ten keys, you’ll have something like that (for twenty five keys):

 ["KeyAttrFlags", [[A, B, A]], "i", []],
 ["KeyAttrDataFloat", [[A1, A2, A3, A4, B1, B2, B3, B4, A1, A2, A3, A4]], "f", []],
 ["KeyAttrRefCount", [[10, 5, 10]], "i", []]]]

Now here comes the funny little trick: for binary format, AnimationStack and AnimationLayer elements need the 13-NULL sentinel, even when they do have properties but no elements (unlike any other type of element in bin FBX, as far as we know).