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
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 examplehttps://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.tgzfiles. 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 fromnode_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).
{
"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"
}
}
# 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
{
"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"]
}
pnpm install2. Install the kit
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 verify3. Entry files
entry point: assembles the kit's tested scene town-customized (scene 1.1.0, connector snapping).
<!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>
// 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
npx vite build
npx agent-ready-assets verify-project --browser chromiumFor 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.
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).