draco-io
draco-io is the low-level format I/O layer for the Draco Rust workspace. It
reads and writes OBJ, PLY, and binary FBX geometry and provides strict glTF/GLB
container, resource, accessor, and Draco-geometry contracts.
For complete, lossless glTF documents, scene preservation, and
document-preserving Draco compression, use
draco-gltf. Those APIs deliberately do
not live in this crate.
Installation
[]
= "0.3"
To keep a binary small, disable the default format readers and writers and enable only the features required by the application:
[]
= { = "0.3", = false, = ["obj-reader", "obj-writer"] }
Supported formats
| Format | Read | Write | Scope |
|---|---|---|---|
| OBJ | Yes | Yes | Meshes, normals, texture coordinates, named groups, and point clouds. |
| PLY | Yes | Yes | ASCII and binary geometry, normals, colors, and point clouds. |
| FBX | Yes | Yes | Binary and ASCII FBX 7.x scene data. See the FBX matrix below. |
| glTF / GLB | Containers and geometry contracts | Containers | GLB inspection, JSON/bin extraction, resource resolution, accessors, and optional KHR_draco_mesh_compression geometry decode. Full-document operations belong to draco-gltf. |
All mesh formats use the draco-core geometry model. Position is required;
the supported optional attributes depend on the source and destination format.
Writers do not claim to preserve data that their target cannot represent.
FBX support
Both containers are read and both are written. Binary is accepted for versions
6000 through 8000 in either byte order — a non-zero endian marker selects
big-endian, as ufbx does — and ASCII for 7000 and later. Output is FBX 7500
little-endian, binary by default;
FbxWriter::with_format(FbxFormat::Ascii) or FbxScene::to_ascii_bytes
selects the text container.
That sharing is structural, not a convention. fbx_container decodes the
binary container and fbx_ascii the text one; both produce a tree of
FbxNode, and fbx_reader reads only that tree, so it is unaware of which
container it was given. Writing mirrors it: FbxWriter::build_document decides
what records the file contains and returns the same FbxNode tree, which
fbx_encoder spells as records and fbx_ascii_writer as text.
fbx_ascii_syntax holds what the two containers disagree about — the
name/class separator, the array element-type schema, the Properties70 type
table — in one place, with each convention next to its inverse.
fbx_transform composes the FBX transform stack into a local matrix.
The ASCII reader produces the same node tree as the binary one, so everything
above it is shared and the two containers cannot drift apart semantically. Two
differences are normalized rather than left for consumers: object names are
written Class::Name where binary uses the reverse order with a different
separator, and values carry no type tag, so a whole number is indistinguishable
from an integer. The element type is recovered from the node's schema instead
of from how the number happens to be written -- guessing would type a mesh with
integer coordinates as an integer array, and 27 of the 369 Vertices arrays in
the corpus are exactly that.
110 of the corpus's ASCII documents decode identically to their binary twin,
compared across geometry, layers, skins, morphs, transforms and animation. Six
documents are excluded by exact name in the corpus test, each with its observed
difference stated there: one relies on an escape ASCII cannot reverse, four are
not pairs at all -- the two exports were taken at different points on the
timeline -- and one prints f64 too coarsely to survive narrowing to f32.
cargo run --example fbx_twin_diff -- <ascii.fbx> prints the same field-by-field
comparison for a single pair, which is how those were established.
Writing text costs precision in two places, and neither can be recovered by
trying harder. ASCII does not record an integer's width, so an i64 small
enough to fit comes back an i32; every reader here accepts either. And a
quotation mark in an object's name is written ", which is not
reversible — one corpus file holds an object named " and another named
literally ", and both are spelled the same way. The same applies to any
string that merely happens to contain ::, which the reader splits into a name
and a class; ufbx reads it that way too. Three shapes are refused outright
rather than written wrong: a node with two array properties, a non-finite
float, and raw bytes outside a Content node. Everything else round-trips: the
corpus test writes all 565 comparable files both ways and compares the node
trees they read back as, record by record.
Scene content, however, is read only from FBX 7000 and later. Earlier
versions use a different object model: objects are identified by a
"name\0\x01Class" string instead of an i64 id, connections reference those
strings, geometry lives on the Model rather than on a separate Geometry,
and array payloads are stored as repeated scalar properties. A pre-7000
document therefore decodes to a structurally valid but empty scene, and says so
through FbxWarningCode::NameKeyedObjectModel rather than looking like a file
that simply has no meshes. 81 of the 308 binary files in the ufbx corpus are
version 6100 and fall in this category.
| Data | Read | Write |
|---|---|---|
| Mesh geometry | Yes | Yes |
| Normals and UV layers | Yes | Yes |
| Vertex colors | Yes | Yes |
| Tangents and binormals | Yes | Yes |
Edges array |
Yes | Yes |
Edge smoothing (LayerElementSmoothing) |
Yes | Yes |
| Edge and vertex creases | Yes | Yes |
| Mesh and model names | Yes | Yes |
| Node hierarchy | Yes | Yes |
| Node transforms | Yes | Yes |
| Materials and textures | Yes | Yes |
| Node-TRS animation | Yes | Yes |
| Multiple animation layers | Yes | Yes |
| Animation layer blending | No | No |
Cameras and lights (NodeAttribute) |
Yes | Yes |
| Skins, bind poses, and influences | Yes | Yes |
| Blend shapes / morph targets | Yes | Yes |
Definitions property templates |
Yes | n/a |
| ASCII container | Yes | Yes |
An exporter that gives a whole class the same value writes it once as a
Definitions/PropertyTemplate and leaves it off the objects, so those values
are read too — the Revit cameras declare almost nothing directly. The object
always wins: 5553 properties in the corpus are declared in both places, Lcl Translation on 928 models among them, and letting the template override would
move every one of them to the origin. Templates are matched to objects by the
ObjectType name, which is the object record's node name, except for
NodeAttribute: a document declares only one template for it while the record
covers unrelated classes, so the template's own class must match the object's
(FbxCamera to Camera, FbxSkeleton to LimbNode, and so on). Nothing
derives that pairing from the strings.
Layer elements are resolved on the polygon-corner domain, so a UV or hard-normal
seam survives instead of being averaged onto its control point. The Draco mesh
welds corners that agree on every attribute, which keeps seams while collapsing
interior duplicates. Every UV, normal, colour and tangent set is preserved on
FbxMeshInstance; only the first of each reaches the Draco mesh, since Draco
has no concept of multiple sets.
Tangents are stored as four components, with the handedness sign in w -- the
layout glTF's TANGENT uses. FBX itself splits them across two sibling arrays,
Tangents and a TangentsW that only 7500 and later write, so a set records
whether its handedness was authored or defaulted to +1; the writer emits the
sibling array only when it was. Binormals are read and written for the same
reason Edges is kept raw -- they are the only carrier of tangent sign in files
that have no TangentsW -- but they stop at FbxMeshInstance, since glTF has
no binormal to lower them onto.
Draco's GeometryAttributeType has no tangent, so tangents never enter the
Draco mesh or its weld key; they travel on FbxMeshInstance and
FbxRenderMesh, the same route extra UV sets take.
Edges is kept verbatim rather than normalized: FBX does not require it to list
every topological edge, and importers reconstruct the rest from faces, so
discarding the distinction would lose information. It is also the domain
ByEdge layers address.
Smoothing flags and crease weights address edges, polygons or control points --
never polygon corners -- so they are preserved raw on FbxMeshInstance beside
Edges rather than resolved onto the render mesh, and they have separate types
because smoothing is an integer flag while a crease is a floating-point weight
that an integer would flatten. glTF has no equivalent for either, so they
survive an FBX-to-FBX rewrite and travel no further.
A layer whose length disagrees with the domain its mapping names is dropped with
a warning instead of being kept as misaligned data; seven layers in the corpus
are in that state. A ByEdge layer in a geometry that has no Edges array is a
separate case -- it addresses the edges an importer would reconstruct, which
this crate does not do -- so it is preserved unchecked rather than discarded.
Property templates in Definitions are not resolved. The specification allows a
property to be omitted from an object and supplied by its class template, but
across 272 binary files in the ufbx corpus no Material or Model relies on
that for any property this crate reads.
FBX materials cover the canonical Phong/Lambert property set (DiffuseColor,
SpecularFactor, Shininess, EmissiveColor/EmissiveFactor,
ReflectionFactor, TransparencyFactor/Opacity, BumpFactor) with diffuse,
normal, and emissive textures (embedded Content or external filename), and
per-polygon material indices. Animation resolves the
AnimationStack → AnimationLayer → AnimationCurveNode → AnimationCurve graph
into per-node TRS channels in seconds, one clip per layer — the same choice
Blender's importer makes. Layers are not blended.
Camera and Light node attributes are read onto
FbxSceneNode::attribute. Every field is optional, because FBX omits any
property left at its class default, and the field sets are limited to what the
corpus actually contains -- no file carries InnerAngle or OuterAngle, so
spot cone angles are not represented. The film back (FilmWidth,
FilmHeight, FilmAspectRatio, ApertureMode) is represented, because a
focal length alone does not give a field of view: Blender computes
sensor_width = FilmWidth * 25.4 and substitutes its own 32 mm default when
the property is missing, which reframes the shot. The viewport decoration
around it -- DisplayTurnTableIcon, ShowManipulators, BackgroundColor,
GateFit -- is not represented. They are written back too: 58 attributes
across 20 corpus files survive a rewrite. That takes more than mirroring the
reader, because the reader is blind to most of what an importer checks -- it
finds an attribute through its OO connection and reads properties by name,
never consulting the Model's class, TypeFlags, the Definitions count or
a P record's declared type. All four are written and asserted on the
document tree, since no write-and-read cycle can see them. Other attribute classes
-- LodGroup, CameraSwitcher, CameraStereo, IK and FK effectors -- raise
FbxWarningCode::DroppedNodeAttribute. LimbNode and Null do not, since a
skeleton attribute is consumed by the skin path and a null carries nothing but
its transform.
Scene export preserves
local affine translation, rotation, scale, skins, bind poses, morph targets,
and authored animation channels. FBX pivot settings and inheritance rules are
not represented by FbxTransform.
Decoding a document twice gives the same result: object order, animation channel order and bind-pose resolution follow FBX object ids rather than hash iteration.
Reading untrusted input
FBX is a length-prefixed binary container with a decompression path, so
FbxReadOptions bounds what one document may allocate and how strictly its
layout is enforced:
use ;
let options = default
.with_limits;
let scene = from_bytes_with_options?;
# Ok::
Limit violations fail with ErrorKind::OutOfMemory and structural violations
with ErrorKind::InvalidData, so a caller can tell "too big, retry with
FbxDecodeLimits::permissive()" from "corrupt". The defaults are calibrated
against real assets, not guessed; see FbxDecodeLimits::default.
FbxReadOptions::strict() additionally rejects anything the container layout
does not permit, including a malformed binary footer. It is off by default
because shipping exporters emit slop that every practical reader tolerates:
222 of 308 real files in the ufbx corpus do not begin their trailing region
with the conventional footer id at all. Deviations accepted in the default mode
are reported through FbxScene::warnings as typed FbxWarningCode values
rather than passing silently.
The web converter adds a source-neutral SceneDocument adapter above this
crate. That adapter preserves extra UV sets and up to eight skin influences in
its GLB and typed-FBX paths; its WebGL preview reports when it uses only the
first four influences. Vertex colours travel end to end as COLOR_0; tangents
remain lossless in SceneDocument/glTF but are reported as unsupported when
lowering to the typed FBX writer. Non-default RotationOrder/InheritType
behavior remains unvalidated beyond the Mixamo, Samba Dancing, and Fox
controls.
Quick start
Read an OBJ mesh:
use ;
use Path;
Convert an OBJ mesh to binary FBX:
use ;
use Path;
Read and re-write a supported FBX scene while keeping its mesh hierarchy and local transforms:
use FbxScene;
let scene = from_bytes?;
write?;
# Ok::
Use FbxReader::read_scene and FbxWriter::add_scene when reading from a
stream or configuring FBX array compression.
For format-agnostic use, Reader, Writer, ReadFromBytes, and
WriteToBytes provide the common I/O traits. The complete API is documented at
docs.rs/draco-io.
Feature flags
| Feature | Default | Purpose |
|---|---|---|
all-readers / all-writers |
Yes | Enable all OBJ, PLY, and FBX readers or writers. |
obj-reader / obj-writer |
Yes | Wavefront OBJ support. |
ply-reader / ply-writer |
Yes | Stanford PLY support. |
fbx-reader / fbx-writer |
Yes | FBX support, binary and ASCII. |
gltf-container |
No | Parse glTF/GLB and load referenced buffers; no mesh decoding. |
gltf-geometry |
No | Convert ordinary glTF accessors into draco-core meshes. |
draco-decode |
No | Add KHR_draco_mesh_compression primitive decoding. |
legacy-bitstream-decode |
No | Decode older Draco bitstreams. |
compression |
Yes | zlib compression for FBX output. |
point_cloud_decode |
Yes | Point-cloud decoding in draco-core. |
Relationship to draco-gltf
Use draco-io when an application needs a strict GLB/container parser,
resource-resolution policy, or low-level accessor geometry. Use draco-gltf
when it needs a full scene document, nodes, materials, animations, skins, or
document-preserving Draco transforms. Keeping this boundary explicit prevents
low-level tooling from accidentally promising full glTF round-tripping.
License
Apache-2.0.