Back to Directory/Automation & Workflow

dev.glbforge/glbforge

Make AI-generated 3D assets web-ready: budgets, SSIM-verified optimization, STL/USDZ export.

Automation & WorkflowTypeScriptv0.8.0

GLBForge — the web-readiness layer for AI-generated 3D

glbforge.dev · raw AI mesh in, shipped asset out

Makes AI-generated 3D assets (Meshy, etc.) actually shippable on the web: analyze → optimize → scaffold a viewer, with performance budgets as a CI-able contract.

Status

  • ✅ glbforge inspect — the edit-loop read (sub-second): one shell or floating pieces, watertight or holes/overlaps, size in metres, up axis, origin placement, unapplied/mirrored transforms — each finding a versioned rule id (topo/open-edges from core-geometry@1) with measured/heuristic certainty, a likely cause with confidence, and a concrete fix. front is never guessed. Same thing as the MCP inspect tool: call it after every edit.
  • ✅ glbforge diff <before> <after> — what changed since the last edit, including what it broke: per-part size deltas ("'legs' is 30% narrower along X"), triangle and shell deltas, topology regressions (was watertight, now has 3 open loops), origin drift, node transforms, meshes added or removed, and --visual front/side/top/iso SSIM with cameras fixed to the before framing. Same thing as the MCP diff tool.
  • ✅ glbforge analyze — budget report card + named lint rules
  • ✅ glbforge optimize — weld/simplify/LOD/compress to hit the budget
  • ✅ glbforge scaffold — emit a React Three Fiber viewer for the optimized asset
  • ✅ glbforge meshy — generate/download via Meshy REST API (image/text → 3D, --optimize glue)
  • ✅ MCP server wrapping all of the above (@glbforge/mcp, 8 tools)
  • ✅ GLBForge Studio (glbforge ui) — local web UI: drag-drop analyze, one-click optimize with a before/after compare slider in the viewport, logo forging, Meshy generation with live progress, STL export. Zero hosting; runs entirely on your machine.
  • ✅ glbforge extrude — deterministic logo/graphic → 3D (no AI, no credits): traces the silhouette (marching squares → Douglas-Peucker → earcut), extrudes it watertight, and projects the source image back on as the texture. For 2D artwork (logos, wordmarks) this beats image-to-3D generation outright: ~1.5k tris vs ~2M, exact silhouette, original colors. --bevel adds a signage-style rounded rim (miter-limited, clamp-probed insets + a crack stitching pass keep it watertight even on thin graffiti strokes; verified 0 boundary / 0 non-manifold edges on real logo fixtures). Photographic inputs are detected and routed to Meshy instead. Also exposed as the extrude_image MCP tool.

Usage

pnpm install && pnpm build
node packages/cli/dist/index.js analyze fixtures/veiled-guardian.glb --profile mobile-hero
node packages/cli/dist/index.js optimize fixtures/veiled-guardian-tex4k.glb -o out.web.glb --lods 40000,10000
node packages/cli/dist/index.js scaffold out.web.glb -o viewer && cd viewer && pnpm install --ignore-workspace && pnpm dev
node packages/cli/dist/index.js ui model.glb   # GLBForge Studio on localhost:5177

inspect <file> (glb/gltf/usdz/usda/usdc) answers what an agent editing a mesh gets wrong blind, in ~70 ms on 150k triangles. --profile authoring (default) treats topology problems as warnings because they are most likely the last edit's doing; a budget profile (mobile-hero) reports them as info. --packs core-geometry@1,core-scene@1 pins rule packs, --no-topology skips the welded pass (rules are listed as skipped, never silently absent), --strict exits 1 on warnings, --json emits the full report. --expect "chair, Z-up, meters, single-shell, 0.4-1.2m tall, front -Y, watertight, origin base" turns the run into a contract (intent@1): shell count, watertight, size range and origin are measured and fail as errors (exit 1); a bare category gives a plausibility warning from a coarse size table with a stated confidence; front is recorded as declared and never measured; Z-up on a GLB is informational because glTF is Y-up by definition. Tokens the parser cannot place are reported, not dropped.

