DocumentationThree.js
Documentation

Three.js integration

Add stylized-town@0.4.0 to a Vite project and render its tested scene with plain three.js. The dependency setup is the starter's; the entry file is the one the MCP tool get_integration_guide returns (generated from the catalog, then built and rendered in our tests).

1. Project and SDK

Terminal
mkdir my-town && cd my-town
mkdir -p vendor src
cp <folder with the SDK tarballs>/*.tgz vendor/

This setup resolves 4 SDK tarballs from vendor/: agent-ready-assets-asset-cli-0.5.1.tgz, agent-ready-assets-contracts-0.5.1.tgz, agent-ready-assets-customization-0.5.1.tgz, agent-ready-assets-runtime-three-0.5.1.tgz.

What the placeholders in the commands mean
  • <registry origin> and <site origin>: the address of this website, scheme and host only (for example https:// followed by the host name in your address bar, no path). The website, the account service and the catalog registry share this one origin; the MCP server is at <registry origin>/mcp. The CLI accepts https only (plain http only for a local loopback registry).
  • <API token from your account page>: create one on your account page after signing in. It installs only what your account is entitled to; the free sampler needs no token.
  • <folder with the SDK tarballs>: the folder with the SDK .tgz files. The SDK packages come as tarballs, not through the public npm registry. See what you need.
  • Run npx agent-ready-assets … only inside the project where the SDK is installed: npx then runs the project's own CLI from node_modules/.bin. Outside such a project npx would look for a package of that name on the public npm registry, where we publish none — do not run it there (pnpm exec agent-ready-assets … never downloads anything).
package.jsoncopy as is
{
  "name": "starter-vanilla",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "description": "Minimal Vite + three.js starter for the stylized-town kit: installs the kit with the agent-ready-assets CLI and renders a small snapped scene (one part swap, one recolour) with @agent-ready-assets/runtime-three. No Blender, no Python.",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "typecheck": "tsc --noEmit",
    "kit:add": "agent-ready-assets kit add $(node -p \"require('./src/scenes/starter.scene.json').kit\") --to public/assets",
    "validate": "agent-ready-assets verify && agent-ready-assets scene validate src/scenes/starter.scene.json --resolve",
    "verify-project": "agent-ready-assets verify-project --browser chromium"
  },
  "dependencies": {
    "@agent-ready-assets/runtime-three": "file:vendor/agent-ready-assets-runtime-three-0.5.1.tgz",
    "three": "0.186.1"
  },
  "devDependencies": {
    "@agent-ready-assets/asset-cli": "file:vendor/agent-ready-assets-asset-cli-0.5.1.tgz",
    "@playwright/test": "1.64.1",
    "@types/three": "0.186.0",
    "typescript": "7.0.2",
    "vite": "8.3.5"
  }
}
pnpm-workspace.yamlcopy as is
# Pre-release: the Agent-Ready Assets SDK packages are not on a public npm registry yet. Put the packed SDK tarballs you
# received into vendor/; these overrides resolve the SDK's internal packages to them. Everything else comes from npm.
overrides:
  '@agent-ready-assets/contracts': file:vendor/agent-ready-assets-contracts-0.5.1.tgz
  '@agent-ready-assets/customization': file:vendor/agent-ready-assets-customization-0.5.1.tgz
  '@agent-ready-assets/runtime-three': file:vendor/agent-ready-assets-runtime-three-0.5.1.tgz

engineStrict: true
saveExact: true
tsconfig.jsoncopy as is
{
  "compilerOptions": {
    "target": "es2022",
    "lib": ["es2023", "dom", "dom.iterable"],
    "module": "preserve",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "allowImportingTsExtensions": true,
    "noEmit": true,
    "strict": true,
    "isolatedModules": true,
    "verbatimModuleSyntax": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}
Terminalcopy as is
pnpm install

2. Install the kit

Terminal
export AGENT_READY_ASSETS_REGISTRY=<registry origin>
export AGENT_READY_ASSETS_TOKEN=<API token from your account page>
npx agent-ready-assets kit add stylized-town@0.4.0 --to public/assets
npx agent-ready-assets verify

3. Entry files

entry point: assembles the kit's tested scene town-customized (scene 1.1.0, connector snapping).

index.htmlcopy as is
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Stylized Town: modular warm toy town</title>
  </head>
  <body>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>
src/main.tscopy as is
// kit stylized-town@0.4.0: assemble a tested scene.json
// Generated by the Agent-Ready Assets integration guide (target: plain Three.js, SDK 0.5.1, tested with three 0.186.1).
// The files are served from this project's own public/ folder (installed with `npx agent-ready-assets ...`):
// no CDN at runtime, no decoders, no Blender.
import * as THREE from 'three';
import { AssetLibrary, isCustomizationError } from '@agent-ready-assets/runtime-three';

const KIT_URL = '/assets/stylized-town/kit.json';
// A version-pinned scene installed with the kit. Copy it into src/ to edit it, then check it with
// `npx agent-ready-assets scene validate <file>` (unknown assets, versions or capabilities are refused).
const SCENE_URL = '/assets/stylized-town/scenes/town-customized.scene.json';

const renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
document.body.style.margin = '0';
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();
scene.background = new THREE.Color('#e9e4da');
scene.add(new THREE.HemisphereLight(0xffffff, 0x8d7c64, 1.4));
const sun = new THREE.DirectionalLight(0xffffff, 2.0);
sun.position.set(4, 8, 5);
scene.add(sun);
const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.05, 400);

/** Points the camera at everything that was loaded (3/4 view). */
function frame(target: THREE.Object3D): void {
  const box = new THREE.Box3().setFromObject(target);
  const center = box.getCenter(new THREE.Vector3());
  const radius = Math.max(box.getSize(new THREE.Vector3()).length() / 2, 0.5);
  const dist = radius / Math.sin(THREE.MathUtils.degToRad(camera.fov / 2));
  camera.position.copy(center).add(new THREE.Vector3(0.55, 0.5, 1).normalize().multiplyScalar(dist * 0.95));
  camera.lookAt(center);
  camera.updateProjectionMatrix();
}

