glTF Transform edits the model as data, not as a document
glTF 2.0 SDK for JavaScript and TypeScript, on Web and Node.js.
At a glance
- What is it?
- glTF-Transform is an MIT licensed TypeScript SDK for reading, editing and writing glTF 2.0 in Node.js and the browser, where the API manages array indices and byte offsets for you so a model can be pruned, deduplicated and compressed reproducibly. It is not a modelling tool.
- Who is it for?
- Adopt glTF-Transform if you are shipping 3D models and need a repeatable step that prunes, deduplicates and compresses them, in a Node build pipeline or a browser tool, given the core SDK is compatible with both.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 3 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 21, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The boundary the README draws against modelling tools
glTF Transform supports reading, editing and writing 3D models in glTF 2.0 format. The README then draws a line: unlike 3D modelling tools, which are ideal for artistic changes to geometry, materials and animation, glTF Transform provides fast, reproducible and lossless control of the low-level details in a model.
That sentence is the product definition. Reproducible is the operative word, and it is worth being clear about why it differs from an export-and-reimport round trip through a DCC tool. Re-exporting a model through a modelling package regenerates buffers, may reorder attributes, may resample animation and will produce a different file each time. A script that runs the same transforms produces the same bytes.
What the API removes is stated next: it automatically manages array indices and byte offsets, which would otherwise require careful management when editing files. That is the unglamorous part of glTF and the part that makes programmatic editing hard to get right by hand. An accessor has a byte offset, a stride and a count, and every vertex you add has to move the buffer views around. The SDK makes those relationships derivable rather than manual.
The README lists the resulting use cases directly: bundling, splitting or optimising an existing model, applying quick fixes for common issues, building a model procedurally, and developing custom extensions on top of the glTF format.
Because the core SDK is compatible with both Node.js and Web, the same code can be an offline workflow or part of a web application. That portability is the reason the package is split the way it is.
Four packages, one per kind of consumer
The package split is small enough to hold in your head, and each package answers a different question.
`@gltf-transform/core` is the core SDK, providing an expressive API to read, edit and write glTF files. `@gltf-transform/extensions` supplies extensions, which the README describes as optional glTF features, for the core SDK. `@gltf-transform/functions` holds functions for common glTF modifications, written using the core API.
The fourth, `@gltf-transform/cli`, is the command-line interface to apply functions to glTF files quickly or in batch.
The layering matters for a decision you will have to make. If you are embedding transformation in a build step, you take core, extensions and functions. If you just want a one-off optimisation from a shell, you take the CLI alone, which pulls in the rest.
The functions package is the piece that most directly answers "do I have to write this myself". A function is a transform that takes a document and returns one, which is why the custom transform example in the README is written as a plain function returning a closure over the document. Anything the functions package ships, you can also write by hand against the same API, and the README names the classes you would use: Material, Primitive and Texture.
The extensions package exists because glTF 2.0 is a core specification with optional features layered on, and a document that uses them has to be read with the corresponding extension registered or the data is silently ignored.
I/O is selected at construction, which decides your runtime
The first thing your code constructs is an I/O object, and the choice is baked in at that moment rather than at each call.
Reading and writing happens through platform I/O utilities: `WebIO`, `NodeIO` or `DenoIO`. The README's example configures `NodeIO`, registers all extensions, and registers decoder dependencies:
import { Document, NodeIO } from '@gltf-transform/core';
import { ALL_EXTENSIONS } from '@gltf-transform/extensions';
import draco3d from 'draco3dgltf';
// Configure I/O.
const io = new NodeIO()
.registerExtensions(ALL_EXTENSIONS)
.registerDependencies({
'draco3d.decoder': await draco3d.createDecoderModule(), // Optional.
'draco3d.encoder': await draco3d.createEncoderModule(), // Optional.
});
// Read from URL.
const document = await io.read('path/to/model.glb');
// Write to byte array (Uint8Array).
const glb = await io.writeBinary(document);Three things to notice. Extensions are registered explicitly, so a document using an optional feature needs its extension present or that feature will not survive a round trip. Dependencies are registered separately from extensions, and the Draco decoder and encoder are both marked optional, so a read-only pipeline needs the decoder and a compression pipeline needs the encoder. And the output is a byte array, not a file write, which is what makes the same code work in a browser.
The chaining style is also worth noting: `new NodeIO().registerExtensions(...).registerDependencies(...)` returns the object, so configuration reads as a pipeline rather than as a sequence of statements.
The install for this path is three packages:
npm install --save @gltf-transform/core @gltf-transform/extensions @gltf-transform/functionsThe README also points to `gltf.report/`, where you can load a glTF model and open the Script tab to try the scripting API without installing anything. For evaluating the tool, that is the fastest route in.
document.transform is a pipeline, and the order is your responsibility
The transform API takes variadic transforms and applies them in order, which is why the examples read as a pipeline rather than as discrete steps.
await document.transform(
// Losslessly resample animation frames.
resample(),
// Remove unused nodes, textures, or other data.
prune(),
// Remove duplicate vertex or texture data, if any.
dedup(),
// Compress mesh geometry with Draco.
draco(),
// Convert textures to WebP (Requires glTF Transform v3 and Node.js).
textureCompress({
encoder: sharp,
targetFormat: 'webp',
resize: [1024, 2024],
}),
// Custom transform.
backfaceCulling({cull: true}),
);The inline comments are the documentation, and they carry two pieces of information worth keeping. `resample` is lossless, which matters if you are worried about animation fidelity after optimisation. And `textureCompress` requires glTF Transform v3 and Node.js, so the same pipeline is not portable to a browser target.
Order is not enforced for you and the sensible order is not alphabetical. Pruning before deduplication removes material the deduplicator would otherwise scan. Resampling first reduces the data that later stages carry. Draco before texture work keeps the two concerns separate.
The custom transform at the end is a short function that shows the shape of anything you write yourself:
function backfaceCulling(options) {
return (document) => {
for (const material of document.getRoot().listMaterials()) {
material.setDoubleSided(!options.cull);
}
};
}A function taking options and returning a document transform. That signature is the extension point the README means when it says you can develop custom extensions on top of the glTF format.
One detail in the texture call is worth flagging rather than copying: the resize value is `[1024, 2024]`, which is not a plausible texture dimension in most cases and is likely a typo for 2048. Use it as a reminder to check the arguments you paste.
The CLI is where the compression choices actually live
The CLI is documented as supported in Node.js LTS versions, and it is where the concrete codec decisions are made.
Installation and the command list:
npm install --global @gltf-transform/cli
gltf-transform --helpThere is one command that does everything at once:
gltf-transform optimize input.glb output.glb --texture-compress webpThe README's advice is that you can also pick and choose your optimisations and build a custom pipeline. For a pipeline you own and will version, that is the better mode, because `optimize` makes its choices for you.
Geometry compression has two options with different scopes. Draco compresses geometry, and accepts a `--method` such as `edgebreaker`. Meshopt compresses geometry, morph targets and keyframe animation together, which is the reason to choose it when the model has morph targets or baked animation.
# Draco (compresses geometry).
gltf-transform draco input.glb output.glb --method edgebreaker
# Meshopt (compresses geometry, morph targets, and keyframe animation).
gltf-transform meshopt input.glb output.glb --level mediumTexture work splits three ways. Resize and WebP go through Sharp. KTX2 with Basis Universal goes through UASTC and ETC1S, and this is where the options get serious:
# Compress textures with KTX2 + Basis Universal codecs, UASTC and ETC1S.
gltf-transform uastc input.glb output1.glb \
--slots "{normalTexture,occlusionTexture,metallicRoughnessTexture}" \
--level 4 --rdo --rdo-lambda 4 --zstd 18 --verbose
gltf-transform etc1s output1.glb output2.glb --quality 255 --verboseUASTC and ETC1S are sequential here, each reading one file and writing another. The `--slots` syntax selects which texture slots to target, and the RDO and zstd flags control the encoder. Note that the two codecs are different trade-offs, so running both is a real pipeline decision rather than a mistake.
The CLI reference is linked as containing much more than what the README reproduces.
What it will not do, and the runtime constraints that bite first
Three boundaries.
The first is artistic change, already covered. If you need to change the shape of a character or re-time an animation, that is a modelling tool's job. glTF Transform can set a material to double-sided, as the custom transform shows, but it does not author geometry.
The second is runtime. The core SDK runs on Node.js and Web, but not everything in the functions package does. `textureCompress` is documented as requiring v3 and Node.js because it depends on Sharp, which the README annotates as Node.js only. The CLI is documented as supported in Node.js LTS versions. So "runs in the browser" applies to the core API and to transforms that are pure JavaScript, not to the full optimisation pipeline.
The third is the dependency registration problem, which is the most common way a glTF pipeline fails quietly. If a model uses Draco compression and you have not registered a `draco3d.decoder`, the document will not read correctly. If a model uses an optional extension you have not registered, that data will be dropped on write. Both failures are silent in the sense that no exception tells you which data went missing.
That is also the strongest argument for the configuration style the README demonstrates: register all extensions and both Draco dependencies up front rather than registering what you currently need.
A fourth, smaller constraint: the project describes itself as a glTF 2.0 SDK. The version of the format is fixed, and features from later or competing formats are not its concern.
Maintenance, licence, and what the repository build actually runs
The licence is MIT, stated in the README badge and in the LICENSE.md file at the repository root, with the copyright line reading 2024.
On maintenance: the repository is not archived and the last push was on 2026-10-16, so the main branch is receiving work. The project has no GitHub releases, so there is no tag to pin on GitHub; versions are published to npm, and the README's first badge tracks the latest npm release of `@gltf-transform/core`, which is the version to pin if you want reproducibility.
The repository layout shows a Yarn workspaces monorepo with a `packages/` directory and `lerna.json`, plus a `benchmarks/` directory and a `deno.lock` alongside `yarn.lock`. That last detail is worth pausing on: a Deno lockfile means the SDK is tested under Deno as well as Node, which matches the three I/O classes the README offers.
The `package.json` scripts describe the developer loop, and it is a mature one. Tests run through `node --test` across the package test directories, with a separate `test:deno` script that grants specific permissions via `--allow-env --allow-ffi --allow-read --allow-write --allow-sys`. Coverage uses `c8`. Linting uses Biome rather than ESLint or Prettier, via `biome check packages/*/{src,test}`.
Two scripts stand out. The `version` script runs tests, cleans, builds, then runs the benchmark with `--report-version --print` and regenerates the CLI documentation by capturing `gltf-transform -h --no-editorial` output and injecting it between marker comments in the docs. And `postpublish` pushes commits and tags then deploys docs to production. In other words, the published documentation for the CLI is generated from the binary at release time, which is why the CLI reference cannot drift from the actual commands.
The `--no-editorial` flag in that command is a hint that the CLI has editorial output, presumably notes about what a given command does to your model, which you can suppress when scripting.
Editorial conclusion
Adopt glTF-Transform if you are shipping 3D models and need a repeatable step that prunes, deduplicates and compresses them, in a Node build pipeline or a browser tool, given the core SDK is compatible with both. Do not reach for it to make artistic changes to geometry, materials or animation, which the README assigns to modelling tools, and do not assume the CLI's texture commands work in every runtime, since Sharp is Node.js only and the CLI is documented as supported in Node.js LTS versions. Verify first which runtimes you need, since `WebIO`, `NodeIO` and `DenoIO` differ, and check that your Draco and KTX2 decoders are registered before reading a compressed model.
Frequently asked questions
What is the difference between a 3D modelling tool and glTF-Transform?
The README draws the line directly: modelling tools are ideal for artistic changes to geometry, materials and animation, while glTF Transform provides fast, reproducible and lossless control of the low-level details of a model. It is aimed at bundling, splitting and optimising existing models.
How do I install the glTF-Transform scripting API?
Run `npm install --save @gltf-transform/core @gltf-transform/extensions @gltf-transform/functions`. For the CLI instead, `npm install --global @gltf-transform/cli`, which is supported in Node.js LTS versions.
Do I need to register Draco to read a compressed glTF model?
Yes. The I/O configuration calls `registerDependencies` with `draco3d.decoder` and `draco3d.encoder`, both marked optional in the example. Extensions are registered separately with `registerExtensions`, and both registrations are needed for documents that use those features.
Which compression should I use, Draco or Meshopt?
Draco compresses geometry and takes a `--method` such as `edgebreaker`. Meshopt compresses geometry, morph targets and keyframe animation, so it is the one to pick when a model has morph targets or baked animation.
Can glTF-Transform run in a browser?
The core SDK is compatible with both Node.js and Web, with WebIO, NodeIO and DenoIO as the platform utilities. But `textureCompress` is documented as requiring glTF Transform v3 and Node.js because Sharp is Node.js only, so the full optimisation pipeline is not browser-safe.
How do I write a custom transform?
A transform is a function taking options and returning a function that takes the document. The README's example returns a closure that iterates `document.getRoot().listMaterials()` and calls `material.setDoubleSided`, and it is passed to `document.transform()` alongside the built-in functions.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/donmccurdy-gltf-transform)Was this useful?