CesiumGS/gltf-pipeline: a glTF optimiser that installs CesiumJS as a dependency
Content pipeline tools for optimizing glTF assets. :globe_with_meridians:
At a glance
- What is it?
- gltf-pipeline is an Apache-2.0 Node tool and library for packing, unpacking, converting and compressing 3D assets, and the details worth reading are the ones the feature list skips: it depends on the full CesiumJS package, its path safety option is bypassed implicitly with a warning, and its destructive optimisations are opt-out rather than opt-in.
- Who is it for?
- gltf-pipeline is the right tool if you have a directory of 3D assets that need to become a binary container, or a binary container that has to become reviewable files, and you want one command that does either without writing your own loader.
- Can I use it commercially?
- Yes. Apache-2.0 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 98 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 28, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Four operations, one tool, two front doors
The feature list is short and the operations are all things you would otherwise write a script for. It converts between the JSON form of glTF and the binary container form, and back again. It writes buffers and textures either embedded in the document or as separate files. It upgrades models authored against the first version of the specification to the second. And it applies Draco mesh compression, which is the operation with the largest effect on file size and the most opportunity to go wrong.
It installs as a command line tool, which is the short version of setup:
npm install -g gltf-pipelineEvery operation has a single letter form on the command line, which is what makes it usable in a build script rather than only by hand:
gltf-pipeline -i model.gltf -o model.glb
gltf-pipeline -i model.gltf -b
gltf-pipeline -i model.glb -o model.gltf
gltf-pipeline -i model.gltf -o modelDraco.gltf -d
gltf-pipeline -i model.gltf -tOnly the input path is required. Everything else has a default, which is worth knowing because the defaults are not all conservative: the separate output flags are off by default, so a run with no flags will embed, and the two output path flags are independent of each other, so a single letter with no output path is a complete instruction.
The other front door is the library. The same operations are exported as functions taking a parsed document and an options object, and they return promises rather than writing files. That design point has a consequence that is easy to miss from the examples and is discussed below: the split between what the tool decides and what you decide is not symmetric, and the interesting half is on your side of the boundary.
The dependency on the whole CesiumJS package
The runtime dependency list has seven entries, and six of them are unremarkable choices for a Node tool: a promise library, a file system helper, a MIME type lookup table, an object hashing utility, an argument parser, and a Draco WebAssembly binding.
The seventh is the one to stop on. The tool depends on the full CesiumJS package, pinned to a minimum of version 1.131.0 and tracking that project's own release numbering. That is a 3D globe and mapping engine, and installing a tool that packs a 3D file format means installing all of it.
The likely reason is that CesiumJS contains a substantial amount of the low level machinery such a tool needs: the matrix and vector types, the geometry helpers, and above all a mature glTF loading implementation that has been hardened against a decade of malformed assets in the wild. Reusing that is a defensible engineering decision, and the version coupling explains something you would otherwise find odd, which is that a small utility package and a global 3D platform are released in lockstep numerically. When the utility's version tracks the platform's version, you are not looking at a stable public interface between two independent projects. You are looking at a build that gets rebuilt when the platform moves.
For a user, that has two practical effects and neither is fatal. The install is heavier than the tool's own code would suggest, and the transitive dependency surface is the 3D engine's, so a security scan of a project that adds this tool will report on packages you have never heard of. The alternative would be to depend on a narrower internal package, which does not appear to exist as a separate publication here.
One more entry in the repository deserves a mention because it is the kind of file that quietly does real work. A third party notice file sits at the root, and given the dependency above, its contents are presumably not short. A tool that vendors or links a large dependency has an obligation to say so, and shipping the file is the start of meeting it.
Every flag has a library twin, and the library hands files back to you
Compare the command line table with the library options and the mapping is one to one. The binary conversion flag corresponds to a dedicated conversion function. The JSON conversion flag corresponds to another. The Draco flag becomes a nested compression options object. The separate textures flag becomes a boolean. The absolute path flag becomes another boolean. The pruning flags become their counterparts.
The library shape is consistent across all of them. You read a file yourself, pass the parsed document plus an options object, and get a promise resolving to a results object:
const gltfPipeline = require("gltf-pipeline");
const fsExtra = require("fs-extra");
const processGltf = gltfPipeline.processGltf;
const gltf = fsExtra.readJsonSync("./model.gltf");
const options = {
dracoOptions: {
compressionLevel: 10,
},
};
processGltf(gltf, options).then(function (results) {
fsExtra.writeJsonSync("model-draco.gltf", results.gltf);
});Note the compression level in that example. Draco exposes a range and the documentation uses the maximum, which is the setting that produces the smallest file at the cost of the most processing time. That single value is the whole tuning surface for the operation that matters most, and it is a reasonable default to document for a build step.
The interesting behaviour is in the separate textures case, because the function does not write the files it produces. It returns them:
const options = {
separateTextures: true,
};
processGltf(gltf, options).then(function (results) {
fsExtra.writeJsonSync("model-separate.gltf", results.gltf);
// Save separate resources
const separateResources = results.separateResources;
for (const relativePath in separateResources) {
if (separateResources.hasOwnProperty(relativePath)) {
const resource = separateResources[relativePath];
fsExtra.writeFileSync(relativePath, resource);
}
}
});The caller iterates the returned map and decides where each resource lands, and the keys are relative paths that the caller is expected to use as given. On the command line, by contrast, the documentation states that separate resources are saved to the same directory as the output file. So the same operation has two different filesystem contracts depending on which front door you came through, and the library one is the more flexible and the more dangerous, because writing arbitrary relative paths is your decision to get right rather than the tool's.
The examples also read input through a file system helper rather than the built-in module, which is a hint that the library expects to be driven from a script with a real project around it rather than from a one liner.
The absolute path option is bypassed by default in one call pattern
There is a security-relevant detail in the documentation that deserves to be read twice rather than skimmed, because it describes an option whose default behaviour depends on how you call the function.
The rule is this. If you do not specify a resource directory, the only paths that can appear inside a glTF document are absolute ones. So the tool has no relative base against which to resolve anything, and the question of whether a document may name a file outside its own source tree does not arise. The documentation then says that if you call the processing function with no resource directory and the document references absolute paths, it behaves as if the absolute option were enabled, but it also emits a warning.
So the flag that allows a glTF file to refer to file URLs outside its source path, which the command line exposes as a required opt-in, is effectively on whenever you use the library in the other style. The mitigation offered is to set it explicitly, which silences the warning and makes the intent visible in your code:
const options = {
allowAbsolute: true,
/*... */
};
const results = await processGltf(gltf, options);Setting the flag to true is not a behaviour change in that situation, because it was already behaving that way. It is a declaration. That distinction matters for anyone auditing a build script, because a reviewer scanning for the flag will find it and conclude the option was deliberately chosen, when in fact the only reason to set it is to stop the warning.
Why this matters beyond tidiness: a glTF document is data, and a document that can name an arbitrary absolute path will cause the tool to read that path. If your pipeline accepts glTF files from anywhere you do not control, the resolution of that path is your security boundary, and the tool's default is permissive in the most common library usage pattern. A resource directory is what narrows it, and the documentation is clear that specifying one is the other half of the pair. The same option appears on the command line with a matching description, and there it defaults to false, so the command line is the safer of the two interfaces by default.
Optimisation here means deleting, and deletion is the default
Two flags in the table are worth reading as warnings rather than as features, because they describe what the tool removes.
The first keeps unused elements, and it is off by default, meaning that unused materials, nodes and meshes are dropped unless you ask otherwise. The second concerns legacy extensions, and it is configured so that when it is false, materials carrying certain older extension identifiers are handled differently rather than preserved. The documentation sentence is cut off in the visible text, but the shape of the option is clear from the name and the default: the modernised output is the default and the legacy form is opt-in.
Both of these are correct behaviour for an optimiser whose job is to make an asset smaller, and both are the kind of decision that should make you look twice before pointing the tool at an asset you cannot regenerate from source. Dropping an unused mesh is usually correct and occasionally a modelling mistake you have not noticed. Rewriting a material to a modern extension set is usually correct and occasionally removes a shading detail that only appears under one particular lighting condition.
The inverse flag is what you reach for when investigating, and the right workflow is to run both and diff. Process an asset with the keeps enabled, process it again with the defaults, and compare the two outputs. A flag called for statistics is provided, which prints numbers about the output document to the console, and that is the cheap first step: the size and element counts before and after tell you whether anything was actually removed before you go looking for what.
The Draco compression path deserves the same caution from the other direction. Compression is a lossy, irreversible transformation, and the output is a different document, not a smaller copy of the same one. Keeping both artefacts, the uncompressed and the compressed, is the only way to make the comparison, which the documentation's own examples do implicitly by writing to different output filenames.
The manifest is a map of the project's era
The dependency list and the configuration files together date this project precisely, and a reader deciding whether to depend on it should know what they are adopting.
The declared engine floor is Node 16 or newer. The linter is on its eighth major version, configured through the older JSON configuration format rather than a flat config, with a shared configuration package published by the same company as the project. The task runner is on its fifth generation, the test framework is Jasmine with a custom reporter, coverage is measured with a tool also on an older major, and the commit hooks run through a hook installer invoked from a prepare script.
The promise library in the runtime dependencies is the clearest single indicator. It predates native promises by years and is now a compatibility shim rather than a necessity, and its presence in a package that must support Node 16 is entirely defensible, while its presence in a package whose most recent commit is from mid-2026 is a sign that the floor and the present have drifted apart. That is not a criticism, it is what a stable utility looks like. The cost is that a maintainer picking this up inherits a build they will want to modernise before they can change anything else, and a user inherits a dependency tree that includes a 3D engine, a promise shim, and a linter on a major version that is no longer current.
Continuous integration reflects the same period. The README carries a build badge pointing at Travis, and there is a Windows configuration file for a hosted build service that predates most of its competitors, alongside a GitHub directory. Two CI systems, one of them older than the other, is a pattern you see in projects where the second was added and the first was never removed.
The one thing that is not dated is the specification work. Upgrading a first generation document to the second, and applying a compression scheme that is itself under active development, is the part of this project that has to keep moving, and the last push in June 2026 suggests it is.
Editorial conclusion
gltf-pipeline is the right tool if you have a directory of 3D assets that need to become a binary container, or a binary container that has to become reviewable files, and you want one command that does either without writing your own loader. It is the wrong tool if you need a slim dependency tree, because installing it installs the CesiumJS 3D globe library with it, and it is the wrong tool for a build that cannot tolerate silent pruning, since unused elements and legacy material extensions are removed unless you ask to keep them. Before running it over anything you cannot regenerate, do a dry run by processing one asset and diffing the result, and read the path policy note, because a file URL outside the source directory is allowed by default in one call pattern and rejected in another, with only a warning to tell you which happened.
Frequently asked questions
How do I convert a glTF file to the binary glb format?
Install the tool globally with npm, then run it with an input path, an output path and the binary flag, or use just the input path and the binary flag to derive the output name. As a library, the same operation is the conversion function that takes a parsed document and an options object with a resource directory.
What does the allowAbsolute option control in gltf-pipeline?
It allows a glTF document to refer to file URLs outside its own source path, and it defaults to false on the command line. If you call the library with no resource directory, the tool already behaves as if it were enabled and emits a warning, and setting it explicitly silences the warning rather than changing the behaviour.
Does gltf-pipeline remove things from my assets by default?
Yes. Unused materials, nodes and meshes are dropped unless you pass the keep unused elements flag, and materials carrying legacy extension identifiers are modernised rather than preserved by default. Run with the keeps enabled and diff the two outputs before trusting either on an asset you cannot regenerate.
Why does installing gltf-pipeline also install CesiumJS?
The package lists the full CesiumJS package as a runtime dependency at version 1.131.0 or newer, which is the same 3D globe and mapping engine. The tool reuses its geometry types and its mature glTF loading code, and the version numbering follows the platform's own release cadence.
How do I apply Draco compression to a model?
Use the compression flag on the command line with an input and output path, or pass a nested options object with a compression level through the general processing function as a library. The documentation example uses the maximum compression level, which produces the smallest file at the cost of the most processing time.
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/cesiumgs-gltf-pipeline)