Task 218: .FBX File Format

Task 218: .FBX File Format

FBX File Format Specifications

The .FBX (Filmbox) format is a proprietary binary (or ASCII) format developed by Autodesk for 3D scene interchange, supporting models, animations, and hierarchies. The binary format is the most common and is what the task focuses on. Specifications were sourced from reverse-engineered documentation, as Autodesk does not publicly release official binary specs (only the SDK for programmatic access). Key references include the Blender Developers Blog for core structure details and an unofficial GitHub Gist for version-specific nuances. The format uses little-endian byte order by default (indicated in the header). Newer versions (≥7500) use 64-bit integers for offsets/counts, but the code below assumes <7500 for simplicity (common in samples); extend as needed.

1. List of All Properties Intrinsic to the File Format

These are the core structural elements defining the binary layout, independent of content (e.g., 3D data). The format is a tree of nodes, each with properties and optional children. Parsing is offset-based for skipping unknowns.

  • Header Magic String: Fixed 21-byte ASCII "Kaydara FBX Binary  " followed by null terminator (\0).
  • Header Unknown Byte 1: 1 byte, typically 0x1A (purpose unclear, consistent across files).
  • Header Endianness Byte: 1 byte, 0x00 (little-endian) or 0x01 (big-endian).
  • Header Version: 4-byte unsigned 32-bit integer (e.g., 7300 for v7.3, 7500 for v7.5).
  • Node End Offset (per node): 4-byte unsigned 32-bit integer (absolute file position after this node's end; used for skipping).
  • Node Num Properties: 4-byte unsigned 32-bit integer (count of direct properties; excludes children).
  • Node Property List Length: 4-byte unsigned 32-bit integer (total byte size of all properties).
  • Node Name Length: 1-byte unsigned 8-bit integer (length of node name in bytes; 0 for root).
  • Node Name: Variable-length byte string (no null terminator; UTF-8 encoded).
  • Node Properties: Sequence of property records (see below; total size matches Property List Length).
  • Node Nested Children: Optional sequence of child node records (parsed recursively until End Offset reached).
  • Node Terminator (NULL Record): Fixed 13 zero bytes (marks end of node; always present after properties/children).

Property Record Properties (each starts after previous; no padding):

  • Type Code: 1-byte char (defines data type; see enums below).
  • Primitive Data (for non-array/special types): Immediate binary value in little-endian.
  • 'Y': 2-byte signed 16-bit integer.
  • 'C': 1-byte unsigned 8-bit boolean (0=false, 1=true; LSB only).
  • 'I': 4-byte signed 32-bit integer.
  • 'F': 4-byte IEEE 754 single-precision float.
  • 'D': 8-byte IEEE 754 double-precision float.
  • 'L': 8-byte signed 64-bit integer.
  • Array Data (for 'f', 'd', 'l', 'i', 'b' types):
  • Array Length: 4-byte unsigned 32-bit integer (element count).
  • Encoding: 4-byte unsigned 32-bit integer (0=raw, 1=zlib deflate compressed; others rare/unobserved).
  • Compressed Length: 4-byte unsigned 32-bit integer (byte size of data; full if encoding=0).
  • Data Bytes: Variable (length * element size if raw; compressed buffer otherwise).
  • 'f': float32 elements.
  • 'd': float64 elements.
  • 'l': int64 elements.
  • 'i': int32 elements.
  • 'b': uint8 boolean elements (0/1).
  • String Data ('S'):
  • Length: 4-byte unsigned 32-bit integer (byte count).
  • String Bytes: Variable UTF-8 bytes (no null terminator).
  • Raw Binary Data ('R'):
  • Length: 4-byte unsigned 32-bit integer (byte count).
  • Raw Bytes: Variable opaque bytes.

3. Ghost Blog Embedded HTML JavaScript (Drag-and-Drop Parser)

Embed this as a Ghost blog post HTML card (e.g., in a post body). It creates a drag-and-drop zone. On drop, parses the .FBX binary (assumes little-endian, v<7500, no compression for arrays; decompresses with pako if needed—include via CDN). Dumps header/node properties to screen. For full zlib, add <script src="https://cdnjs.cloudflare.com/ajax/libs/pako/2.1.0/pako.min.js"></script>.

Drag and drop an .FBX file here to parse properties.

4. Python Class

Uses struct for binary parsing. Assumes little-endian, v<7500, encoding=0 (raw arrays; extend with zlib for compression). Reads full structure recursively, prints to console. Write stub creates minimal valid FBX with header/root.

import struct
import zlib  # For compression if needed

class FBXParser:
    def __init__(self, filename):
        with open(filename, 'rb') as f:
            self.data = f.read()
        self.pos = 0

    def read_uint8(self):
        v = struct.unpack_from('<B', self.data, self.pos)[0]
        self.pos += 1
        return v

    def read_uint32(self):
        v = struct.unpack_from('<I', self.data, self.pos)[0]
        self.pos += 4
        return v

    def read_int32(self):
        v = struct.unpack_from('<i', self.data, self.pos)[0]
        self.pos += 4
        return v

    def read_float32(self):
        v = struct.unpack_from('<f', self.data, self.pos)[0]
        self.pos += 4
        return v

    def read_float64(self):
        v = struct.unpack_from('<d', self.data, self.pos)[0]
        self.pos += 8
        return v

    def read_int64(self):
        v = struct.unpack_from('<q', self.data, self.pos)[0]
        self.pos += 8
        return v

    def read_string(self, length):
        s = self.data[self.pos:self.pos + length].decode('utf-8')
        self.pos += length
        return s

    def read_bytes(self, length):
        b = self.data[self.pos:self.pos + length]
        self.pos += length
        return b

    def parse_property(self):
        type_code = chr(self.read_uint8())
        value = None
        if type_code in 'YCIFDL':
            if type_code == 'Y': value = struct.unpack_from('<h', self.data, self.pos)[0]; self.pos += 2
            elif type_code == 'C': value = self.read_uint8() != 0; self.pos -= 1  # Already read
            elif type_code == 'I': value = self.read_int32()
            elif type_code == 'F': value = self.read_float32()
            elif type_code == 'D': value = self.read_float64()
            elif type_code == 'L': value = self.read_int64()
        elif type_code == 'S':
            length = self.read_uint32()
            value = self.read_string(length)
        elif type_code == 'R':
            length = self.read_uint32()
            value = self.read_bytes(length)
        elif type_code in 'fdlib':
            length = self.read_uint32()
            encoding = self.read_uint32()
            comp_len = self.read_uint32()
            data_len = length * (4 if type_code in 'fi' else 8 if type_code in 'dl' else 1)
            raw_data = self.read_bytes(comp_len if encoding == 1 else data_len)
            if encoding == 1:
                try:
                    value = zlib.decompress(raw_data)
                except:
                    value = b'[decompress failed]'
            else:
                value = raw_data
            value = { 'len': length, 'encoding': encoding, 'data_preview': value[:10] }
        else:
            value = '[unknown]'
        return { 'type': type_code, 'value': value }

    def parse_node(self, name='Root', indent=''):
        end_offset = self.read_uint32()
        num_props = self.read_uint32()
        prop_len = self.read_uint32()
        name_len = self.read_uint8()
        node_name = self.read_string(name_len) if name_len else name
        props = [self.parse_property() for _ in range(num_props)]
        children_str = ''
        start_children = self.pos
        while self.pos + 13 < end_offset:
            children_str += self.parse_node('', indent + '  ') + '\n'
        # Skip to end
        self.pos = end_offset - 13
        self.read_bytes(13)  # NULL record
        print(f"{indent}Node: {node_name}")
        print(f"{indent}  EndOffset: {end_offset}")
        print(f"{indent}  NumProps: {num_props}")
        print(f"{indent}  PropLen: {prop_len}")
        print(f"{indent}  Properties: {props}")
        print(children_str)
        return ''

    def parse_header(self):
        magic = self.read_string(21)
        unk1 = self.read_uint8()
        endian = self.read_uint8()
        version = self.read_uint32()
        print(f"Header Magic: {magic}")
        print(f"Unknown1: 0x{unk1:02x}")
        print(f"Endian: {'Little' if endian == 0 else 'Big'}")
        print(f"Version: {version}")
        self.pos = 27  # After header

    def read_and_print(self):
        self.parse_header()
        self.parse_node()

    def write(self, filename, version=7300):  # Simple writer: minimal empty FBX
        with open(filename, 'wb') as f:
            f.write(b'Kaydara FBX Binary  \x00\x1A\x00')
            f.write(struct.pack('<I', version))
            # Root node: empty
            root_end = 27 + 4 + 4 + 4 + 1 + 0 + 0 + 13  # Calc
            f.write(struct.pack('<III B', root_end, 0, 0, 0))  # End, numprops=0, proplen=0, namelen=0
            f.write(b'\x00' * 13)  # NULL

# Usage
# parser = FBXParser('cube.fbx')
# parser.read_and_print()
# parser.write('minimal.fbx')

5. Java Class

Uses ByteBuffer for parsing (little-endian). Similar to Python; assumes no compression. Prints to console. Write creates minimal FBX.

import java.io.*;
import java.nio.*;
import java.nio.channels.FileChannel;
import java.util.*;

public class FBXParser {
    private ByteBuffer buffer;
    private int pos = 0;

    public FBXParser(String filename) throws IOException {
        RandomAccessFile file = new RandomAccessFile(filename, "r");
        buffer = file.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, file.length());
        buffer.order(ByteOrder.LITTLE_ENDIAN);
    }

    private short readUint8() { short v = (short) buffer.get(pos); pos++; return v; }
    private int readUint32() { int v = buffer.getInt(pos); pos += 4; return v; }
    private int readInt32() { int v = buffer.getInt(pos); pos += 4; return v; }
    private float readFloat32() { float v = buffer.getFloat(pos); pos += 4; return v; }
    private double readFloat64() { double v = buffer.getDouble(pos); pos += 8; return v; }
    private long readInt64() { long v = buffer.getLong(pos); pos += 8; return v; }
    private String readString(int length) {
        byte[] bytes = new byte[length];
        buffer.position(pos);
        buffer.get(bytes);
        pos += length;
        return new String(bytes);
    }
    private byte[] readBytes(int length) {
        byte[] bytes = new byte[length];
        buffer.position(pos);
        buffer.get(bytes);
        pos += length;
        return bytes;
    }

    private Map<String, Object> parseProperty() {
        String type = String.valueOf((char) readUint8());
        Object value = null;
        switch (type) {
            case "Y": value = buffer.getShort(pos); pos += 2; break;
            case "C": value = readUint8() != 0; pos -= 1; break;
            case "I": value = readInt32(); break;
            case "F": value = readFloat32(); break;
            case "D": value = readFloat64(); break;
            case "L": value = readInt64(); break;
            case "S": {
                int len = readUint32();
                value = readString(len);
                break;
            }
            case "R": {
                int len = readUint32();
                value = readBytes(len);
                break;
            }
            case "f":
            case "d":
            case "l":
            case "i":
            case "b": {
                int len = readUint32();
                int enc = readUint32();
                int compLen = readUint32();
                int dataLen = len * (type.equals("f") || type.equals("i") ? 4 : type.equals("d") || type.equals("l") ? 8 : 1);
                byte[] data = readBytes(enc == 1 ? compLen : dataLen);
                // TODO: Decompress if enc==1 using Inflater
                value = Map.of("len", len, "enc", enc, "data", Arrays.copyOf(data, Math.min(10, data.length)));
                break;
            }
            default: value = "[unknown]";
        }
        return Map.of("type", type, "value", value);
    }

    private void parseNode(String name, String indent) {
        int endOffset = readUint32();
        int numProps = readUint32();
        int propLen = readUint32();
        int nameLen = readUint8();
        String nodeName = nameLen > 0 ? readString(nameLen) : name;
        List<Map<String, Object>> props = new ArrayList<>();
        for (int i = 0; i < numProps; i++) {
            props.add(parseProperty());
        }
        // Simplified children skip
        pos = endOffset - 13;
        buffer.position(pos);
        byte[] nullRec = new byte[13];
        buffer.get(nullRec);
        System.out.println(indent + "Node: " + nodeName);
        System.out.println(indent + "  EndOffset: " + endOffset);
        System.out.println(indent + "  NumProps: " + numProps);
        System.out.println(indent + "  PropLen: " + propLen);
        System.out.println(indent + "  Properties: " + props);
    }

    public void parseHeader() {
        byte[] magicBytes = new byte[21];
        buffer.position(0);
        buffer.get(magicBytes);
        String magic = new String(magicBytes);
        int unk1 = readUint8();
        int endian = readUint8();
        int version = readUint32();
        System.out.println("Header Magic: " + magic);
        System.out.println("Unknown1: 0x" + Integer.toHexString(unk1));
        System.out.println("Endian: " + (endian == 0 ? "Little" : "Big"));
        System.out.println("Version: " + version);
        pos = 27;
        buffer.position(pos);
    }

    public void readAndPrint() {
        parseHeader();
        parseNode("Root", "");
    }

    public void write(String filename, int version) throws IOException {
        try (FileOutputStream fos = new FileOutputStream(filename);
             FileChannel fc = fos.getChannel()) {
            ByteBuffer bb = ByteBuffer.allocate(50).order(ByteOrder.LITTLE_ENDIAN);
            bb.put("Kaydara FBX Binary  \0\x1A\0".getBytes());
            bb.putInt(version);
            int rootEnd = 27 + 13;  // Minimal
            bb.putInt(rootEnd);
            bb.putInt(0);  // Num props
            bb.putInt(0);  // Prop len
            bb.put((byte) 0);  // Name len
            bb.put(new byte[13]);  // NULL
            bb.flip();
            fc.write(bb);
        }
    }

    // Usage
    // FBXParser parser = new FBXParser("cube.fbx");
    // parser.readAndPrint();
    // parser.write("minimal.fbx", 7300);
}

