Skip to content

Latest commit

 

History

122 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

fbxtool

Inspect, render, edit and export 3D model files, with no dependencies and without the Autodesk FBX SDK.

Format Support
FBX binary and ASCII, 6.x and 7.x inspect, render, export
Wavefront OBJ (+ .mtl) inspect, render
COLLADA .dae inspect, render, materials from the *.materials.json beside it — the format BeamNG.drive ships its cars in
glTF 2.0 (.gltf, .glb) inspect, render, import, export; Draco decompressed
Blender .blend inspect, render (MVert/MPoly/MLoop layout)
3ds Max .max inspect, render — Editable Poly and Editable Mesh, materials, textures
Assetto Corsa .kn5 inspect, render, skins; embedded DDS decoded

Every reader produces the same record tree, so all options, the analysis and the viewer apply to all formats unchanged.

Install

pip install .                  # installs fbxinfo and kn5togltf
python3 -m fbxtool scene.fbx   # or run from a checkout, no install

Python 3.9+, standard library only (struct, zlib, mmap, json).

Command line

fbxinfo FILE [FILE ...] [options]

sections:
  --tree              print the raw record tree
  --depth N           limit --tree to N levels
  --props             print every property value, not just a preview
  --objects           list the Objects section entry by entry
  --connections       list connections individually, with names resolved
  --no-hierarchy      skip the reconstructed scene hierarchy
  -a, --all           everything: --tree --props --objects --connections
  --brief             one summary line per file

output:
  --json              emit JSON instead of text
  --indent N          JSON indent (default: 2, 0 for compact)
  --max-list N        cap listed objects/connections/hierarchy rows (default: 40)
  --decode-arrays     decode (and inflate) array payloads so values can be shown
  --max-array N       keep at most N values per decoded array (0 for all)
  --ascii             draw with plain ASCII instead of box-drawing characters

fbxinfo is the only entry point. Box drawing falls back to ASCII automatically when the output encoding cannot represent it; --ascii forces it everywhere.

What gets reported

Section Contents
File encoding, size, version stamp and product, 32/64-bit node offsets, footer and its version stamp
Metadata creator, creation timestamp, file id, encryption type, originating and last-saving application
Global settings axis system (up/front/right with sign), unit scale and name, time mode, time span
Record structure total records, nesting depth, per-section counts, property histogram by type, stored array bytes
Definitions declared object count, per-ObjectType counts, property templates
Objects count by Type (SubType), vertex and polygon counts, shading model, texture paths, cluster weights, curve key counts
Scene hierarchy transform tree rebuilt from Connections, with geometry and materials as attachments
Connections totals by kind (OO, OP), optionally each connection with both endpoints resolved
Animation stacks, layers, curve and curve-node counts, stack durations, 6.x takes; Assetto Corsa .ksanim clips beside a car read and played
Warnings structural inconsistencies found while reading

--tree prints the container itself. Array properties are summarised as *24 [d] deflate 87 B; --decode-arrays also shows leading values.

Format support

FBX

  • Binary and ASCII, versions 6000 to 7700 and beyond (unlisted stamps described generically).
  • 6.x legacy layout: objects addressed by Class::Name, mesh stored on the Model, numbers written one property each, Property records with three strings, Connect: records.
  • 7.x: UID addressing, Geometry records, array properties, P records.
  • Node offsets widen to 64 bits at 7500.
  • Full local-matrix composition: T * Roff * Rp * Rpre * R * Rpost⁻¹ * Rp⁻¹ * Soff * Sp * S * Sp⁻¹, node RotationOrder, geometric transforms, negative scale.
  • UV and normal layers in Direct and IndexToDirect, per polygon vertex or per control point.
  • Per-polygon material indices resolved through the connection graph.
  • Encrypted files reported, not decrypted.

Wavefront OBJ

  • v / vn / vt / f, all five face-index syntaxes including negative indices.
  • usemtl as a per-polygon material layer, map_Kd as a texture reference.
  • o and g kept as separate parts; vertex pools gathered and renumbered per part.
  • .mtl read from beside the file, or supplied separately.
  • d and Tr for transparency.