glbforge usage is the local, opt-in counter behind the one metric that says whether GLBForge is in the loop: invocations per asset. Off by default; opt in with glbforge usage --enable or GLBFORGE_USAGE=1. Every inspect / diff / analyze (and every MCP call) then appends one JSON line to ~/.config/glbforge/usage.jsonl (GLBFORGE_CONFIG_DIR to move it). Nothing is ever sent anywhere; --clear deletes it. Because every edit changes the file's hash, assets are grouped into lineages: the same path in the same session, the same path within two hours, a diff edge (before → after), or an explicit --lineage <id>. The report gives median / p90 invocations per lineage and the share of assets inspected at least five times.

analyze flags: --profile mobile-hero|desktop-hero|product-configurator, --json, --no-topology. Exits non-zero when the asset is over budget — wire it into CI like a linter. optimize flags: --target <tris>, --lods a,b, --no-textures, --no-compress, --no-verify. LOD files (--lods 40000,10000) are geometry-only: materials stripped, positions welded, meshopt-simplified, and when non-manifold topology stalls the simplifier (stacked forge layers, doubled shells) they fall back to grid vertex clustering so the target is always reached — the CLI and the Action report say grid-clustered when that happened.

Measured on the included Meshy 7 fixture (4K textures, 1.99M tris): 89.4MB → 5.5MB (93.8% smaller), score 25 → 100, ~7s. Pipeline: dedup → weld → meshopt-simplify to budget (error ladder) → fill missing normals → texture resize + WebP (normal maps near-lossless) → prune → EXT_meshopt_compression → perceptual verification.

"No visible loss" is measured, not claimed

Every optimize/ship renders the asset from four fixed cameras before and after (deterministic software rasterizer, 2x supersampled, smooth shading, base-color textures) and scores the pairs with SSIM. The weakest view must clear the profile's minSsim floor (mobile-hero 0.94, product-configurator 0.95, desktop-hero 0.96) or the report card gets an error-severity fidelity/perceptual finding and the command exits non-zero, exactly like a budget violation. A passing run records the number as an info finding, so the score ships with the report everywhere (CLI, --json, MCP, Studio).

  visual fidelity ✓ SSIM 96.7%  weakest view 95.8% @ verify_135 · floor 94.0% · geometric deviation ≤ 0.1%

On the fixture: the budget pass (1.99M → 150k tris) measures 0.958; forcing 40k tris drops to 0.896 (fails, and it should — hair strands merge); 10k measures 0.73. glbforge verify <candidate> <reference> scores any two files the same way. The cameras are fixed to the reference's bounds so a shifted or shrunken result cannot re-frame itself into a good score.

Packages

packagepurpose
@glbforge/corepure analysis library: stats, topology, rules, budgets
glbforge (CLI)glbforge command-line interface
@glbforge/meshytyped Meshy REST client: tasks, polling w/ backoff, downloads
@glbforge/mcpMCP server: validate / inspect / render GLB and USD with stable error codes + prim paths, compact report cards, optimize/ship/forge/STL/USDZ with dry-run + diff, generation

MCP server

.mcp.json registers the server for Claude Code automatically (build first: pnpm build). For other clients:

claude mcp add glbforge -- node /path/to/XUI/packages/mcp/dist/index.js

Built for agents. Every tool answers one envelope — { ok, summary, duration_ms, errors[], data } — where errors[] carries every diagnostic with a stable code (docs/error-codes.md), a severity and the prim path it refers to, and every response validates against schemas/<tool>.output.json. validate, inspect_geometry, inspect_animation, inspect_materials, analyze_performance and inspect_all read GLB, glTF, USDZ, USDA and USDC (pure-TypeScript USD readers, single layer); render / render_animation_strip draw any of them, posed at any animation time. Mutating tools take dry_run and return a diff plus post_validation, and report every change they made on their own (generated normals, joined primitives, transcoded textures…) as a coded entry. Results are compact cards (verdict, key numbers, every error plus top findings, nextActions, a drillDown pointer) with inspect_report for the full findings/textures/topology on demand; every tool that touches a GLB returns a rendered thumbnail or 2x2 turntable, and a failing optimization returns a reference | result | change-heatmap sheet so the agent sees where the loss is (compare_glb for any two files). capabilities says which providers have keys and whether KTX2 is available before a plan is made; every written file reports its sha256; read-only tools carry readOnlyHint so clients can auto-approve them. Generation tools are deliberately split into create/status/download — tasks take minutes, and agents poll at their own pace instead of holding a tool call open.

