@r2u/glb-pipeline
v0.2.1
Published
Manifest-driven GLB texture optimization, KTX2 compression, and LOD generation pipeline.
Downloads
796
Keywords
Readme
@r2u/glb-pipeline
Manifest-driven GLB pipeline for texture optimization, KTX2 compression, material overrides, and geometry-only LOD generation.
Requirements
- Node.js 22 or newer.
- KTX-Software
ktxCLI for KTX2 steps.- Linux x64/arm64: downloaded on demand and verified by SHA-256 when it is not on
PATH. - macOS/Windows: install KTX-Software and add
ktxtoPATH.
- Linux x64/arm64: downloaded on demand and verified by SHA-256 when it is not on
- Linux automatic provisioning also requires
tarwith bzip2 support.
No binary is downloaded during package installation.
Install
npm install --save-dev @r2u/glb-pipeline
# or
yarn add --dev @r2u/glb-pipelineGlobal installation is also supported:
npm install --global @r2u/glb-pipelineCLI
r2u-glb build --manifest ./models/manifest.yml
r2u-glb build --manifest ./models/manifest.yml --dry-run
r2u-glb optimize --input ./models --output ./models/optimized --keep-nodes
r2u-glb lod ./models/model.glb --ratios 1,0.5,0.2 --keep-nodesWithout a global install, use npx @r2u/glb-pipeline ... or yarn r2u-glb ....
Run r2u-glb <command> --help for every option.
Manifest
Manifest paths are relative to the manifest directory. Outputs must remain in a subdirectory of that root. Unknown fields are rejected to catch typos.
version: 1
output: optimized
presets:
full:
steps:
- type: ktx2
slots:
baseColor: { mode: uastc, size: 2048 }
normal: { mode: uastc, size: 1024 }
metallicRoughness: { mode: etc1s, size: 512 }
- type: lod
ratios: [1.0, 0.5, 0.2]
lite:
output: optimized-light
steps:
- type: ktx2
slots:
baseColor: { mode: etc1s, size: 1024 }
normal: { mode: etc1s, size: 512 }
- type: lod
ratios: [0.75, 0.5, 0.2]
assets:
- input: 'models/*.glb'
preset: full
- input: 'models/*.glb'
preset: liteThe unversioned manifest format used by r2u-maquete-imersiva is accepted as version 1. The complete example is in examples/manifest.yml, and the JSON Schema is exported as @r2u/glb-pipeline/manifest-schema.json.
Steps
texture: resize and encode selected slots as WebP, JPEG, or PNG.ktx2: encode globally or per slot with UASTC/ETC1S.lod: generate geometry-only siblings named<model>_LOD_<index>.glb.draco: compress geometry in the main GLB and every LOD generated by earlier steps. Must appear only once and be the final step.
Steps run in declaration order. A source can target multiple outputs, which is how normal and lite variants are produced.
keepUniqueNames defaults to true for texture and ktx2, including preset steps and the exported optimization defaults. It maps directly to dedup({ keepUniqueNames }): equivalent materials, meshes, textures and skins with different names remain distinct. Accessor deduplication is unchanged. Set keepUniqueNames: false on a step or use standalone optimize --no-keep-unique-names to restore name-insensitive deduplication; --keep-unique-names explicitly enables preservation. Equivalent resources with the same name can still merge, and unused resources can still be pruned. This control does not replace keepNodes: with keepNodes: true, meshes are excluded from deduplication even when keepUniqueNames: false; with keepNodes: false, distinct mesh names are protected by keepUniqueNames: true, but empty/detached nodes can still be pruned. LOD continues to omit all materials/textures and deduplicate only accessors; Draco does not perform deduplication. Neither step accepts keepUniqueNames. A later optimization step with explicit false can merge resources preserved earlier.
steps:
- type: texture # keepUniqueNames: true is implicit
- type: ktx2
keepUniqueNames: false # allow merging equivalent resources with different nameskeepNodes defaults to true for texture, ktx2, and lod; it can be omitted from the manifest. This preserves all nodes, including empty and detached nodes. Set keepNodes: false on an individual step to allow pruning; standalone optimize and lod accept --no-keep-nodes for the same opt-out. --keep-nodes remains supported. The exported optimization and LOD defaults also use true.
For texture and ktx2, preservation excludes nodes and meshes from pruning and excludes meshes from deduplication, retaining node/mesh associations and mesh names. Other unused resources can still be cleaned up. LOD generation may still remove empty geometry/meshes during simplification and always omits materials/textures. A later step with explicit keepNodes: false can remove nodes preserved by earlier steps.
steps:
- type: texture # keepNodes: true is implicit
- type: lod
keepNodes: false # explicitly allow node pruning in this stepDesign choice: In glTF-Transform 4.5.0, prune() removes empty scene leaves recursively and can remove unreferenced nodes. keepLeaves: true only protects leaves in scenes; it does not guarantee preservation of detached nodes. Disabling prune() entirely would also retain unused meshes, textures and other resources. In texture optimization, the default preservation excludes nodes and meshes from prune's propertyTypes instead. Although dedup() does not deduplicate nodes, its default mesh deduplication can replace distinct mesh names referenced by nodes; with keepNodes, mesh deduplication is disabled while other deduplication respects keepUniqueNames. Standalone LOD generation restricts its deduplication to accessors. Preservation is enabled on every manifest step by default; explicit false restores the previous pruning behavior. See the real-GLB regression analysis for a reproducible comparison.
TypeScript API
import { buildAssets, type PipelineLogger } from '@r2u/glb-pipeline'
const logger: PipelineLogger = {
debug: console.debug,
info: console.info,
warn: console.warn,
error: console.error,
}
const result = await buildAssets({
manifestPath: './models/manifest.yml',
dryRun: false,
logger,
})
if (result.errorCount > 0) {
throw new Error(`${result.errorCount} asset(s) failed`)
}Granular APIs such as loadManifest, createBuildPlan, optimizeGlb, and generateLods are also exported. Core APIs return typed results and never terminate the host process.
Draco geometry compression
Draco is opt-in and compatible with KTX2 and WebP/JPEG/PNG texture compression. Add it after texture processing and optional LOD generation:
steps:
- type: ktx2
slots:
baseColor: { mode: etc1s, size: 2048 }
normal: { mode: uastc, size: 1024 }
- type: lod
ratios: [1, 0.5, 0.2]
- type: draco
method: sequential
quantizePosition: 14See examples/draco.manifest.yml for architecture, assets and materials presets. Manifest inputs must follow that example's folder structure; the flat files in temp/ are used by the separate local benchmark.
Draco compresses the staged main file and all LODs created in that asset execution, not existing LODs found on disk. All must succeed before promotion. keepNodes is accepted for consistency, but the Draco stage always preserves node/mesh identity: it does not prune, deduplicate meshes or weld vertices. Non-indexed triangle primitives are indexed in their original order. Other primitive modes may remain uncompressed.
Default method is sequential, deliberately different from glTF-Transform's edgebreaker default. The supplied assets showed a change in triangle index counts with edgebreaker; sequential retained counts. Edgebreaker remains available explicitly and may remove degenerate faces. Neither method guarantees identical vertex ordering or lossless attributes.
Options: method (sequential/edgebreaker), encodeSpeed/decodeSpeed (integers 0–10, default 5), quantizePosition (default 14), quantizeNormal (10), quantizeColor (8), quantizeTexcoord (12), quantizeGeneric (12), and quantizationVolume (mesh default, or scene). Quantization bits must be integers 1–30. More bits generally preserve more precision at a size cost.
Standalone commands support --draco and corresponding kebab-case encoder flags, e.g.:
r2u-glb optimize --input ./models --output ./optimized --keep-nodes --draco
r2u-glb lod ./models/model.glb --keep-nodes --draco --draco-quantize-position 16Direct OptimizeConfig and LodConfig accept optional draco: DracoConfig; omit it to preserve existing behavior. The dedicated compressDraco(inputPath, { outputPath?, config?, dryRun? }) API defaults to replacing the input, uses a temporary file before replacement, and rejects on error. Its dry-run only validates configuration and reads file size; it does not encode. Always provide outputPath to keep an original untouched.
The consumer must support KHR_draco_mesh_compression (e.g. configure DRACOLoader with Three.js GLTFLoader). Draco does not reduce triangle counts by design, does not compress textures and does not guarantee smaller output on already-compressed or tiny geometry. Existing Draco inputs are decoded and recompressed when explicitly requested, potentially adding quantization loss.
Local benchmark: yarn tsx test/benchmark-draco.ts, with three input GLBs in temp/. Outputs and JSON report go to temp/draco-results/; summary and limitations: test/draco-benchmark.md. KTX2 combination validation requires the native ktx binary.
Output safety
Each asset is processed in a staging directory. Existing output files are replaced only after all declared steps for that asset succeed. If processing fails before promotion, existing outputs remain untouched.
Manifest inputs and outputs cannot use absolute paths or .. to escape the manifest directory. Direct low-level APIs still operate on paths explicitly provided by the caller.
KTX cache
The default cache is platform-specific:
- Linux:
$XDG_CACHE_HOME/r2u-glb-pipelineor~/.cache/r2u-glb-pipeline; - macOS:
~/Library/Caches/r2u-glb-pipeline; - Windows:
%LOCALAPPDATA%/r2u-glb-pipeline.
Set R2U_GLB_CACHE_DIR to override it. Use --no-ktx-download to require a preinstalled ktx binary.
Development
yarn install
yarn format:check
yarn lint
yarn typecheck
yarn test
yarn build
npm pack --dry-runSet R2U_TEST_KTX2=1 when running tests to enable the KTX2 integration test.
Publishing
This is a public NPM package with a proprietary license. Public availability does not grant permission to copy, modify, redistribute, or create derivative works; see LICENSE.
The package must exist in the NPM registry before a trusted publisher can be configured. An authorized @r2u maintainer must bootstrap the first beta with 2FA:
npm publish --access public --tag betaAfter the initial publication:
- Create the protected
npmenvironment inr2u-io/glb-pipelineon GitHub. - In the package settings on npmjs.com, configure a GitHub Actions trusted publisher for organization
r2u-io, repositoryglb-pipeline, workflowpublish.yml, environmentnpm, andnpm publishpermission. - Publish subsequent versions by creating a GitHub release.
.github/workflows/publish.ymlvalidates and publishes through OIDC without a long-lived NPM token.
The current publishConfig.tag is beta; change it when promoting a stable release.