COLLADA .dae

  • COLLADA 1.4.1, whichever prefix the file binds the schema to.
  • <polylist> and <triangles>; vcount + p become an FBX polygon run with each polygon's last corner complemented.
  • NORMAL and TEXCOORD sources read at the offsets their inputs state, as IndexToDirect layers; the <vertices> indirection followed to the position source.
  • Texture coordinates not flipped — COLLADA measures V upwards, as FBX and OBJ do and unlike glTF.
  • <accessor> strides read rather than assumed.
  • Nodes placed by <matrix>, composed down the tree and decomposed to translation, Euler rotation and scale. The matrix is row-major acting on column vectors, so the translation is the last column; a negative determinant is kept as a negative scale.
  • <up_axis> and <unit> become GlobalSettings.
  • Materials from profile_COMMON — lambert, phong, blinn and constant — for a flat diffuse colour. Each part is connected only to the materials its own primitives ask for.
  • Materials from the file beside the model. A BeamNG .dae carries a lambert stub and names one image for a car's eighty-odd; what its surfaces actually are lives in a *.materials.json in the same folder, read for both of the game's generations — the newer baseColorMap/roughnessFactor/metallicFactor/clearCoatFactor, and the older colorMap. Matched on mapTo, then name, then with Blender's _001 duplicate suffix dropped.
  • Base colour, normal and ambient-occlusion maps become texture records; roughness, metalness and base colour go on under a vendor prefix, and a clear coat becomes the coat the shader already draws.
  • The sidecar names the picture its artist authored — bolide_main_b.color.png — where the game ships the one it converted, bolide_main_b.color.DDS, so a supplied image is matched on its name without the extension where the exact name is not there.
  • Of 2,027 materials across the 88 cars that ship a sidecar, 537 are dressed from it, 341 are lights whose entries state nothing at all, and 1,149 are shared names defined in the game's own common package rather than beside the car.
  • Not read: library_animations and library_controllers, neither of which is geometry; the .cdae beside a BeamNG car, which is the game's own compiled cache of the same model; the separate roughnessMap and metallicMap (the viewer wants the two in one picture, as glTF packs them); opacityMap; and the layers a material states beyond its first stage.

glTF 2.0

  • .glb and .gltf, with the .bin read from beside it or supplied separately.
  • Reported: container, generator, meshes, primitives, triangles, nodes, materials, images, buffer views, accessors, component types, extensions.
  • Interleaved attributes behind a byteStride; 8, 16 and 32-bit indices; sparse accessors; primitives with no indices.
  • Nodes placed by matrix or by quaternion.
  • Metallic-roughness mapped to material properties; V flipped on the way in and out.
  • Extensions read: KHR_materials_specular, KHR_materials_ior, KHR_materials_transmission, KHR_materials_pbrSpecularGlossiness, KHR_materials_clearcoat, KHR_texture_transform, KHR_draco_mesh_compression, KHR_texture_basisu.
  • EmissiveColor, AlphaMode and AlphaCutoff read.
  • Transparency kept as the two statements it is: an alpha for how much of the surface is there, KHR_materials_transmission for how much of the light goes through what is. Multiplied only where something is drawn, and written back apart — a windscreen stating a quarter alpha under full transmission goes out stating both, rather than as a sheet at alpha nought.
  • Every map becomes its own Texture record under the FBX property name for its slot, with the sampler's wrap modes.
  • Where a map is read on its picture, for a file that addresses one through coordinates that are not the map's own. A Sketchfab car's body is a single atlas its paint occupies a twentieth of, with UVs running 0 to 20 across it and KHR_texture_transform stating the scale that brings them back — 15 of the 144 models to hand state one, and without it the atlas tiles twenty times over every panel and the car draws as noise rather than as its livery. Restated the way V runs here rather than copied across, carried on the Texture record as the Scaling and Translation an FBX texture states, and written back out as the extension again.
  • The clear coat over the paint. A car's body is two surfaces — a coloured base with a sharp clear mirror over it — and read as the base alone it draws as flat plastic. The extension says how much coat there is and fixes what it reflects at the 4% of a clear lacquer; both are kept, since a coat carried as the factor alone comes back a mirror.

