Optimize glTF and GLB models for web delivery: set size budgets, inspect and validate, compress textures with KTX2/Basis or WebP/AVIF, simplify geometry, apply Draco or meshopt compression, and build LOD variants. Triggers on shrinking a GLB, a model loading slowly on a web page, texture or triangle budgets, or preparing 3D assets for a browser viewer.
./install.sh gltf-asset-optimizationname: gltf_asset_optimization description: "Optimize glTF and GLB models for web delivery: set size budgets, inspect and validate, compress textures with KTX2/Basis or WebP/AVIF, simplify geometry, apply Draco or meshopt compression, and build LOD variants. Triggers on shrinking a GLB, a model loading slowly on a web page, texture or triangle budgets, or preparing 3D assets for a browser viewer." metadata: { "includeInPrompt": true }
Take a glTF or GLB file that is too big or too heavy for a web page and produce a smaller file that still looks right in the target viewer. The work is asset preparation: budgets, inspection, texture compression, geometry simplification, geometry compression, level of detail variants, and validation. Writing the viewer code that loads the result is a different job.
Cousin skills, one line each:
gltf-transform inspect input.glb and read the report: it shows whether the weight sits in geometry, textures, or draw calls. Run the budget checker in step 3 for file size, triangle count, and which extensions the file already uses or requires.budget_check.py, then run python3 budget_check.py input.glb --max-mb 5 --max-tris 150000. It reads only the GLB JSON chunk with the Python standard library, so it gives counts before any tool modifies the file.python
#!/usr/bin/env python3
"""Report GLB size and counts, and flag budget breaches.
Usage: python3 budget_check.py MODEL.glb [--max-mb 5] [--max-tris 100000]
[--max-textures 16]
Reads only the GLB JSON chunk. Exit code 1 when any stated budget is breached.
"""
import argparse, json, struct, sys
def parse_glb(path):
data = open(path, "rb").read()
if len(data) < 20 or data[0:4] != b"glTF":
sys.exit("not a GLB file (missing glTF magic)")
version, total = struct.unpack_from("<II", data, 4)
if version != 2:
sys.exit(f"unsupported GLB version: {version}")
clen, ctype = struct.unpack_from("<I4s", data, 12)
if ctype != b"JSON":
sys.exit("first chunk is not JSON")
return data, json.loads(data[20:20 + clen])
def main():
ap = argparse.ArgumentParser()
ap.add_argument("path")
ap.add_argument("--max-mb", type=float, default=None)
ap.add_argument("--max-tris", type=int, default=None)
ap.add_argument("--max-textures", type=int, default=None)
a = ap.parse_args()
data, doc = parse_glb(a.path)
accessors = doc.get("accessors", [])
tris = 0
for mesh in doc.get("meshes", []):
for prim in mesh.get("primitives", []):
if prim.get("mode", 4) != 4:
continue
if "indices" in prim:
tris += accessors[prim["indices"]].get("count", 0) // 3
elif "POSITION" in prim.get("attributes", {}):
tris += accessors[prim["attributes"]["POSITION"]].get("count", 0) // 3
mb = len(data) / 1_000_000
print(f"file: {a.path} size: {mb:.2f} MB ({len(data)} bytes)")
print(f"nodes: {len(doc.get('nodes', []))} meshes: {len(doc.get('meshes', []))} "
f"materials: {len(doc.get('materials', []))} textures: {len(doc.get('textures', []))} "
f"images: {len(doc.get('images', []))} animations: {len(doc.get('animations', []))}")
print(f"triangles (indexed/non-indexed triangle primitives only): {tris}")
print(f"extensionsUsed: {doc.get('extensionsUsed', [])}")
print(f"extensionsRequired: {doc.get('extensionsRequired', [])}")
bad = []
if a.max_mb is not None and mb > a.max_mb:
bad.append(f"size {mb:.2f} MB over budget {a.max_mb} MB")
if a.max_tris is not None and tris > a.max_tris:
bad.append(f"triangles {tris} over budget {a.max_tris}")
if a.max_textures is not None and len(doc.get("textures", [])) > a.max_textures:
bad.append(f"textures {len(doc.get('textures', []))} over budget {a.max_textures}")
for b in bad:
print("BREACH:", b)
sys.exit(1 if bad else 0)
if __name__ == "__main__":
main()
gltf-transform validate input.glb, or the Khronos validator binary gltf_validator input.glb, which writes a report file named <asset_filename>.report.json next to the asset and returns a non-zero exit code when it finds at least one error. Fix or record source errors first.dedup (deduplicate accessors and textures), prune (remove unreferenced properties), weld (merge equivalent vertices), resample (losslessly deduplicate animation keyframes), and sparse (store mostly-zero arrays as sparse accessors). Run them one at a time, in the shape gltf-transform dedup input.glb step.glb, and re-inspect between passes while learning a new asset type.etc1s (KTX2 with Basis ETC1S, smaller files, lower quality) and uastc (KTX2 with Basis UASTC, higher quality, larger files). Rule of thumb: ETC1S for secondary color maps and data maps that tolerate loss, UASTC for hero color textures and normal maps. Cap dimensions first with resize. WebP (extension EXT_texture_webp) and AVIF shrink downloads but still decode to full bitmaps on the GPU, and loader support varies more than KTX2 support, so check the target viewer before choosing them over KTX2. For per-file work outside a GLB, KTX-Software provides the toktx tool that creates KTX2 files; check toktx --help on the installed version for exact flags before scripting it.simplify reduces vertex count using meshoptimizer under the hood; quantize reduces precision and memory. Check gltf-transform help simplify on the installed version for the exact ratio and error option names, then simplify in small steps and view the result each time. Silhouettes, thin parts, and skinned joints break first, so protect them with lower ratios or exclude those meshes.gltf-transform draco) gives strong compression and wide viewer support. Meshopt (extension EXT_meshopt_compression, command gltf-transform meshopt) usually decodes faster and also compresses animation. Either way, the file becomes dependent on a decoder at load time: the target viewer must be configured with the matching decoder or the model will not load. Confirm decoder support in the actual viewer before shipping, and keep an uncompressed fallback when the viewer is unknown.gltfpack -i input.glb -o output.glb -cc for meshopt compression (single -c is the lighter setting), add -tc to convert textures to KTX2 with Basis, add -si 0.5 to simplify toward half the original triangle count, and add -kn when outside code needs to find named nodes after packing. gltfpack changes data aggressively by default, so diff its output visually against the source before adopting it in a pipeline._lod0, _lod1, _lod2. Switching between them by distance happens in application code. An in-file LOD extension exists in some engines, but support is uneven; confirm the target viewer supports it before relying on it, otherwise ship separate files.gltf-transform optimize input.glb output.glb --compress draco --texture-compress webp. Swap the values to match steps 6 and 8 (for example --compress meshopt and --texture-compress ktx2 are the expected shapes; confirm the accepted values with gltf-transform optimize -h on the installed version). Defaults in optimize may not fit every scene, so treat its output as a draft and still run steps 12 and 13.budget_check.py and gltf-transform inspect on the output. Compare counts side by side with the source.Report the result in this shape, with real numbers from the tools, never estimates presented as measurements:
extensionsRequired in the budget checker output, plus a line confirming the target viewer has the decoders for them or stating that this was not confirmed.-kn when the application looks nodes up by name, and test those lookups after optimizing.@gltf-transform/cli version 4.5.1 from npm view; the command list, the optimize example, and the install command from the official docs at gltf-transform.dev, which list version 4.5.0; gltfpack flags from the gltfpack manual page covering -i, -o, -c, -cc, -tc, -si, and -kn; validator usage and its report file naming from the KhronosGroup glTF-Validator repository readme; meshoptimizer npm package at version 1.3.0 and the gltf-validator npm package at 2.0.0-dev.3.10 from the npm registry. Where a flag was not confirmed this way, the workflow says to check the installed tool help, and that check is part of the job, not optional.npm install --global @gltf-transform/cli for glTF Transform; gltfpack and gltf_validator ship as standalone binaries from their project releases) or state plainly that the step was skipped. Do not substitute a remembered command for a missing tool.publisher key 2e43c913833850e6d80f939677e3de76e806d47785c0f1caaca90f9d18c98e1e
No ratings yet — be the first to rate it.