6. JavaScript Class (Node.js/Browser)

Uses Buffer (Node) or ArrayBuffer (browser). For browser, adapt FileReader. Assumes no compression. Prints to console. Write creates minimal.

class FBXParser {
  constructor(buffer) {
    this.buffer = Buffer.isBuffer(buffer) ? buffer : new Uint8Array(buffer);
    this.pos = 0;
  }

  readUint8() {
    const v = this.buffer[this.pos];
    this.pos += 1;
    return v;
  }

  readUint32() {
    const v = this.buffer.readUInt32LE(this.pos);
    this.pos += 4;
    return v;
  }

  readInt32() {
    const v = this.buffer.readInt32LE(this.pos);
    this.pos += 4;
    return v;
  }

  readFloat32() {
    const v = this.buffer.readFloatLE(this.pos);
    this.pos += 4;
    return v;
  }

  readFloat64() {
    const v = this.buffer.readDoubleLE(this.pos);
    this.pos += 8;
    return v;
  }

  readInt64() {
    const hi = this.readUint32();
    const lo = this.readUint32();
    return (BigInt(hi) << 32n) | BigInt(lo);
  }

  readString(length) {
    const str = this.buffer.toString('utf8', this.pos, this.pos + length);
    this.pos += length;
    return str;
  }