Animated assets

Skinned meshes and morph targets go through a bone-aware path: meshoptimizer's attribute-aware simplifier sees the skin weights (and per-target morph deltas) as vertex attributes, vertices on dominant-joint boundaries are locked, and every attribute — JOINTS_n, WEIGHTS_n, each target — is compacted with the same remap. Skins, inverse bind matrices, and animation clips survive untouched; analyze reports scene/animated-asset. Tested on a rigged cylinder with a rotation clip and a morph target (blend band preserved, weights normalized, deterministic).

USDZ for iOS AR

npx glbforge usdz model.web.glb          # → model.usdz (PNG textures; --jpeg for smaller)

Writes a binary USD (usdc, crate 0.8.0) layer with UsdPreviewSurface materials (base color, metallic/roughness via channel outputs, normal, occlusion, emissive, alpha mask/blend), transcodes WebP textures to PNG/JPEG, and packs a store-only zip with 64-byte-aligned payloads as the spec requires. The crate writer is GLBForge's own, pure TypeScript — its byte layout was verified section-by-section against files from Pixar's writer, and test/usd-oracle.py opens the usdc and its usda twin with Pixar's USD and checks every prim, value, connection, and relationship for equality (set GLBFORGE_PXR_PYTHON to a python with usd-core to run it). On the Meshy fixture the layer is 5.4MB binary vs 13.4MB ASCII. --usda writes the text layer instead. Animated assets export as UsdSkel: joints ordered parents-first with bind and rest transforms, SkelBindingAPI joint indices/weights on the meshes, and the first animation clip sampled at 30 fps into a SkelAnimation (translations, quaternion rotations, half scales). The oracle test asks Pixar's UsdSkel to recompute joint transforms from the result. Morph targets become UsdSkel BlendShape prims with the weight track sampled alongside (a morph-only mesh gets a one-joint skeleton so UsdSkel applies them). Primitives without a material get a neutral default one, since an unbound mesh renders as RealityKit's magenta "missing material" pattern. KTX2 inputs are rejected with guidance. Also export_usdz on the MCP server and an Export USDZ button in the Studio (in-browser too — textures transcode through a canvas). Reference it from <model-viewer ios-src="model.usdz"> for the AR button on iOS. Verified on an iPhone in AR Quick Look: flat-material, fully textured (JPEG color, PNG normal and metallic/roughness), skinned + animated, and blend-shape exports all open and play correctly.

glbforge init — make a project agent-ready

npx glbforge init            # in your project; idempotent

Writes a marker-delimited section into CLAUDE.md (what the budget is, which scripts and MCP tools to use, the forge-vs-generate routing rule, and that "no visible loss" is measured), adds glb:check / glb:analyze / glb:optimize / glb:ship / glb:verify / glb:studio npm scripts (plus glbforge as a devDependency), and registers the MCP server in .mcp.json (--client cursor|both for .cursor/mcp.json). Existing content, other MCP servers, and your own scripts are preserved; a second run reports everything unchanged. Flags: --profile, --assets <dir>, --local, --dry-run, --force, --no-claude-md, --no-scripts, --no-mcp. From then on any agent session in that project picks GLBForge up automatically.

glb:check runs glbforge audit <dir>: every GLB in the folder against the budget, non-zero exit if any fails — the pre-commit / CI gate.

GitHub Action

# .github/workflows/assets.yml
on: pull_request
permissions: { contents: write, pull-requests: write }
jobs:
  assets:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
        with: { fetch-depth: 0, lfs: true }
      - uses: glbforge/glbforge@main
        with:
          profile: mobile-hero
          optimize: true      # open a PR with the web-ready files

Runs only on GLB/glTF files changed in the pull request. Every run posts a sticky report-card comment and gates the check on the budget. With optimize: true it also optimizes each failing asset (<name>.web.glb beside the original, or in place with replace: true), verifies it perceptually, and opens or updates a pull request against the PR branch with the results — before/after table, visual SSIM, and the pipeline steps. Same input, profile, and CLI version always produce identical bytes, so outputs are cached by content hash (cache: true) and re-runs are no-ops. Fork PRs cannot receive a bot branch; their outputs are uploaded as a workflow artifact instead. Opening the PR needs the repository (and, for organizations, the org) setting Allow GitHub Actions to create and approve pull requests; when it is off, the run still posts the report card and uploads the optimized files as the glbforge-optimized artifact. In optimize mode the gate passes when the optimized output passes, so the fix is always one merge away. target-triangles overrides the profile's triangle target and lods: 40000,10000 adds geometry-only LOD files (<name>.web.lodN.glb) to the PR; both are part of the cache key. Inputs: profile, fail-on-budget, optimize, optimize-all, replace, verify, target-triangles, lods, cache, open-pr, pr-branch, version, token.

