@three-ws/glb-tools

@three-ws/glb-tools

Inspect, re-theme, and bake GLB models from the shell or CI. The client SDK behind the three.ws 3D asset pipeline — structural inspection, token-themed mesh synthesis, and appearance baking over live three.ws endpoints.

npm install @three-ws/glb-tools
View on GitHubStar the repo

Inspect, re-theme, and bake GLB models from the shell or CI — the toolkit behind three.ws's 3D asset pipeline.

npm downloads license node

Install · Quick start · API · How it works · Pricing · three.ws


@three-ws/glb-tools is the official client for the three.ws GLB pipeline — the same inspection, theming, and baking engine three.ws runs on every asset it serves. It wraps three live endpoints: structural model inspection (/api/x402/model-check), token-themed mesh synthesis (/api/x402/mint-to-mesh), and server-side appearance baking (/api/avatars/:id). One ergonomic call replaces a custom glTF parser, a @gltf-transform build chain, and the polling/billing plumbing around them. It pairs with @three-ws/forge — Forge makes the GLB, glb-tools measures, themes, and optimizes it.

#Why

A GLB looks like one file but hides a graph: scenes, nodes, meshes, materials, textures, skins, animations, extensions. Answering "how heavy is this, is it rigged, will it choke a phone, can I ship it" means either booting a headless viewer or hand-writing a glTF-Transform pipeline — per asset, per CI run. glb-tools does it as a function call:

This is the SDK twin of the inspection tools exposed on the 3D Studio MCP server — the same engine, as plain functions instead of MCP tools.

#Install

npm install @three-ws/glb-tools

Zero runtime dependencies. Works in Node 18+, modern browsers, and CI runners (uses fetch). To generate the GLBs you inspect, add @three-ws/forge.

#Quick start

Inspect any public GLB — exact stats, no viewer:

import { inspect } from '@three-ws/glb-tools';

const report = await inspect('https://three.ws/avatars/cesium-man.glb');

console.log(report.model.counts.totalTriangles); // → 4672
console.log(report.model.counts.skins > 0);       // → true  (rigged)
report.suggestions.forEach((s) => console.log(`[${s.severity}] ${s.message}`));

Turn a Solana mint into a renderable GLB:

import { theme } from '@three-ws/glb-tools';
import { writeFile } from 'node:fs/promises';

const out = await theme('FeMbDoX7R1Psc4GEcvJdsbNbZA3bfztcyDCatJVJpump'); // $THREE
console.log(out.theme.color);   // → [0.92, 0.45, 0.18]  (hashed from the mint)
await writeFile('three.glb', out.bytes); // ready for three.js / model-viewer

Bake a customized appearance into one flat, optimized GLB:

import { bake } from '@three-ws/glb-tools';

const baked = await bake('avatar_8f3a…', {
  outfit: 'streetwear-01',
  colors: { hair: '#1b1b1f', outfit: '#3b82f6' },
  accessories: ['glasses-aviator'],
}, { token: process.env.THREE_WS_TOKEN });

console.log(baked.bakedStorageKey, baked.sizeBytes);

#API

Public exports: inspect, theme, bake, createGlbTools, ThreeWsError, PaymentRequiredError, and DEFAULT_BASE_URL. The three headline functions use a shared zero-config client; createGlbTools({ baseUrl, fetch, apiKey, headers }) builds your own when you need a payment-aware fetch, an owner token, or a custom origin reused across calls.

#inspect(url, options?) → Promise<InspectReport>

Fetch a public glTF/GLB by URL and return its structure plus optimization advice. Wraps GET /api/x402/model-check?url=<url>. The model is parsed server-side with @gltf-transform; only the JSON/scene graph is analyzed — fast even on large meshes. Source URL must be public HTTPS; max 16 MiB.

Options

Option Type Default Notes
payWith 'x402' | 'credits' 'x402' Billing lane (see Pricing).
signal AbortSignal — Cancel an in-flight inspection.