  readBytes(length) {
    const bytes = Buffer.from(this.buffer.slice(this.pos, this.pos + length));
    this.pos += length;
    return bytes;
  }

  parseProperty() {
    const typeCode = String.fromCharCode(this.readUint8());
    let value;
    switch (typeCode) {
      case 'Y':
        value = this.buffer.readInt16LE(this.pos); this.pos += 2;
        break;
      case 'C':
        value = this.readUint8() !== 0; this.pos -= 1;
        break;
      case 'I': value = this.readInt32(); break;
      case 'F': value = this.readFloat32(); break;
      case 'D': value = this.readFloat64(); break;
      case 'L': value = this.readInt64(); break;
      case 'S': {
        const len = this.readUint32();
        value = this.readString(len);
        break;
      }
      case 'R': {
        const len = this.readUint32();
        value = this.readBytes(len);
        break;
      }
      case 'f':
      case 'd':
      case 'l':
      case 'i':
      case 'b': {
        const len = this.readUint32();
        const enc = this.readUint32();
        const compLen = this.readUint32();
        const elemSize = typeCode === 'f' || typeCode === 'i' ? 4 : typeCode === 'd' || typeCode === 'l' ? 8 : 1;
        const dataLen = len * elemSize;
        let data = this.readBytes(enc === 1 ? compLen : dataLen);
        if (enc === 1) {
          // Require zlib-sync or pako
          try { data = require('zlib').inflateSync(data); } catch (e) { data = Buffer.from('[decompress failed]'); }
        }
        value = { len, enc, data: data.slice(0, 10).toString('hex') + (data.length > 10 ? '...' : '') };
        break;
      }
      default: value = '[unknown]';
    }
    return { type: typeCode, value };
  }