Blender .blend

  • Reported: Blender version, pointer size, endianness, compression, file-block counts by code, SDNA, every datablock with name and type.
  • Meshes from MVert, MLoop, MPoly and MLoopUV; materials from the mesh slot table.
  • Every offset and struct size computed from the file's own SDNA.
  • Gzip-compressed files unwrapped; Zstandard detected and reported.
  • Releases that replaced these arrays with generic attributes (3.6+, 4.0) detected and reported rather than guessed at.

3ds Max .max

  • Microsoft compound file container; per-stream gzip (2022+) undone; files truncated mid-sector read to the end.
  • Geometry: Editable Poly (0x0100 vertices, 0x011a n-gon faces, 0x0128/0x012b UVs) and Editable Mesh (0x0914, 0x0912, 0x2394/0x2396).
  • A vertex array is counted in slots, not points: a flag word carrying 0x02 is a run of slots nobody filled, left where a vertex was deleted, and the faces after it name the numbers the run leaves behind.
  • A map vertex is a point — u, v and w — and the texture coordinate is the first two of them.
  • Node names, class table, plugins, parent links (0x0960), Dummy helpers.
  • Placement from Position/Rotation/Scale controllers down to per-axis float controllers, plus the node-to-mesh offset.
  • Smoothing groups (full 32-bit word) and material ids read as separate fields.
  • Symmetry modifier applied — mirror plane, weld threshold, reversed winding, seam faces not duplicated.
  • TurboSmooth not run; the viewer's own subdivision covers it.
  • Materials: Standard (Blinn, Phong, Oren-Nayar-Blinn, Anisotropic), VRayMtl, CoronaMtl / CoronaLegacyMtl, Multi/Sub-Object, Blend, VRayBlendMtl, CoronaLayeredMtl, CoronaColor, Falloff.
  • Diffuse, reflection, refraction (as the opposite of opacity), glossiness, reflection index of refraction, clear coat.
  • Material names read from 0x5431, 0x0FA0 and 0x4000.
  • Per-class texture slot numbering for VRayMtl and CoronaMtl; unknown classes fall back to first-colour-is-diffuse.
  • Texture file names resolved through FileAssetMetaData2 / FileAssetMetaData3.
  • Not read: modifier stack beyond Symmetry, edge creases, the Slice option, Shell, uncollapsed primitives (Box, Line) and plugin classes (counted and named only).