Returns InspectReport

Field Type Notes
url string Canonicalized source URL.
fetchedBytes number Bytes downloaded for analysis.
model ModelInfo Structural summary (below).
suggestions Suggestion[] Prioritized optimization advice.

ModelInfo carries container ('glb' \| 'gltf'), generator, version, copyright, extensionsUsed, extensionsRequired, a counts object (scenes, nodes, meshes, materials, textures, animations, skins, totalVertices, totalTriangles, indexedPrimitives, nonIndexedPrimitives), primitiveModes, a per-texture textures[] array (name, mimeType, width, height, byteSize), and a per-material materials[] array (name, alphaMode, doubleSided, hasBaseColorTexture, hasNormalTexture, hasMetallicRoughnessTexture, hasEmissiveTexture, hasOcclusionTexture). A non-empty counts.skins means the model is rigged.

Each Suggestion is { id, severity: 'info' | 'warn' | 'critical', message, estimate? } — e.g. tri_budget (decimate/LOD over 500k triangles), draco, meshopt, texture_size.

#theme(mint, options?) → Promise<ThemedMesh>

Synthesize a themed GLB for a Solana fungible-token mint. Wraps GET /api/x402/mint-to-mesh?mint=<base58>. The server reads on-chain Metaplex metadata, colors a unit cube from a stable hash of the mint, and — when the off-chain metadata exposes a PNG/JPEG — embeds the token image as a baseColor texture on every face. The mint's name, symbol, and a timestamp are written to asset.extras so downstream agents can introspect the model.

Input — mint is a base58 SPL address (32–44 chars).

Returns ThemedMesh