  parseNode(name = 'Root', indent = '') {
    const endOffset = this.readUint32();
    const numProps = this.readUint32();
    const propLen = this.readUint32();
    const nameLen = this.readUint8();
    const nodeName = nameLen ? this.readString(nameLen) : name;
    const props = [];
    for (let i = 0; i < numProps; i++) {
      props.push(this.parseProperty());
    }
    let childrenStr = '';
    const startPos = this.pos;
    while (this.pos + 13 < endOffset) {
      childrenStr += this.parseNode('', indent + '  ') + '\n';
    }
    this.pos = endOffset - 13;
    this.readBytes(13);  // NULL
    console.log(`${indent}Node: ${nodeName}`);
    console.log(`${indent}  EndOffset: ${endOffset}`);
    console.log(`${indent}  NumProps: ${numProps}`);
    console.log(`${indent}  PropLen: ${propLen}`);
    console.log(`${indent}  Properties:`, props);
    console.log(childrenStr);
    return '';
  }

  parseHeader() {
    const magic = this.readString(21);
    const unk1 = this.readUint8();
    const endian = this.readUint8();
    const version = this.readUint32();
    console.log(`Header Magic: ${magic}`);
    console.log(`Unknown1: 0x${unk1.toString(16)}`);
    console.log(`Endian: ${endian === 0 ? 'Little' : 'Big'}`);
    console.log(`Version: ${version}`);
    this.pos = 27;
  }