Assetto Corsa .kn5

  • Both versions in the wild; texture table, material table and node tree read straight through.
  • Reported: scene node/mesh counts and depth, geometry counts, inactive nodes and hidden meshes, material count, metals, dimmed materials, shader names, embedded texture count and size.
  • Interleaved vertices (44 bytes, 76 skinned), unpacked lazily.
  • V stored negated and undone on the way in, leaving it upwards in [0, 1] as the other readers write it; Direct3D row-major transforms decomposed, negative determinant kept as negative scale.
  • Materials: txDiffuse as albedo, ksSpecularEXP as shininess exponent, ksSpecular as highlight strength, fresnelC / fresnelEXP / fresnelMaxLevel as a Schlick Fresnel with a ceiling. Every named parameter also carried under its own name.
  • ksAmbient and ksDiffuse read as how much of the light a material takes, against a 0.5/0.6 baseline.
  • Metalness inferred from reflectance at normal incidence (dielectrics below ~17%, metals above ~50%); nothing inferred from a see-through surface.
  • Shader table (fbxtool/acshaders.py, mirrored in web/app/acshaders.js): a shader name read as a base with suffixes, so ksPerPixelMultiMap_AT_NMDetail is described without a row of its own and a Custom Shaders Patch name nobody has seen is still described by its suffixes. Gives the family, the capability flags and what each texture slot is. Settled by tools/shader_census.py over the cars to hand, and held to the JavaScript copy by tests/test_acshaders.py.
  • Texture slots: txDiffuse, txNormal, txGlow, txEmissive, txDetail, txNormalDetail and txMaps mapped; everything else keeps the game's own name.
  • txMaps carried as acMaps — a slot of its own rather than as a metallic-roughness map, whose channels drive a Blinn-Phong highlight and which put in that slot gives every panel a metalness and a roughness nobody wrote. 201 of the 528 materials counted bind one.
  • On the shaders that model a car being crashed, txNormal is the dents rather than the panel's own relief, and is not drawn as relief.
  • shaders switch — one control settling both the view and the export, offered only for a file that states a game's material and on by default for one that does. On, the surface is what the game states: the whole of the light it takes as its albedo, the Fresnel it stated as its reflection, and no conductor anywhere. Off, it is the PBR approximation this tool derives. Remembered per file.
  • The switch turns off this tool's inference and nothing else: a paint, a colour set by hand and a finish the car's own config names all stand whichever way it is thrown. The glTF core stays inside what a stranger can render either way — a dielectric's reflectance is still held at 4% and an emissive past white still carries its strength in an extension — with the game's own numbers in extras, marked shaderModel.
  • txMaps shaded per texel with the switch on — its red channel weighs the highlight and its green multiplies the exponent, which is what keeps a badge, a shut line and a chrome strip from taking the same gloss as the panel around them. Its blue is deliberately not read: it is the red again (correlation 0.994, identical on 175 of 178) because the map is authored grey, and taken as glTF packs a metallic-roughness map it would make chrome of every panel wearing a bright mask.
  • What those channels are was settled by decoding them — tools/maps_channels.js over the 221 that could be read — rather than by what the name suggests. Red varies on 175 and sits at an arbitrary constant when it does not, which is a level; green is flat on 142 and 133 of those are flat at exactly 1, which is a multiplier's default; alpha is flat on 214 at exactly 1 or exactly 0, which is what the two block formats give a channel nobody wrote.
  • ksPerPixelNM_UVMult tiles its colour and its relief by the two multipliers it states apart — a median of 12.5 and 195 across the 32 materials that state them. A stated nought is a multiplier nobody set, not one set to nothing.
  • ksAlphaRef stated as nought read as the game's own 0.5: every alpha-tested material counted states nought, and nought cuts nothing out, so a grille taken at face value is a solid rectangle.
  • txDetail applied as a tiled grain, neutral at its own average. A map that is one colour from corner to corner is no grain and is refused — a third of the 581 in the 67 cars to hand are that, and 55 are saturated enough to repaint whatever wears them — unless the worn skin is what brought it, in which case the flat colour is the paint: a BMW Z3 states its colour nowhere else, and its thirteen skins share one grey ambient-occlusion sheet and differ only in the metal_detail.dds each lays over it.
  • Meshes marked invisible and nodes marked inactive are read, counted, reported and not drawn; visibility descends.
  • Textures carried on the Video clip and shared between materials; textures a material names but the file lacks are listed.
  • AlphaBlend and alpha-tested materials reported with their AlphaMode.
  • Lamp lens colours read from GLASS_COLOR in the whole extension/ folder, keyed by mesh; a white or grey tint darkens, a saturated one replaces and reduces what shows through.
  • Protected cars detected by the __AC_SHADERS_PATCH_KN5ENC_v1__ marker and by triangle winding disagreeing with vertex normals; reported, never decrypted.
  • Not read: data.acd, .knh hierarchies, the encrypted half of a protected car.

