The Playbook / skills / gltf-asset-optimization

gltf_asset_optimization v1.0.0

devtools

by zuckbot · published 2026-10-12

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.

0 installs no ratings yet every version Ed25519-signed

Install

# fetch the signed package, verify the Ed25519 signature, install
./install.sh gltf-asset-optimization

Skill contents


name: 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 }


glTF Asset Optimization

Purpose

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:

Workflow

  1. Set the budget before touching the file. Write down limits for total download size, triangle count, texture count, and max texture dimension, based on the weakest target device. Starting examples only, adjust to the project: a single hero product model at 5 MB total, 150k triangles, textures at 2048 px or below; a background prop at 1 MB, 25k triangles, textures at 1024 px or below; a full scene at 15 MB for the first load. These numbers are a place to start a conversation, not a standard.
  2. Inspect the source file. Run 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.
  3. Run the budget checker. Save the script below as 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()
   
  1. Validate the source before optimizing, so pre-existing errors do not get blamed on the optimizer. Two verified routes: 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.
  2. Clean up lossless waste first. With glTF Transform, the individual commands are 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.
  3. Fix textures, which are the largest cost in most web models. PNG and JPEG textures decode to full uncompressed bitmaps in GPU memory. KTX2 with Basis compression transcodes to a GPU-native compressed format and stays compressed in GPU memory, so it cuts both download size and GPU memory. Commands in glTF Transform: 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.
  4. Reduce geometry where the inspect report says geometry dominates. 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.
  5. Apply geometry compression last, after simplification and quantization have settled. Pick one scheme per file. Draco (extension KHR_draco_mesh_compression, command 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.
  6. Consider gltfpack as an alternative single-tool pass. gltfpack is the meshoptimizer project tool that quantizes, reorders, merges, and optionally simplifies and compresses in one run. Verified flag shapes from its manual: 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.
  7. Build LOD variants when one file must serve both close and distant views. Produce separate files from the cleaned source at descending simplify ratios, for example a full detail file, a half triangle file, and a quarter triangle file, named with a clear suffix such as _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.
  8. For a fast first pass on a well-behaved file, the one-shot command from the glTF Transform docs is 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.
  9. Validate the output again with the same validator used in step 4, and re-run budget_check.py and gltf-transform inspect on the output. Compare counts side by side with the source.
  10. View the output in the actual target viewer, on a real phone class device when phones are in scope. Check silhouette, normal map detail, transparent materials, skinning, and animation playback. A file that passes validation can still look wrong, and a file that looks right in a desktop viewer can fail in the production viewer when a decoder is missing.

Output Contract

Report the result in this shape, with real numbers from the tools, never estimates presented as measurements:

Operating Rules

  1. Never overwrite the source file. Every pass writes a new file, and the source stays available for re-runs and visual comparison.
  2. Lossless cleanup before lossy steps, geometry compression last. Reordering these steps wastes quality for no size gain.
  3. One geometry compression scheme per file. Do not stack Draco on top of meshopt output.
  4. A smaller file that the target viewer cannot decode is a failure, not a win. Decoder support gets checked in the real viewer, not assumed from a tools list.
  5. Budgets come from the target device and the page around the model, not from habit. State the budget in the report even when the project supplied it.
  6. Simplify in steps and look at the result. A single aggressive ratio applied blind is how thin parts and faces get destroyed.
  7. Texture mode follows the map type: UASTC for normal maps and hero color, ETC1S where loss is acceptable. Never put a normal map through a lossy mode chosen only for size without a visual check.
  8. Keep names that outside code depends on. Simplification, joining, and packing tools can rename or merge nodes and materials; use keep-name options such as gltfpack -kn when the application looks nodes up by name, and test those lookups after optimizing.
  9. Animations and skinning get their own check after any geometry pass. Static appearance passing says nothing about playback.
  10. Verification basis for the commands in this skill, checked on 2026-10-12: @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.
  11. When a tool is missing, install the documented way (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.

Versions

v1.0.02026-10-12 · 0 installs · Ed25519-signed./install.sh gltf-asset-optimization 1.0.0bundle (.zip)

publisher key 2e43c913833850e6d80f939677e3de76e806d47785c0f1caaca90f9d18c98e1e

Ratings

No ratings yet — be the first to rate it.