  readAndPrint() {
    this.parseHeader();
    this.parseNode();
  }

  write(filename, version = 7300) {
    const fs = require('fs');
    const buf = Buffer.alloc(50);
    buf.write('Kaydara FBX Binary  \0\x1A\0', 0);
    buf.writeUInt32LE(version, 23);
    const rootEnd = 27 + 13;
    buf.writeUInt32LE(rootEnd, 27);
    buf.writeUInt32LE(0, 31);  // Num props
    buf.writeUInt32LE(0, 35);  // Prop len
    buf.writeUInt8(0, 39);  // Name len
    buf.fill(0, 40, 53);  // NULL
    fs.writeFileSync(filename, buf.slice(0, 53));
  }
}

// Usage (Node.js)
// const parser = new FBXParser(fs.readFileSync('cube.fbx'));
// parser.readAndPrint();
// parser.write('minimal.fbx');

7. C Class (Struct-Based)

Uses FILE* and fread. Assumes little-endian (use macros for portability). Basic read/print; compression stub (use zlib.h). Write minimal.

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>

typedef struct {
    uint8_t* data;
    size_t pos;
    size_t size;
} FBXBuffer;

typedef struct {
    char type;
    void* value;  // Union or variant in real impl
} FBXProperty;

FBXBuffer* fbx_load(const char* filename) {
    FILE* f = fopen(filename, "rb");
    if (!f) return NULL;
    fseek(f, 0, SEEK_END);
    size_t len = ftell(f);
    fseek(f, 0, SEEK_SET);
    uint8_t* buf = malloc(len);
    fread(buf, 1, len, f);
    fclose(f);
    FBXBuffer* b = malloc(sizeof(FBXBuffer));
    b->data = buf;
    b->pos = 0;
    b->size = len;
    return b;
}

uint8_t fbx_read_uint8(FBXBuffer* b) {
    uint8_t v = b->data[b->pos++];
    return v;
}

uint32_t fbx_read_uint32(FBXBuffer* b) {
    uint32_t v = *(uint32_t*)(b->data + b->pos);
    b->pos += 4;
    return __builtin_bswap32(v);  // To LE if needed; assume host LE
}

int32_t fbx_read_int32(FBXBuffer* b) {
    int32_t v = *(int32_t*)(b->data + b->pos);
    b->pos += 4;
    return __builtin_bswap32(v);
}

float fbx_read_float32(FBXBuffer* b) {
    float v;
    memcpy(&v, b->data + b->pos, 4);
    b->pos += 4;
    return v;  // Assume LE
}

double fbx_read_float64(FBXBuffer* b) {
    double v;
    memcpy(&v, b->data + b->pos, 8);
    b->pos += 8;
    return v;
}

int64_t fbx_read_int64(FBXBuffer* b) {
    int64_t v;
    memcpy(&v, b->data + b->pos, 8);
    b->pos += 8;
    return v;
}

char* fbx_read_string(FBXBuffer* b, uint32_t len) {
    char* s = malloc(len + 1);
    memcpy(s, b->data + b->pos, len);
    s[len] = '\0';
    b->pos += len;
    return s;
}