Skins

  • Every folder under skins/<name>/ is offered in the viewer, replacing the car's textures by name.
  • Which materials are the paint, in order of trust: the skin's ext_config.ini, the car's extension/ext_config.ini, then what the car's other skins agree on. Both CarPaintMaterial = … and [Material_CarPaint_*] + Materials = … spellings.
  • Colours from cm_skin.json (#AARRGGBB), from ChameleonColorA in ext_config.ini, or from the livery.png chip.
  • enabled is the paint shop's switch, not a claim about the car: a stated colour is the paint either way, except plain white or anything with no channel above 8, which are what an untouched picker holds and are handed to the chip instead.
  • The chip is read only where nothing landed on the car and the skin does not bring the paint's own texture; the commonest colour over the upper half, 32 steps a channel.
  • Materials and colours paired by order; one colour is spread over however many materials are named.
  • Names held against the model — a material the car has not got is not painted.
  • The paint tints its texture rather than replacing it.
  • A skin that states no colour anywhere can still be the paint by the txDetail map it brings; where a flat detail map came from is what says whether it is a placeholder or the paint.
  • A named paint checked against what it reads like. A material naming a wheel, a brake, a lamp or the cabin — a Ferrari Mondial's own extension/ext_config.ini points its paint at EXT_RIM_AO, its wheel rims' baked ambient occlusion — is flagged, and where the car's own materials name exactly one thing that reads like paint and is not the same mistake itself, the web viewer puts the colour there instead; more than one candidate is not guessed among.

Animations

  • Everything under animations/<name>.ksanim beside the car is read: both versions in circulation — 1,379 of the 1,461 clips across the cars to hand are version 2 (quaternion, translation and scale per key), 71 are version 1 (a 4x4).
  • The format has no magic number, so a clip is accepted only when its structure lands exactly on the last byte of the file. That is what rejects the ._-prefixed macOS resource forks that turn up in the same folders — eleven of the 1,461.
  • A key is the node's whole placement, not a change to it. A BMW Z3's capote.ksanim opens on the translation, rotation and scale its own model states for the same node, so at position 0 the car is exactly the car the file describes.
  • Played by position, not by time. There is no clock in the file: the game drives a clip from how far the wheel is turned or the door is open, so the viewer offers a position from 0 to 1 and a button that sweeps it.
  • Held against the model, and against itself. A clip is offered only where it names a node this car has and moves it. Of 123 clips across 22 cars, 48 name nothing but DRIVER: rig nodes — the driver is a separate model living inside the game — and others name nodes here and hold every one of them still: a BMW Z3's steer.ksanim names 270 nodes, 13 of which the car has, and all 13 are the same placement in all 100 keys, because the turning is in the 257 belonging to the car it was authored against. Both are counted and said rather than offered as a slider that does nothing.
  • Only this car's nodes are decoded, which is most of the work saved on a clip written for another one.
  • A clip naming a node that carries no mesh moves everything hanging off it, so a rigged door swings from its hinge.
  • Applied as one matrix per part rather than by rebuilding the scene: the difference between where the clip puts a part and where the file had it, in the vertex stage of all three passes, so the picture, the ground shadow and what the mouse can land on agree.
  • Not written out: neither glTF nor FBX is given the clip. What leaves is the car at whichever position it is standing at.

Converting a car

kn5togltf car/                        # list the skins, ask which one, write it
kn5togltf car/ --list                 # list them and stop
kn5togltf car/ --skin "Rosso Corsa"   # wear that one, no questions
kn5togltf car/ --no-skin              # the car as the file has it, unpainted
kn5togltf car/ --shaders              # write the game's own material model
kn5togltf car/ --format gltf -o out/  # a .gltf beside its .bin

A car is a folder rather than a file — the .kn5 holds it unpainted and everything under skins/<name>/ replaces the textures of that name — so converting one means answering which skin. Named with --skin it is worn without asking; named as neither that nor --no-skin, the skins are listed with what each paints and one is asked for, by number or by name.

  • --shaders writes the game's own reading of a material rather than the PBR one derived from it. Either way the game's own numbers travel in extras, and the glTF core stays inside what a stranger can render; what moves is the materials the reader inferred a metalness for — 7 of a BMW Z3's 70, none of a Ferrari 456's 16.
  • The exporter is the viewer's own (web/app/gltf.js), driven through a headless Chromium by tools/kn5_to_gltf.js. There is one exporter, not two: it needs a canvas to decode a DDS and to bake a tiled grain into the picture it multiplies, so what this command writes is the file the Export button writes by construction rather than by agreement.
  • Needs node and playwright for that reason, and a built page (python3 web/build.py). Installed globally, playwright is found the way the test harnesses find it — npm root -g into NODE_PATH.
  • The folder is staged down to what is worth reading before it is handed over: the model, the car's own config, the chosen skin whole, and every other skin's ext_config.ini and cm_skin.json. The page reads at most 512 files from a drop and a real car folder is often more — a BMW Z3's is 636 — so a skin at the end of the alphabet would otherwise be dropped before anything saw it. Those two settings files are kept for every skin because what a car calls its paint is settled across the whole folder when it names it nowhere. Staged by hard link, so a 200 MB model costs nothing to lay out.
  • Animation is not written: what leaves is the car standing still.

Web viewer

python3 web/build.py          # -> web/dist/fbxview.html

One self-contained HTML file — WebAssembly, CSS and JavaScript inlined. Runs from file:// with no server, no CDN and no network; nothing is uploaded.

Layer Where it runs
DEFLATE, binary record walking, triangulation, normal generation WebAssembly (web/src/fbx.c, freestanding, no libc, no imports)
ASCII FBX, OBJ, .blend and .max reading, analysis, report JavaScript
Rendering WebGL2, orbit camera, per-material shading

Opening files

  • Drop a file, drop a whole folder, or use Open folder; a folder is read as far as 512 files, models and their images first.
  • The model is picked by extension, not by order; where a folder holds the same scene several ways, the one with the most to draw opens (a subdividing cage counted for what it becomes, ties within a tenth broken by what else each file carries).
  • Where a folder holds one model split into many numbered .obj fragments instead (a raw per-part dump, no single file the whole scene) they are merged into one document first, rather than the richest fragment opening alone and the rest being discarded.
  • Where the opened file has no maps at all, materials are borrowed from a sibling file, matched part by part on punctuation-stripped names.
  • Images matched by file name rather than path, with URI escapes undone; supplied with the model or afterwards.
  • Missing images named in the viewport and in the Materials tab.

Scene

  • Whole-scene assembly with instancing — one mesh shared by several models drawn once per model.
  • Up axis picked from the geometry where it clearly disagrees with the declared UpAxis, shown, overridable, remembered per file.
  • Flip X / Y / Z, remembered per file, written into the export; winding switched for an odd number of mirrored axes.
  • Turn — a quarter turn of the camera about the up axis, remembered per file, never exported.
  • Explode slider, in the vertex shader, with the shadow pass and ground plane following.
  • Click to select a part by index buffer read-back; part name, triangle count, bounds and materials shown. Ctrl-click picks out more than one, and the line is drawn round the set (not shift — that pans the camera).
  • Delete (Delete) and split a part — split by connected geometry, by material by the file's own grouping. Held as a segment list, so edits are cheap and re-splittable.
  • Rename a part by typing over its name in the readout. The new name is written on the node an export makes, which is what a car's config, the clips beside it and the game look a part up by; the pieces of a split share one node, so a piece renamed is renamed on screen and says so.
  • Merge (J) every picked part into one, across meshes the file never had together; unmerge puts them back under the names and materials they went in with. A merge is held in world space and placed by no node, so it is written at the root of an export and a clip beside the car no longer moves it.
  • Smooth ×1 / ×2 — Catmull-Clark over polygons before triangulation, with open borders smoothed as curves, normals and UVs subdivided linearly. Automatic level held to a 384 MiB vertex-buffer budget, and said when held back.
  • Ctrl+Z / Ctrl+Y undo and redo; Restore all puts the scene back.
  • Ground — a floor at the model's lowest point with a self-shadowing depth-map shadow; toggleable.
  • Environment — the model can be looked at in a Studio, a Garage, in Daylight, at Dusk or at Night: the sky its reflections read, the sun that casts the ground shadow and the backdrop all change together, and the choice is remembered.
  • WebGL context loss and out-of-memory reported rather than failing silently.

Materials

  • Materials tab: colour, metalness, roughness, transparency, presets, rename, From file, Clear all.
  • Grouped by material rather than by slot, ordered by how much of the model each covers, hover to highlight.
  • Per-row image list — slot, file, and whether it arrived — with × to drop a map and + to take it back.
  • Change what a part is made of, including + new material.
  • Save assignment writes JSON (fbxtoolMaterials), keyed on the file's own material names and on model UIDs/names; drop it back in to reapply, in any order. Materials the file lacks are built.
  • Shading modes: File colours, Index colours, Clay, Normals, textures toggleable.

Shading

  • GGX specular over Lambert diffuse, one sun plus an analytic studio environment, drawn behind the model.
  • Shininess ↔ roughness as the fourth root of 2 / (exponent + 2), both directions.
  • Renderer-native parameters preferred over the Phong approximation, matched with the vendor prefix and separators removed: base_color/baseColor, roughness/specularRoughness, metalness/metallic, opacity/transparency.
  • Reflection index of refraction read where stated (reflection_ior, fresnelIor, ReflectionIor) and turned into ((n-1)/(n+1))²; Phong specular colour otherwise capped at 0.16.
  • Clear coat as a second specular lobe with its own roughness, scaled by the blend amount, taking its share out of what reaches the base.
  • Bump maps told from normal maps by inspection; tangent frame recovered per pixel from screen-space derivatives, so no mesh tangents are needed.
  • Transparency in a second blended pass — solid first, then back faces before front faces, depth read but not written. Coverage from Opacity, TransparencyFactor, OBJ d/Tr, Blender alpha, or per-pixel from a blended material's colour texture. Cut-out materials tested against alpha and kept in the solid pass. Files with nothing transparent never pay for the second pass.
  • Colour managed end to end: sRGB upload, linear shading, filmic tone map, sRGB out.

Image decoding

Format What is decoded
DDS BC1, BC2, BC3, BC4, BC5 (third channel rebuilt), BC7 (all eight modes); uncompressed surfaces by channel mask; DX10 headers. BC6H and floating-point declined.
KTX2 / Basis ETC1S, including alpha slices (web/src/ktx2.c)
Draco EdgeBreaker (standard and valence) and sequential connectivity, rANS tagged and raw, attribute seams, depth-first and prediction-degree traversal, difference / parallelogram / constrained multi-parallelogram / portable texture coordinate / geometric normal predictions, wrap and octahedral transforms (web/src/draco.c)
PSD the flattened composite, raw and PackBits; 16-bit, CMYK and PSB declined

Colour and alpha are kept unpremultiplied all the way to the GPU.

Export

Export writes what is on screen — assigned materials, and parts deleted, split, merged or renamed.

.glb one self-contained binary file
.gltf + .bin, zipped the same document with readable JSON, both in one .zip
.fbx binary FBX 7.4
  • Hierarchy, names, parents and instancing kept; each mesh written once in its own local space.
  • glTF: one primitive per material, materials covering no triangles dropped, primitives welded, V flipped, up axis and units on the root node's matrix.
  • FBX: several materials per mesh with a per-polygon index, up axis and units in GlobalSettings, nodes decomposed to translation/rotation/scale with mirrors kept as a negative X scale, arrays deflated where that helps, textures embedded once per Video record.
  • Textures embedded; PNG and JPEG passed through untouched, everything else encoded as PNG by the built-in writer (not canvas.toBlob, which premultiplies). Alpha dropped where it says nothing.
  • Deflate parallelised across workers, with a single-threaded fallback.
  • txDetail grain baked into the base colour on the GPU, with a CPU fallback that agrees byte for byte.
  • Bump maps converted to normal maps on the way out.
  • Reports what it left behind: materials removed by name, nodes removed, nodes renamed, and any map a material names that was not supplied.
  • Zip writer: local headers, central directory, deflate-raw, stored where deflate does not help. No ZIP64.
  • Checked against the Khronos glTF-Validator and re-opened in the page on every sample.
  • Not exported: animation, skins, morph targets, cameras, lights, tangents, vertex colours, second UV sets. Geometric offsets are baked into vertices.

Library

from fbxtool import read_fbx, analyze

doc = read_fbx("scene.fbx")
print(doc.encoding, doc.version)          # 'binary' 7400
print(doc.wide_offsets, doc.has_footer)   # False True

info = analyze(doc)
for obj in info.objects:
    print(obj.uid, obj.kind, obj.display_name, obj.detail)
Name Purpose
read_fbx(path) sniff the encoding and parse a file
parse_bytes(data) the same, for data already in memory
parse_binary(data) / parse_ascii(text) force one reader
detect_format(head) "binary", "ascii" or "unknown"
analyze(doc) scene-level facts (Analysis)
render_text(analysis) / to_dict(analysis) the CLI's text and JSON output
render_tree(node) record tree as a list of lines
describe(stamp) version stamp → dotted version, product, layout flags

The record tree is reachable directly through doc.root — path(), path_value() and walk(). Array payloads are not decoded by default, only measured; pass load_arrays=True (and optionally max_array_values=N).

FBX versions

Stamp Version Product
6000 6.0.0 FBX 6.0 (legacy layout)
6100 6.1.0 FBX 2009 / 2010 (legacy layout)
7000 7.0.0 FBX 2010
7100 7.1.0 FBX 2011
7200 7.2.0 FBX 2012
7300 7.3.0 FBX 2013
7400 7.4.0 FBX 2014 / 2015
7500 7.5.0 FBX 2016 / 2017 — node offsets widen to 64 bits
7600 7.6.0 FBX 2018
7700 7.7.0 FBX 2019 / 2020

Unlisted stamps are still reported, with the dotted version and the layout flags derived from the number.

Damaged files

Structural problems are collected as warnings rather than raised, so a partly corrupt file still produces a report — mismatched property-list lengths, out-of-range or inconsistent end offsets, unreadable array payloads, a missing or mismatched footer, and for ASCII unclosed braces, stray } and records without a :.