Field Type Notes
mint string Echoed mint address.
theme.name string | null On-chain token name.
theme.symbol string | null On-chain symbol.
theme.color [number, number, number] RGB in [0,1], the baseColorFactor.
theme.imageUrl string | null Source image URL, when present.
theme.hasImage boolean true when an image was embedded as a texture.
bytes Uint8Array Decoded GLB bytes (from the response's base64).
glb.mimeType 'model/gltf-binary' —
glb.bytes number GLB size in bytes.

The raw endpoint returns the GLB base64-encoded under glb.base64; the SDK decodes it for you into bytes and leaves glb.bytes as the size.

#bake(avatarId, appearance, options?) → Promise<BakeResult>

Bake an appearance into a three.ws avatar's GLB, server-side. Wraps PATCH /api/avatars/:id { appearance }, which triggers the synchronous baker. Requires an authenticated owner token (the avatar belongs to a signed-in account).

appearance accepts any combination of:

Field Type Effect
outfit string Outfit preset id — applies its morph bindings.
accessories string[] Bone-mounted accessory preset ids (hats, glasses…).
colors Record<slot, hex> Tint material slots (skin, hair, outfit, glasses).
morphs Record<name, 0..1> Raw morph-target overrides (win over preset bindings).
hidden string[] Slots to hide, exposing the base body.

A bakeable appearance returns BakeResult { avatarId, bakedStorageKey, appearanceHash, sizeBytes, cleared, bakeError } (the endpoint's snake_case avatar fields, shaped to camelCase). An empty / cleared appearance clears the cached baked GLB (the base model is served again) and resolves with bakedStorageKey: null and cleared: true.

#How it works

Three independent endpoints, one client. None of the heavy lifting happens in your process:

inspect(url) ─────▶ GET /api/x402/model-check?url=…
                        │  fetch ≤16 MiB → @gltf-transform readJSON
                        ▼
                    counts · textures · materials · extensions
                        + suggestOptimizations() → InspectReport

theme(mint) ──────▶ GET /api/x402/mint-to-mesh?mint=…
                        │  Metaplex metadata → colorFromMint() (FNV-1a hash)
                        │  optional image → baseColor texture
                        ▼
                    createThemedGLB() → unit-cube GLB (base64) → ThemedMesh

bake(id, appr) ───▶ PATCH /api/avatars/:id { appearance }
                        │  applyMorphs · applyColors · merge accessories · applyHidden
                        │  unpartition → prune → dedup → weld → quantize
                        │  → meshopt → textureCompress (WebP, ≤1024px)
                        ▼
                    optimized GLB in R2 → BakeResult

#Pricing

The inspection and theming endpoints are pay-per-call x402 lanes settled in USDC (6-decimal atomics):

Capability Endpoint Lane Price
inspect() /api/x402/model-check x402 / USDC on Solana $0.001 per call
theme() /api/x402/mint-to-mesh x402 / USDC on Solana $0.001 per call
bake() /api/avatars/:id authenticated owner included with the avatar

Set payWith: 'x402' (default) to pay per call with USDC, or payWith: 'credits' to draw from a signed-in prepaid balance. Pair with @three-ws/x402-fetch to automate the 402 settlement. Internal/subscription/OAuth callers can be granted a payment bypass (x-payment-bypass header on the response). Both x402 routes are also exposed as MCP tools on the 3D Studio server.

#Errors & edge cases

inspect(), theme(), and bake() reject with a typed ThreeWsError carrying a code and status. A payment challenge (HTTP 402, or the x402-over-401 envelope some transports use) rejects with its PaymentRequiredError subclass, whose accepts field carries the x402 challenge. Client-side validation (a missing or malformed url, mint, avatarId, or appearance) rejects with code: 'invalid_input' or 'invalid_mint' before any network call. Server-side codes mirror the endpoint's error contract:

code HTTP Meaning Recovery
missing_url / missing_mint 400 Required input omitted. Pass url / mint.
invalid_url 400 Not a fetchable HTTPS URL. Use a public HTTPS GLB.
invalid_mint 400 Not a base58 SPL address (32–44 chars). Pass a valid mint.
payment_required 402 x402 lane with no payment proof. Provide an x402 payer or use credits.
method_not_allowed 405 Wrong HTTP verb. The SDK uses the correct verb; surfaces only on raw calls.
verify_failed / settle_failed 502 Payment verification/settlement upstream failed. Retry; check the payer balance.
internal_error 500 Upstream parse/build error. Retry, or check the input asset.

Inspection rejects oversized inputs before download (the 16 MiB cap), so a runaway URL fails fast rather than streaming forever. A bake of an empty appearance is not an error — it intentionally clears the cached baked GLB. Every state is designed: a missing key returns a code, not a crash.

#Examples

CI asset gate — fail the build when a model busts the triangle budget:

import { inspect } from '@three-ws/glb-tools';

const { model, suggestions } = await inspect(process.env.MODEL_URL, {
  payWith: 'credits',
});
const critical = suggestions.filter((s) => s.severity === 'critical');
if (model.counts.totalTriangles > 250_000 || critical.length) {
  console.error('Model exceeds web budget:', model.counts.totalTriangles);
  process.exit(1);
}

Forge → inspect — generate, then verify it's rig-ready before shipping:

import { forge } from '@three-ws/forge';
import { inspect } from '@three-ws/glb-tools';

const { glbUrl } = await forge('a cartoon astronaut, full body');
const { model } = await inspect(glbUrl);
console.log('rigged:', model.counts.skins > 0, '· tris:', model.counts.totalTriangles);

Theme a token, render inline in the browser:

<script type="module">
  import { theme } from '@three-ws/glb-tools';

  const { bytes } = await theme('FeMbDoX7R1Psc4GEcvJdsbNbZA3bfztcyDCatJVJpump'); // $THREE
  const url = URL.createObjectURL(new Blob([bytes], { type: 'model/gltf-binary' }));
  const el = Object.assign(document.createElement('model-viewer'), { src: url });
  document.body.append(el);
</script>

Built by three.ws · The only coin is $THREE