uint8_t* fbx_read_bytes(FBXBuffer* b, uint32_t len) {
    uint8_t* bytes = malloc(len);
    memcpy(bytes, b->data + b->pos, len);
    b->pos += len;
    return bytes;
}

void fbx_parse_property(FBXBuffer* b, FBXProperty* prop) {
    prop->type = (char)fbx_read_uint8(b);
    // Simplified: print type and skip data bytes based on type (implement full union)
    switch (prop->type) {
        case 'Y': prop->value = malloc(2); memcpy(prop->value, b->data + b->pos, 2); b->pos += 2; break;
        case 'C': /* 1 byte already read */ break;
        case 'I': prop->value = malloc(4); memcpy(prop->value, b->data + b->pos, 4); b->pos += 4; break;
        // Add F, D, L similarly
        case 'S':
        case 'R': {
            uint32_t len = fbx_read_uint32(b);
            prop->value = fbx_read_bytes(b, len);
            break;
        }
        case 'f': case 'd': case 'l': case 'i': case 'b': {
            uint32_t len = fbx_read_uint32(b);
            uint32_t enc = fbx_read_uint32(b);
            uint32_t comp_len = fbx_read_uint32(b);
            // Stub: skip data
            b->pos += (enc == 0 ? len * (prop->type == 'f' || prop->type == 'i' ? 4 : 8) : comp_len);
            prop->value = malloc(4);  // Preview
            break;
        }
        default: prop->value = NULL;
    }
    printf("Property type: %c\n", prop->type);
}

void fbx_parse_node(FBXBuffer* b, const char* name, const char* indent) {
    uint32_t end_offset = fbx_read_uint32(b);
    uint32_t num_props = fbx_read_uint32(b);
    uint32_t prop_len = fbx_read_uint32(b);
    uint8_t name_len = fbx_read_uint8(b);
    char* node_name = name_len ? fbx_read_string(b, name_len) : strdup(name);
    printf("%sNode: %s\n", indent, node_name);
    printf("%s  EndOffset: %u\n", indent, end_offset);
    printf("%s  NumProps: %u\n", indent, num_props);
    printf("%s  PropLen: %u\n", indent, prop_len);
    for (uint32_t i = 0; i < num_props; i++) {
        FBXProperty prop = {0};
        fbx_parse_property(b, &prop);
        // Free prop.value if needed
    }
    // Skip children and NULL (13 bytes)
    b->pos = end_offset - 13;
    b->pos += 13;
    free(node_name);
}

void fbx_parse_header(FBXBuffer* b) {
    char magic[22];
    memcpy(magic, b->data, 21);
    magic[21] = '\0';
    b->pos = 21;
    uint8_t unk1 = fbx_read_uint8(b);
    uint8_t endian = fbx_read_uint8(b);
    uint32_t version = fbx_read_uint32(b);
    printf("Header Magic: %s\n", magic);
    printf("Unknown1: 0x%02x\n", unk1);
    printf("Endian: %s\n", endian == 0 ? "Little" : "Big");
    printf("Version: %u\n", version);
    b->pos = 27;
}

void fbx_read_and_print(FBXBuffer* b) {
    fbx_parse_header(b);
    fbx_parse_node(b, "Root", "");
}

void fbx_write(const char* filename, uint32_t version) {
    FILE* f = fopen(filename, "wb");
    if (!f) return;
    const char* header = "Kaydara FBX Binary  \0\x1A\0";
    fwrite(header, 1, 25, f);  // 21 +1 null +2 unk +1 for version start
    fwrite(&version, 4, 1, f);
    uint32_t root_end = 27 + 13;
    fwrite(&root_end, 4, 1, f);
    uint32_t zero = 0;
    fwrite(&zero, 4, 1, f);  // Num props
    fwrite(&zero, 4, 1, f);  // Prop len
    uint8_t name_len = 0;
    fwrite(&name_len, 1, 1, f);
    uint8_t null_rec[13] = {0};
    fwrite(null_rec, 1, 13, f);
    fclose(f);
}

int main() {
    FBXBuffer* b = fbx_load("cube.fbx");
    if (b) {
        fbx_read_and_print(b);
        // fbx_write("minimal.fbx", 7300);
        free(b->data);
        free(b);
    }
    return 0;
}