Only a file that cannot be identified at all, or whose property stream is undecodable, raises: ParseError / UnsupportedFormatError, both subclasses of FbxError.

Limitations

  • Nothing writes OBJ or .blend; nothing on the Python side writes anything. The web viewer exports glTF and FBX.
  • The Python side evaluates no geometry — no triangulation, transforms or bounding boxes; that happens in the web viewer.
  • Encrypted FBX files are reported, not decrypted.
  • The version table ends at 7700; newer stamps are described generically.
  • Deletes, splits, merges and renames last only for the session; assignments remember whole parts only, so a merge is not filed under any of the parts it was made of.

Development

pytest                          # no dependencies beyond pytest itself
python3 tools/make_samples.py   # regenerate the generated files in samples/
python3 web/build.py            # rebuild web/dist/fbxview.html
python3 tools/shader_census.py <folder of cars>   # what the game's shaders are made of
node tools/maps_channels.js <car.kn5> [more.kn5 ...]  # what is in a txMaps, channel by channel

tools/shader_census.py reads only each .kn5's header and material table — the texture payloads are stepped over rather than loaded — and counts, per shader name, how many materials wear it and across how many cars, which parameters they state and what those come to, and which texture slots they bind. Every decision in fbxtool/acshaders.py is settled by its output rather than by what a shader's name suggests; --json emits the counts for holding against.