Meshy API key

Copy .env.example to .env and set MESHY_API_KEY (gitignored, loaded automatically, never overrides real env vars). Or keep it out of files entirely via macOS Keychain:

security add-generic-password -a "$USER" -s meshy-api-key -w
# ~/.zshrc: export MESHY_API_KEY=$(security find-generic-password -s meshy-api-key -w)

Full loop, one command:

node packages/cli/dist/index.js meshy image art.png --pbr --optimize -p mobile-hero -o hero.glb

Open-model generation (fal.ai)

True image→3D via open weights on GPU inference — often 5-25x cheaper than commercial generation. Set FAL_KEY (https://fal.ai/dashboard/keys):

node packages/cli/dist/index.js gen photo.png --model hunyuan --optimize   # Hunyuan3D-2
node packages/cli/dist/index.js gen photo.png --model trellis             # TRELLIS
node packages/cli/dist/index.js gen photo.png --model triposr             # fastest

The studio's generate panel picks up every configured provider automatically (Meshy + the open trio side by side, with per-model credit costs in hosted mode).

Rules

perf/* budget violations fail the build; geo/*, topo/*, mat/*, tex/*, scene/* describe defects typical of AI-generated assets, each with a concrete fix. See packages/core/src/rules.ts.

Design decisions

  • Pure Node/TS, no Blender dependency. Analysis and (upcoming) optimization run on @gltf-transform + meshoptimizer — native to glTF, no lossy DCC round-trip, installable via npx, CI-friendly. Ops are designed as pluggable backends so Blender-only capabilities (retopo, UV unwrap, baking) can be added later without changing the CLI surface.
  • Topology in welded space. Boundary/non-manifold counts are computed after unifying position-duplicate vertices, so unwelded exports don't produce garbage numbers.
  • Budgets are profiles, not advice. An asset passes or fails a named, versioned target (mobile-hero@1 pins; bare mobile-hero = latest; a cap never changes in place). The methodology behind every cap and the version changelog are in docs/BUDGETS.md / glbforge.dev/budgets. Determinism makes it automatable.

Fixtures

  • fixtures/veiled-guardian.glb — Meshy 7 High-Detail geometry-stage export (1.99M tris, POSITION-only, 34MB). Welded + manifold; tests assert we don't cry wolf.
  • fixtures/veiled-guardian-tex4k.glb — same asset after the 4K texture stage (89MB). Its 77k position-duplicate vertices are UV-seam splits, not waste; the topo/unwelded rule distinguishes these (regression-tested).

Installation

Source-derived launch command. Check the maintainer’s required arguments and credentials before running:

bash
npx -y @glbforge/mcp

Set up in your AI client

Merge this template into ~/Library/Application Support/Claude/claude_desktop_config.json. Keep existing servers. Add any arguments, credentials, and permissions required by the maintainer; this template has not been install-tested.

json
{
  "mcpServers": {
    "dev-glbforge-glbforge": {
      "command": "npx",
      "args": [
        "-y",
        "@glbforge/mcp"
      ]
    }
  }
}

Restart Claude Desktop completely for changes to take effect. Confirm the server appears connected in the client’s tool list, then try a read-only example from its documentation.

Claude Desktop setup reference

Package

@glbforge/mcpnpm

Compatible MCP Clients

dev.glbforge/glbforge works with any MCP-compatible client. Copy the config snippet from the Configuration section above and add it to the file shown for your client, then restart the application.

  • Claude Desktop~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.
  • Cursor~/.cursor/mcp.jsonRestart Cursor for changes to take effect.
  • VS Code.vscode/mcp.jsonReload VS Code window for changes to take effect.
  • Windsurf~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect.
  • Claude Code.mcp.jsonSave at the project root, then start Claude Code in that project and review the MCP server approval prompt. Keep real credentials out of shared files.

Learn More