window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.render(scene, camera);
});

async function main(): Promise<void> {
  const library = await AssetLibrary.fromKit(KIT_URL);
  const response = await fetch(SCENE_URL);
  if (!response.ok) throw new Error(`${SCENE_URL}: HTTP ${response.status}`);
  // optimize (SDK 0.5.0): repeated meshes are drawn with GPU instancing; same image, customization, clips, picking, release()
  const town = await library.assembleScene(await response.json(), { optimize: true }); // all-or-nothing: an invalid scene adds nothing
  scene.add(town.group);
  const ground = new THREE.Mesh(new THREE.PlaneGeometry(60, 60), new THREE.MeshStandardMaterial({ color: '#8fa86a' }));
  ground.rotation.x = -Math.PI / 2;
  ground.position.y = -0.02;
  scene.add(ground);
  frame(town.group);
  renderer.render(scene, camera);
  renderer.domElement.dataset.ready = 'true';
}

main().catch((error: unknown) => {
  // Unsupported or invalid customizations are refused with a structured error instead of being approximated.
  const message = isCustomizationError(error) ? `${error.code}: ${error.message}` : String(error);
  renderer.domElement.dataset.error = message;
  console.error(message);
});

TypeScript: the SDK's declarations ship next to its JavaScript (node_modules/@agent-ready-assets/runtime-three/dist/index.d.ts) and resolve from the plain import; npx agent-ready-assets doctor prints the path.

4. Build and verify

Terminalcopy as is
npx vite build
npx agent-ready-assets verify-project --browser chromium

For development: npx vite.

Large scenes: one option