The JavaScript and WebAssembly layers have their own harnesses, each run by pytest and each usable on its own:

node web/test/units.js                        # transform maths, material mapping
node web/test/heap.js samples/cube_binary.fbx # the WASM bump allocator
node web/test/dump.js samples/cube_binary.fbx # the WASM reader's whole tree
node web/test/browser.js samples/*.fbx        # the built page in Chromium
node web/test/transparency.js glass.fbx       # reads pixels through glass
node web/test/materials.js samples/scene_parts.fbx   # the material list
node web/test/ground.js samples/scene_parts.fbx      # the floor and its shadow
node web/test/gltf.js samples/cube_textured.fbx      # export, then validate it
node web/test/subdivide.js model.fbx                 # smoothing through the module
node web/test/smoothing.js samples/cube_binary.fbx samples/scene_parts.fbx
node web/test/gltfin.js samples/cube_textured.fbx    # export, then read it back
node web/test/reload.js a.fbx b.fbx                  # one file replacing another
node web/test/parts.js samples/scene_parts.fbx       # the explode, and picking
node web/test/flip.js samples/scene_parts.fbx        # mirroring, and its winding
node web/test/turn.js samples/scene_parts.fbx        # facing the other way
node web/test/skin.js <car folder> <other.kn5>       # putting a skin on a car
node web/test/animation.js car/                      # the .ksanim clips beside a car
python3 -m pytest tests/test_convert.py              # the kn5 -> glTF command
node web/test/shaders.js <dir> <chrome.kn5> <paint.kn5> <plain.fbx>  # the shaders switch
node web/test/acmaps.js <dark.kn5> <bright.kn5> <tiled.kn5>  # the per-texel finish
node web/test/drop.js samples/pyramid.obj samples/pyramid.mtl samples/checker.png
node web/test/fragments.js                           # a model split into many numbered .obj files
node web/test/edits.js samples/Shelby.fbx            # deleting, splitting, merging, renaming

tests/fbxbuild.py writes .blend and glTF fixtures, so those readers are testable without Blender or an exporter installed. The Draco fixtures in samples/draco and the KTX2 fixtures in samples/ktx2 are encoded by Google's and Binomial's own encoders and checked against their own decoders; those packages are test-time oracles only — nothing ships with a dependency.

The web tests skip cleanly when clang or node is unavailable. Building the module needs clang and wasm-ld, which ships with LLVM's lld rather than with clang itself; web/build.py finds it beside clang or in the toolchain directories, and says what is missing if it cannot.

License

MIT

About

FBX file format tool

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages