Packages

Reader for the glTF 2.0 3D interchange format — .gltf and .glb, with accessor decoding that handles interleaved strides and sparse substitution.

Current section

Files

Jump to
gltf README.md
Raw

README.md

# gltf
Reader for **glTF 2.0**`.gltf` JSON and `.glb` binary containers. Pure
Elixir, one dependency (`jason`), no NIFs.
glTF is the interchange format essentially every 3D tool can write, which
makes it the practical way to get geometry into or out of an Elixir service.
This library reads one. It does not render, and it does not fetch anything
over the network.
```elixir
{:ok, gltf} = Gltf.read("model.glb")
Gltf.info(gltf)
#=> %{version: "2.0", generator: "Khronos glTF Blender I/O", meshes: 1, nodes: 2, ...}
{:ok, positions} = Gltf.attribute(gltf, 0, 0, "POSITION")
#=> [[-0.5, -0.5, 0.5], [0.5, -0.5, 0.5], ...]
{:ok, indices} = Gltf.indices(gltf, 0, 0)
#=> [0, 1, 2, 3, 2, 1, ...]
```
## Installation
```elixir
def deps do
[{:gltf, "~> 0.1"}]
end
```
## Don't box a million floats
For anything vertex-sized, take the bytes:
```elixir
{:ok, bytes, layout} = Gltf.attribute_binary(gltf, 0, 0, "POSITION")
layout
#=> %{type: "VEC3", component: :float32, element_size: 12, count: 24, ...}
```
`bytes` is tightly packed, little-endian, de-interleaved, with any sparse
block applied — the form a GPU buffer, a file, or a socket wants. Building
three million boxed floats only to hand them straight back out is work nobody
asked for.
## The parts that are actually hard
A glTF reader is a weekend project right up until it meets a real file. These
are the four places quick implementations go wrong, and all four are covered
by tests against the [Khronos sample assets](https://github.com/KhronosGroup/glTF-Sample-Assets)
rather than only against fixtures written here.
**Interleaved `byteStride`.** A buffer view may weave several attributes
together so the GPU can read a whole vertex in one pass. Reading elements back
to back then decodes a mixture of positions and normals — which looks like
plausible geometry, not like a crash. Khronos publishes `Box` and
`BoxInterleaved`: the same cube in both layouts. This library decodes them to
identical geometry, and the test asserts exactly that.
**Sparse accessors.** An accessor may override individual elements by index,
possibly with *no base buffer view at all* — in which case the base is defined
to be zeros, not an error and not an empty list. Ignoring a sparse block gives
a model that is silently wrong.
**Normalised integers.** `normalized: true` means an integer component encodes
a fraction: unsigned onto `0.0..1.0`, signed onto `-1.0..1.0` with the most
negative value clamped, so `-128` and `-127` both give `-1.0`. Return the raw
integers and colours come out in the hundreds.
**Non-finite floats.** The BEAM has no float term for NaN or infinity, so
`<<v::little-float-32>> = <<0, 0, 0xC0, 0x7F>>` does not raise — it fails to
match, and a reader that does not expect that dies on a file every other
viewer opens. Exporters emit them; a NaN normal is a routine artefact of a
degenerate triangle. They decode to `:nan`, `:infinity` and `:neg_infinity`.
## Scene graph
Node transforms compose as `T * R * S`, in that order, and an explicit
`matrix` wins when a node has one. Get the order wrong and a model looks
correct until something is both rotated and non-uniformly scaled, at which
point it shears.
```elixir
{:ok, transforms} = Gltf.world_transforms(gltf)
Gltf.Node.transform_point(transforms[1], {0.0, 1.0, 0.0})
#=> {0.0, 0.0, -1.0}
```
Matrices are column-major flat lists of 16 numbers — the same order glTF
stores them in, so a matrix read from a file needs no rearranging.
The node graph is a forest by specification, but nothing in a file enforces
it. A node listing an ancestor as its child makes naive traversal loop until
the machine dies, so `world_transforms/2` tracks the chain it is descending
and reports the cycle instead.
## Reading someone else's file
A glTF document is data, usually downloaded, and `"uri"` is a string its
author chose. `"../../../etc/passwd"` is a valid one.
External buffers are therefore confined to the asset's own directory. Pass
`allow_outside_base: true` to follow a symlinked asset library on purpose, or
`external: false` to refuse sibling files entirely. A URI with a scheme —
`http:`, `file:` — is refused rather than fetched: a parser that opens sockets
is a parser you cannot point at untrusted input. Fetch it yourself and pass
`:buffers`.
## Scope
**In:** the GLB container, the JSON document graph, accessor decoding, buffer
resolution, node transforms, and validation with errors that say which index
of what went wrong.
**Out:** rendering, mesh processing, and writing. Draco and meshopt
compression are out too — both are substantial codecs, and the honest options
are shelling out or not pretending.
Extension objects are preserved in the document but not interpreted.
`Gltf.info/1` reports `extensions_required`, which a caller should check
before trusting the geometry: a file requiring `KHR_draco_mesh_compression`
parses fine here and its meshes are compressed nonsense.
## Prior art
[`eagl`](https://hex.pm/packages/eagl) loads glTF, but bundled inside an
OpenGL library aimed at native desktop rendering through `:wx`. This is the
standalone parser — no renderer, no windowing, no GPU.
## Testing
```
mix test # hand-built fixtures, always run
test/corpus/fetch.sh # download the Khronos samples
mix test # now also runs corpus_test.exs
```
The sample assets are fetched rather than committed: they are third-party
files under their own licences, and a parser should be checked against the
canonical copies rather than a snapshot that quietly drifts. Without them the
corpus tests skip and a fresh checkout still runs green.
## Related
[`ply`](https://github.com/mdon/ply) reads the other 3D format worth having —
Stanford Polygon, which is what Gaussian-splat trainers emit.
[`splat_tools`](https://github.com/mdon/splat_tools) turns those into web
assets.
## Licence
MIT.