The entry file above already assembles the scene with { optimize: true } (SDK 0.5.1, opt-in). Every mesh that at least two instances draw with the same geometry and material becomes one THREE.InstancedMesh: fewer draw calls, the same triangles. Customized instances keep their own materials and parts, a running clip is never batched, and an instance you move or hide yourself is drawn by its own objects from that frame on. town.customize(id, values) rebuilds the batches.

The optimization handle
import { AssetLibrary, optimizeScene } from '@agent-ready-assets/runtime-three';

const town = await library.assembleScene(sceneJson, { optimize: true }); // or { optimize: 'strict' }: unmodified, clip-free instances only
scene.add(town.group);
town.optimization?.stats(); // { drawsBefore, drawsAfter, instancedMeshes, batchedInstances, triangles, ... }
const hit = town.optimization?.pick(raycaster); // first drawn hit: { instance_id, asset, part, object, point, distance, face } | null
town.release(); // the instances, their parts and the optimization

// a scene you assembled without the option: const batches = optimizeScene(town); ... batches.dispose();

An instance your own code animates, highlights, drags or decorates per object should stay out of the batches: when not to batch an instance (exclude, unbatch(id), strict).

Measured: the 10 town scenes coding agents wrote in recorded runs draw 169–264 draw calls per frame plain and 63–93 optimized (−55.6 % to −69.6 %); the kit's explorable town (161 instances) 621 → 148 (−76.2 %). At most 0.070 % of the pixels differ from the plain frame (pass condition: <= 0.1 % pixels > 16/255). Measurements.

Caveats

  • Customization is limited to the declared capabilities (get_customization_capabilities); requests outside them fail with UNSUPPORTED_OPERATION and must be reported, not approximated.
  • Recoloring changes the named material colour; it does not paint baked textures. Scaling uses a wrapper node; the GLB itself is never modified or re-exported.
  • Call instance.release() (vanilla) when removing an instance; <Asset> releases automatically on unmount.
  • Part swaps mount the pinned part asset from your installed kit (hash-verified like any asset); a part that is not installed is refused with INCOMPATIBLE_PART/PART_NOT_IN_KIT. Mounted parts are owned and released by their host.
  • Visibility toggles hide only that instance's own clone of an optional part; snapped scenes (scene 1.1.0 attach_to) take every transform from the connector frames, so edit attach_to, not coordinates, for snapped instances.
  • Requires @agent-ready-assets/runtime-three 0.5.1 or later, the SDK these packages were built for (the CLI refuses an older SDK with DEPENDENCY_MISMATCH); SDKs before 0.4.0 do not implement node-visibility, modular-swap, swatches or snapping.
  • To render your own scene file instead (e.g. src/scenes/town.scene.json, scene 1.1.0), import it (`import town from './scenes/town.scene.json'`, a Vite JSON import) and pass the object to library.assembleScene(town, { optimize: true }) (vanilla) or library.validateScene(town) (R3F, rendered inside <OptimizedScene> as the entry file does); check it first with `npx agent-ready-assets scene validate src/scenes/town.scene.json --resolve`.
  • Large scenes: keep { optimize: true } (vanilla) or <OptimizedScene> with instanceId on each <Asset> (R3F). Repeated meshes are then drawn with GPU instancing; the image, customization, clips, picking and release() are unchanged (in the measured agent-written towns it cut draw calls by more than half). It needs SDK 0.5.0 or later; with an older SDK remove the option and the wrapper. Do not batch an instance you animate yourself, give its own materials or interact with per frame: exclude it by its scene instance id (vanilla { optimize: { exclude: [id] } }, R3F <OptimizedScene exclude={[id]}>) or use the strict preset (only unmodified instances without clips). An exclusion names the scene instance, so it still applies after town.customize replaces that instance; SDK 0.5.1 and later also accept the LoadedAsset of the instance (with SDK 0.5.0 only an id survives town.customize, so prefer ids).