Skip to content
Kiln1.3Get Kiln

text-to-3d

Your agent writes the asset as JavaScript; Kiln runs it, returns rendered views and structural checks, and exports a GLB with its source.

MIT · runs on your machine · MCP, CLI, TypeScript library and agent skills · 1.3

Want hosted access? Join the Kiln Discord and ask in #request-access or message matt_941.Hosted Kiln is open by invitation, with ten introductory accounts sharing 200 native requests per month. Codes are sent privately as capacity allows. Local Kiln needs no account or invitation.

Golden Gate Bridge seen obliquely from above: both towers, the main cables and the suspended roadway on a neutral grey backdrop.
  1. GoldenGateBridge/SouthTower/SouthTower_CrownWest
  2. GoldenGateBridge/NorthTower/NorthTower_Pier
Golden Gate Bridge91.44 × 262.76 × 2,067.96 mWeb tier · revision r_de88c9d5…c81cf0a9Claude Sonnet 5.5 · Claude Code 2.1.280. Requested effort: max. Independently confirmed: not recorded.148 named parts · 45,274 bytes of JavaScript source · rendered under review-neutral-v1 from this revision’s GLB

Kiln in live games

I use Kiln in my own games.

I build Kiln, Sheepdog Sim and OBJEKT-62. Both games use Kiln-authored assets, adapted to their own rendering and gameplay. Working on them lets me test the whole path from editable source to an asset players actually encounter.

A sheepdog faces the fenced pen, with the Kiln-authored farmhouse, red barn and trees behind it in Sheepdog Sim.
House, barn, fences and trees in the game.

Sheepdog Sim

A sheep herding game in your browser. I authored its house, barn, fences and trees with Kiln, then integrated them with the game's own materials and rendering.

The game is open source. Browse the repository to see how the assets fit into a working game.

The Kiln-authored survey rifle in the foreground, an outpost and parked aircraft ahead, and a monument on the distant desert horizon in OBJEKT-62.
Survey rifle and outpost · live 0.21.2.

OBJEKT-62

A sci-fi survival game in your browser. I use Kiln for most of its 3D assets, including guns, ship parts, vehicles, outpost modules and salvage. They are integrated with the game's own materials, collision and interaction systems.

Play OBJEKT-62

The source is something your agent can change.

A roof pitch, a named part, a material pin: the shape has an editable definition. Kiln returns views and geometry checks so your agent can inspect what the program actually built. Here one request changes the farmhouse floor and leaves the rest of the house as it was.

One farmhouse revision, from request to files
  1. 1. Request

    A wood ground floor in place of the masonry-looking floor.

  2. 2. Source

      createPart('InteriorFloor', B.floor.geo(), M.wood, { parent: shell });
  3. 3. Inspect

    Farmhouse interior before the floor revision, with a masonry-looking floor.
    Before · masonry-looking
    Farmhouse interior after the revision, with honey-coloured wooden floorboards.
    After · wood

    Bounds unchanged. 132 exterior probes unchanged.

  4. 4. Retain

    r_01f97f5e943e4c2693b6db8596f3e353

    Saved as a new revision; the parent stays intact.

  5. 5. Deliver

    Farmhouse GLB
    Editable source

    The same saved revision.

Gemini 3.8 Flash via Antigravity CLI edited the source written by Claude Opus 5.5. Kiln rendered the room before and after: 132 exterior probes identical, bounds unchanged. Fourteen meshes, all transforms and all materials stayed unchanged; the stone shell was re-tessellated around the new floor.

The child revision keeps its parent and model attribution. The current downloads include the wood floor.

Inspect the farmhouse and its history
Parent
r_37f835d35a1040fc9f64d6f3b271eac5
Child
r_01f97f5e943e4c2693b6db8596f3e353
Attribution
Gemini 3.8 Flash High · Antigravity CLI (agy)
Farmhouse · source.kiln.js
// Farmhouse — cream masonry, charcoal roof, front dormer, gable chimney, olive shutters,
// honey-wood covered porch and steps. Metres; +X front, +Y up, +Z right; body centred on origin.
const meta = { name: 'Farmhouse', role: 'building' };

const P = {
  halfX: 3.0, halfZ: 4.0, wallT: 0.3, floorY: 0.4, eaveY: 3.6, pitchDeg: 40,
  roofT: 0.18, eaveOver: 0.35, gableOver: 0.35,
  doorHalfW: 0.5, doorTop: 2.55, winY0: 1.15, winY1: 2.45, shutterW: 0.55,
  porchX: 5.0, porchHalfZ: 3.45, postX: 4.8, postZ: [-3.3, -0.95, 0.95, 3.3],
  porchLedgerY: 3.3, porchSlope: 0.2, porchRoofT: 0.12, porchRoofX: 5.25,
  beamTop: 2.80, beamD: 0.18, stepHalfZ: 0.8,
};
const TH = P.pitchDeg * Math.PI / 180;
const K = Math.tan(TH);
const RIDGE_Y = P.eaveY + P.halfX * K; // roof underside at ridge
// Stripe-based wood maps run grain lines along texture V; set false if review shows cross grain.
const WOOD_GRAIN_ALONG_V = true;

const MAT = {
  masonry: { schemaVersion: 2, model: 'pbrMetallicRoughness', name: 'Farm cream masonry (restrained blocks)', baseColor: 16777215, roughness: 1, metalness: 0, emissiveIntensity: 1, alphaMode: 'opaque', alphaCutoff: 0.5, doubleSided: false, textures: { baseColor: { kind: 'resource', resourceId: 'kiln.library.d1c42bd60d214b06715046b8d7bfdcd7b6591c427e02cf8b3eecf7a7f61772b6.base-color' }, normal: { kind: 'resource', resourceId: 'kiln.library.d1c42bd60d214b06715046b8d7bfdcd7b6591c427e02cf8b3eecf7a7f61772b6.normal' }, metallicRoughness: { kind: 'resource', resourceId: 'kiln.library.d1c42bd60d214b06715046b8d7bfdcd7b6591c427e02cf8b3eecf7a7f61772b6.metallic-roughness' } } },
  roof: { schemaVersion: 2, model: 'pbrMetallicRoughness', name: 'Farm charcoal roof shingles', baseColor: 16777215, roughness: 1, metalness: 0, emissiveIntensity: 1, alphaMode: 'opaque', alphaCutoff: 0.5, doubleSided: false, textures: { baseColor: { kind: 'resource', resourceId: 'kiln.library.ff552c543e787c67a870286f2eb1fab3a342891a4f7b41675a07943fad550f85.base-color' }, normal: { kind: 'resource', resourceId: 'kiln.library.ff552c543e787c67a870286f2eb1fab3a342891a4f7b41675a07943fad550f85.normal' }, metallicRoughness: { kind: 'resource', resourceId: 'kiln.library.ff552c543e787c67a870286f2eb1fab3a342891a4f7b41675a07943fad550f85.metallic-roughness' } } },
  wood: { schemaVersion: 2, model: 'pbrMetallicRoughness', name: 'Farm honey wood (subdued grain derivative)', baseColor: 16777215, roughness: 1, metalness: 0, emissiveIntensity: 1, alphaMode: 'opaque', alphaCutoff: 0.5, doubleSided: false, textures: { baseColor: { kind: 'resource', resourceId: 'kiln.library.710f9ab0183deb86923b3c398286f9cbcb0eaea7ec50657a72fc83c2bd3c38cd.base-color' }, normal: { kind: 'resource', resourceId: 'kiln.library.710f9ab0183deb86923b3c398286f9cbcb0eaea7ec50657a72fc83c2bd3c38cd.normal' }, metallicRoughness: { kind: 'resource', resourceId: 'kiln.library.710f9ab0183deb86923b3c398286f9cbcb0eaea7ec50657a72fc83c2bd3c38cd.metallic-roughness' } } },
  metal: { schemaVersion: 2, model: 'pbrMetallicRoughness', name: 'Brushed neutral metal', roughness: 1, metalness: 1, emissiveIntensity: 1, alphaMode: 'opaque', alphaCutoff: 0.5, doubleSided: false, textures: { baseColor: { kind: 'resource', resourceId: 'kiln.library.67c9159b792a918da293bffc9468ed8834957dd51d83ce290af980a72a9065d0.base-color' }, normal: { kind: 'resource', resourceId: 'kiln.library.67c9159b792a918da293bffc9468ed8834957dd51d83ce290af980a72a9065d0.normal' }, metallicRoughness: { kind: 'resource', resourceId: 'kiln.library.67c9159b792a918da293bffc9468ed8834957dd51d83ce290af980a72a9065d0.metallic-roughness' } } },
};
const plain = (name, baseColor, roughness, metalness = 0) =>
  ({ schemaVersion: 2, model: 'pbrMetallicRoughness', name, baseColor, roughness, metalness });

// ---------- small vector helpers ----------
const add = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
const sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
const mul = (a, s) => [a[0] * s, a[1] * s, a[2] * s];
const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
const len = (a) => Math.hypot(a[0], a[1], a[2]);
const norm = (a) => mul(a, 1 / (len(a) || 1));
function newell(vs) {
  const n = [0, 0, 0];
  for (let i = 0; i < vs.length; i++) {
    const a = vs[i], b = vs[(i + 1) % vs.length];
    n[0] += (a[1] - b[1]) * (a[2] + b[2]);
    n[1] += (a[2] - b[2]) * (a[0] + b[0]);
    n[2] += (a[0] - b[0]) * (a[1] + b[1]);
  }
  return norm(n);
}

// ---------- per-material mesh builder with metre-scaled UVs ----------
// tile = physical texture repeat in metres; grain = member direction mapped to U (or V for wood).
function MB(tile, grainAlongV = false) {
  const bld = { tile, gv: grainAlongV, pos: [], nor: [], uv: [], idx: [] };
  bld.face = (vs, inside, grain) => {
    let n = newell(vs);
    if (dot(n, sub(vs[0], inside)) < 0) { vs = vs.slice().reverse(); n = mul(n, -1); }
    let g = sub(grain, mul(n, dot(grain, n)));
    if (len(g) < 0.3) g = Math.abs(n[1]) < 0.9 ? cross([0, 1, 0], n) : [1, 0, 0];
    g = norm(g);
    let u, v;
    if (bld.gv) { v = g; u = cross(v, n); }
    else { u = g; v = cross(n, u); if (Math.abs(n[1]) < 0.9 && v[1] < 0) { u = mul(u, -1); v = mul(v, -1); } }
    const base = bld.pos.length / 3;
    for (const p of vs) { bld.pos.push(...p); bld.nor.push(...n); bld.uv.push(dot(p, u) / bld.tile, dot(p, v) / bld.tile); }
    for (let i = 1; i < vs.length - 1; i++) bld.idx.push(base, base + i, base + i + 1);
  };
  bld.obox = (c, ex, ey, ez, hx, hy, hz, grain) => {
    const C = (i, j, k) => add(add(add(c, mul(ex, i ? hx : -hx)), mul(ey, j ? hy : -hy)), mul(ez, k ? hz : -hz));
    const g = grain || (hx >= hy && hx >= hz ? ex : hy >= hz ? ey : ez);
    const F = [
      [[1, 0, 0], [1, 1, 0], [1, 1, 1], [1, 0, 1]], [[0, 0, 0], [0, 1, 0], [0, 1, 1], [0, 0, 1]],
      [[0, 1, 0], [1, 1, 0], [1, 1, 1], [0, 1, 1]], [[0, 0, 0], [1, 0, 0], [1, 0, 1], [0, 0, 1]],
      [[0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1]], [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]],
    ];
    for (const f of F) bld.face(f.map(([i, j, k]) => C(i, j, k)), c, g);
  };
  bld.aabb = (x0, x1, y0, y1, z0, z1, grain) => {
    bld.obox([(x0 + x1) / 2, (y0 + y1) / 2, (z0 + z1) / 2], [1, 0, 0], [0, 1, 0], [0, 0, 1],
      (x1 - x0) / 2, (y1 - y0) / 2, (z1 - z0) / 2, grain);
  };
  // convex polygon pts [a,b] mapped by map(a,b,d) and extruded d0..d1
  bld.prism = (pts, map, d0, d1, grain) => {
    const c0 = pts.map(([a, b]) => map(a, b, d0)), c1 = pts.map(([a, b]) => map(a, b, d1));
    const all = c0.concat(c1);
    const inside = mul(all.reduce((s, p) => add(s, p), [0, 0, 0]), 1 / all.length);
    bld.face(c0, inside, grain); bld.face(c1, inside, grain);
    for (let i = 0; i < pts.length; i++) {
      const j = (i + 1) % pts.length;
      bld.face([c0[i], c0[j], c1[j], c1[i]], inside, grain);
    }
  };
  bld.strut = (a, b, w, h, side, ext = 0.03) => {
    const d = norm(sub(b, a));
    const ey = norm(cross(d, side)), ez = cross(d, ey);
    bld.obox(mul(add(a, b), 0.5), d, ey, ez, len(sub(b, a)) / 2 + ext, h / 2, w / 2, d);
  };
  bld.geo = () => meshGeo({ positions: bld.pos, normals: bld.nor, uvs: bld.uv, indices: bld.idx });
  return bld;
}

// Solid wall split into blocks around real openings. axis 'x': wall faces ±X and runs along Z.
function wall(mb, axis, t0, t1, s0, s1, y0, y1, holes) {
  const cuts = [...new Set([s0, s1, ...holes.flatMap((h) => [h.s0, h.s1])])].sort((a, b) => a - b);
  const emit = (a, b, ya, yb) => axis === 'x'
    ? mb.aabb(t0, t1, ya, yb, a, b, [0, 0, 1]) : mb.aabb(a, b, ya, yb, t0, t1, [1, 0, 0]);
  for (let i = 0; i < cuts.length - 1; i++) {
    const a = cuts[i], b = cuts[i + 1];
    if (b - a < 1e-6) continue;
    const hs = holes.filter((h) => h.s0 <= a + 1e-6 && h.s1 >= b - 1e-6).sort((p, q) => p.y0 - q.y0);
    let y = y0;
    for (const h of hs) { if (h.y0 > y + 1e-6) emit(a, b, y, h.y0); y = Math.max(y, h.y1); }
    if (y1 > y + 1e-6) emit(a, b, y, y1);
  }
}

async function build() {
  const root = createRoot('Farmhouse');
  const M = {
    masonry: await compilePortableMaterialSpecV2({ ...MAT.masonry, baseColor: 0xeadcbf }),
    stone: await compilePortableMaterialSpecV2({ ...MAT.masonry, name: 'Farm stone (masonry maps, stone tint)', baseColor: 0xaea796 }),
    roof: await compilePortableMaterialSpecV2({ ...MAT.roof, baseColor: 0x8c8c8c }),
    wood: await compilePortableMaterialSpecV2({ ...MAT.wood, baseColor: 0xdcae78 }),
    metal: await compilePortableMaterialSpecV2({ ...MAT.metal, name: 'Hinge iron (brushed metal, dark tint)', baseColor: 0x8a8c88 }),
    trim: await compilePortableMaterialSpecV2(plain('Painted cream trim', 0xe6dcc6, 0.8)),
    olive: await compilePortableMaterialSpecV2(plain('Olive shutter paint', 0x55622e, 0.72)),
    red: await compilePortableMaterialSpecV2(plain('Painted red door', 0x9f3f2b, 0.68)),
    // Clear, lightly tinted glazing: portable core-glTF alpha BLEND (no transmission extension).
    // Single-sided on closed pane boxes so each view crosses exactly one glazed face.
    glass: await materialRecipe('kiln.material.glass.v1', { baseColor: '#a9cdd4', roughness: 0.05, opacity: 0.14, doubleSided: false }),
    dark: await compilePortableMaterialSpecV2(plain('Flue soot', 0x1c1a18, 1)),
  };
  const B = {
    masonry: MB(2), stone: MB(3), trim: MB(1), glass: MB(1), dark: MB(1),
    roof: MB(1), porchWood: MB(1, WOOD_GRAIN_ALONG_V), porchStone: MB(3), porchRoof: MB(1),
    steps: MB(1, WOOD_GRAIN_ALONG_V), olive: MB(1), hinge: MB(1),
    doorRed: MB(1), doorMetal: MB(1), ceiling: MB(1),
    floor: MB(1, WOOD_GRAIN_ALONG_V),
  };
  const { halfX: HX, halfZ: HZ, wallT: WT, floorY: FY, eaveY: E } = P;
  const ix = HX - WT, iz = HZ - WT;

  // ---------- window unit: frame, glass, sill, lintel, optional shutters with strap hinges ----------
  function windowUnit(o) {
    const P3 = (s, y, d) => (o.axis === 'x' ? [o.face - o.out * d, y, s] : [s, y, o.face - o.out * d]);
    const box = (mb, s0, s1, y0, y1, d0, d1, grain) => {
      const a = P3(s0, y0, d0), b = P3(s1, y1, d1);
      mb.aabb(Math.min(a[0], b[0]), Math.max(a[0], b[0]), Math.min(a[1], b[1]), Math.max(a[1], b[1]),
        Math.min(a[2], b[2]), Math.max(a[2], b[2]), grain);
    };
    const { s0, s1, y0, y1 } = o, f = 0.06;
    box(B.glass, s0 + f, s1 - f, y0 + f, y1 - f, 0.15, 0.17);
    box(B.trim, s0, s1, y0, y0 + f, 0.08, 0.2); box(B.trim, s0, s1, y1 - f, y1, 0.08, 0.2);
    box(B.trim, s0, s0 + f, y0 + f, y1 - f, 0.08, 0.2); box(B.trim, s1 - f, s1, y0 + f, y1 - f, 0.08, 0.2);
    const sm = (s0 + s1) / 2, ym = y0 + (y1 - y0) * 0.58;
    box(B.trim, sm - 0.02, sm + 0.02, y0 + f, y1 - f, 0.12, 0.15);
    box(B.trim, s0 + f, s1 - f, ym - 0.02, ym + 0.02, 0.12, 0.15);
    box(B.stone, s0 - 0.07, s1 + 0.07, y0 - 0.06, y0 + 0.02, -0.07, 0.08);
    box(B.stone, s0 - 0.12, s1 + 0.12, y1 + 0.001, y1 + 0.16, -0.03, 0.1);
    if (!o.shutters) return;
    const w = o.shutterW || P.shutterW;
    for (const side of [-1, 1]) {
      const inner = side < 0 ? s0 - 0.04 : s1 + 0.04, outer = inner + side * w;
      const a = Math.min(inner, outer), b = Math.max(inner, outer);
      box(B.olive, a, b, y0, y1, -0.045, 0);
      for (const yb of [y0 + 0.16, y1 - 0.16]) {
        box(B.olive, a + 0.03, b - 0.03, yb - 0.045, yb + 0.045, -0.065, -0.045);
        const strapEnd = inner + side * 0.32;
        box(B.hinge, Math.min(inner - side * 0.03, strapEnd), Math.max(inner - side * 0.03, strapEnd), yb - 0.02, yb + 0.02, -0.075, -0.064);
        box(B.hinge, Math.min(inner, inner - side * 0.035), Math.max(inner, inner - side * 0.035), yb - 0.03, yb + 0.03, -0.078, 0);
      }
    }
  }

  // ---------- Shell: foundation, walls with real openings, gables, dormer walls, chimney ----------
  // Perimeter stone foundation under walls; exterior plinth and seating preserved exactly
  B.stone.aabb(ix, HX + 0.08, 0, FY, -HZ - 0.08, HZ + 0.08);
  B.stone.aabb(-HX - 0.08, -ix, 0, FY, -HZ - 0.08, HZ + 0.08);
  B.stone.aabb(-ix, ix, 0, FY, -HZ - 0.08, -iz);
  B.stone.aabb(-ix, ix, 0, FY, iz, HZ + 0.08);
  // Main room wooden plank floor spanning interior between walls
  B.floor.aabb(-ix, ix, 0, FY - 0.02, -iz, iz, [0, 0, 1]);
  const numBoards = 18, boardW = (2 * ix) / numBoards;
  for (let i = 0; i < numBoards; i++) {
    const xa = i === 0 ? -ix : -ix + i * boardW + 0.002;
    const xb = i === numBoards - 1 ? ix : -ix + (i + 1) * boardW - 0.002;
    B.floor.aabb(xa, xb, FY - 0.02, FY, -iz, iz, [0, 0, 1]);
  }
  const frontWins = [-2.0, 2.0].map((c) => ({ s0: c - 0.55, s1: c + 0.55, y0: P.winY0, y1: P.winY1 }));
  const door = { s0: -P.doorHalfW, s1: P.doorHalfW, y0: FY, y1: P.doorTop };
  wall(B.masonry, 'x', HX - WT, HX, -HZ + WT, HZ - WT, FY, E, [door, ...frontWins]);
  wall(B.masonry, 'x', -HX, -HX + WT, -HZ + WT, HZ - WT, FY, E, frontWins);
  for (const w of frontWins) {
    windowUnit({ axis: 'x', out: 1, face: HX, ...w, shutters: true });
    windowUnit({ axis: 'x', out: -1, face: -HX, ...w, shutters: true });
  }
  const sideWin = { '1': { s0: -0.55, s1: 0.55 }, '-1': { s0: 0.35, s1: 1.45 } };
  const gableWin = { s0: -0.4, s1: 0.4, y0: 4.1, y1: 4.9 };
  for (const sz of [1, -1]) {
    const t0 = sz > 0 ? HZ - WT : -HZ, t1 = sz > 0 ? HZ : -HZ + WT;
    const sw = { ...sideWin[String(sz)], y0: P.winY0, y1: P.winY1 };
    wall(B.masonry, 'z', t0, t1, -HX, HX, FY, E, [sw]);
    windowUnit({ axis: 'z', out: sz, face: sz * HZ, ...sw, shutters: true });
    // gable above the eave, meeting the roof underside, with a real gable window
    const map = (a, b, d) => [a, b, d];
    const yAt = (x) => E + (HX - Math.abs(x)) * K;
    B.masonry.prism([[-HX, E], [-0.4, E], [-0.4, yAt(-0.4)]], map, t0, t1, [1, 0, 0]);
    B.masonry.prism([[0.4, E], [HX, E], [0.4, yAt(0.4)]], map, t0, t1, [1, 0, 0]);
    B.masonry.aabb(-0.4, 0.4, E, gableWin.y0, t0, t1, [1, 0, 0]);
    B.masonry.prism([[-0.4, gableWin.y1], [0.4, gableWin.y1], [0.4, yAt(0.4)], [0, RIDGE_Y], [-0.4, yAt(-0.4)]], map, t0, t1, [1, 0, 0]);
    windowUnit({ axis: 'z', out: sz, face: sz * HZ, ...gableWin, shutters: false });
  }
  // Front dormer walls (roof part in Roof assembly)
  const D = { x0: 2.15, x1: 2.3, hz: 0.9, y0: 4.0, eave: 5.25, win: { s0: -0.4, s1: 0.4, y0: 4.55, y1: 5.05 } };
  const dormerPeak = D.eave + D.hz * K;
  wall(B.masonry, 'x', D.x0, D.x1, -D.hz, D.hz, D.y0, D.eave, [D.win]);
  B.masonry.prism([[-D.hz, D.eave], [D.hz, D.eave], [0, dormerPeak]], (a, b, d) => [d, b, a], D.x0, D.x1, [0, 0, 1]);
  B.masonry.aabb(0.8, D.x0, D.y0, D.eave, D.hz - 0.15, D.hz, [1, 0, 0]);
  B.masonry.aabb(0.8, D.x0, D.y0, D.eave, -D.hz, -D.hz + 0.15, [1, 0, 0]);
  windowUnit({ axis: 'x', out: 1, face: D.x1, ...D.win, shutters: true, shutterW: 0.38 });
  // Finished interior ceiling: one continuous solid slab seated 0.05 m into all four walls
  // (hides roof/dormer construction), with a plain cornice strip closing each wall join.
  const CEIL = { y: 3.2, t: 0.1, cornice: 0.07 };
  const cy = CEIL.y, cc = CEIL.cornice;
  B.ceiling.aabb(-ix - 0.05, ix + 0.05, cy, cy + CEIL.t, -iz - 0.05, iz + 0.05, [0, 0, 1]);
  B.ceiling.aabb(ix - cc, ix, cy - cc, cy, -iz, iz, [0, 0, 1]);
  B.ceiling.aabb(-ix, -ix + cc, cy - cc, cy, -iz, iz, [0, 0, 1]);
  B.ceiling.aabb(-ix + cc, ix - cc, cy - cc, cy, iz - cc, iz, [1, 0, 0]);
  B.ceiling.aabb(-ix + cc, ix - cc, cy - cc, cy, -iz, -iz + cc, [1, 0, 0]);
  // Door surround
  B.trim.aabb(HX, HX + 0.04, FY, P.doorTop, -0.64, -0.5); B.trim.aabb(HX, HX + 0.04, FY, P.doorTop, 0.5, 0.64);
  B.trim.aabb(HX, HX + 0.05, P.doorTop, P.doorTop + 0.16, -0.66, 0.66);
  // Gable-end stone chimney on -Z with a real hollow flue crown
  const ch = { x0: -2.1, x1: -1.1, z0: -4.65, z1: -HZ, sx0: -1.95, sx1: -1.25, sz0: -4.55, top: 6.85 };
  B.stone.aabb(ch.x0, ch.x1, 0, 3.4, ch.z0, ch.z1);
  B.stone.prism([[ch.x0, 3.4], [ch.x1, 3.4], [ch.sx1, 3.8], [ch.sx0, 3.8]], (a, b, d) => [a, b, d], ch.z0, ch.z1, [1, 0, 0]);
  B.stone.aabb(ch.sx0, ch.sx1, 3.8, ch.top, ch.sz0, ch.z1);
  const fx = -1.6, fz = (ch.sz0 + ch.z1) / 2, fh = 0.15, cy0 = ch.top, cy1 = ch.top + 0.12;
  B.stone.aabb(ch.sx0 - 0.05, ch.sx1 + 0.05, cy0, cy1, ch.sz0 - 0.05, fz - fh);
  B.stone.aabb(ch.sx0 - 0.05, ch.sx1 + 0.05, cy0, cy1, fz + fh, ch.z1 + 0.05);
  B.stone.aabb(ch.sx0 - 0.05, fx - fh, cy0, cy1, fz - fh, fz + fh);
  B.stone.aabb(fx + fh, ch.sx1 + 0.05, cy0, cy1, fz - fh, fz + fh);
  B.dark.aabb(fx - fh, fx + fh, cy0, cy0 + 0.02, fz - fh, fz + fh);

  // ---------- Roof: two slabs meeting under a ridge cap, dormer gable roof ----------
  const c = Math.cos(TH), s = Math.sin(TH), T = P.roofT;
  for (const sx of [1, -1]) {
    const eu = [sx * c, -s, 0], en = [sx * s, c, 0];
    const top = [0, RIDGE_Y, 0], low = [sx * (HX + P.eaveOver), E - P.eaveOver * K, 0];
    const mid = add(mul(add(top, low), 0.5), mul(en, T / 2));
    B.roof.obox(mid, eu, en, [0, 0, 1], len(sub(low, top)) / 2, T / 2, HZ + P.gableOver, [0, 0, 1]);
  }
  const r45 = Math.SQRT1_2;
  B.roof.obox([0, RIDGE_Y + 0.1, 0], [r45, r45, 0], [-r45, r45, 0], [0, 0, 1], 0.15, 0.15, HZ + P.gableOver + 0.05, [0, 0, 1]);
  const dx0 = 0.1, dx1 = D.x1 + 0.15, dOver = 0.15;
  for (const sz of [1, -1]) {
    const eu = [0, -s, sz * c], en = [0, c, sz * s];
    const top = [0, dormerPeak, 0], low = [0, D.eave - dOver * K, sz * (D.hz + dOver)];
    const mid = add(mul(add(top, low), 0.5), mul(en, T / 2));
    B.roof.obox(add(mid, [(dx0 + dx1) / 2, 0, 0]), eu, en, [1, 0, 0], len(sub(low, top)) / 2, T / 2, (dx1 - dx0) / 2, [1, 0, 0]);
  }
  B.roof.obox([(dx0 + dx1) / 2, dormerPeak + 0.09, 0], [1, 0, 0], [0, r45, r45], [0, -r45, r45], (dx1 - dx0) / 2 + 0.03, 0.12, 0.12, [1, 0, 0]);

  // ---------- Porch: stone plinth, deck, posts, beam, knee braces, rafters, ledger, railings, shed roof ----------
  const W = B.porchWood, PZ = P.porchHalfZ, PX = P.postX;
  B.porchStone.aabb(HX + 0.08, P.porchX, 0, 0.3, -PZ + 0.05, PZ - 0.05);
  W.aabb(HX + 0.08, P.porchX + 0.05, 0.3, FY, -PZ, PZ, [0, 0, 1]);
  const beamBot = P.beamTop - P.beamD;
  for (const z of P.postZ) W.aabb(PX - 0.08, PX + 0.08, FY, beamBot, z - 0.08, z + 0.08, [0, 1, 0]);
  W.aabb(PX - 0.08, PX + 0.08, beamBot, P.beamTop, -PZ, PZ, [0, 0, 1]);
  const braceDir = [1, -1, 1, -1];
  P.postZ.forEach((z, i) => {
    const dz = braceDir[i];
    W.strut([PX, beamBot - 0.44, z + dz * 0.04], [PX, beamBot + 0.05, z + dz * 0.49], 0.08, 0.08, [1, 0, 0]);
  });
  const ps = P.porchSlope, pc = 1 / Math.hypot(1, ps);
  const peu = [pc, -ps * pc, 0], pen = [ps * pc, pc, 0];
  const underY = (x) => P.porchLedgerY - (x - HX) * ps - P.porchRoofT / pc;
  W.aabb(HX, HX + 0.12, 3.0, underY(HX + 0.12), -PZ, PZ, [0, 0, 1]);
  for (const z of [-3.3, -1.65, 0, 1.65, 3.3]) {
    const a = [HX, underY(HX), z], b = [P.porchRoofX - 0.05, underY(P.porchRoofX - 0.05), z];
    W.obox(add(mul(add(a, b), 0.5), mul(pen, -0.06)), peu, pen, [0, 0, 1], len(sub(b, a)) / 2, 0.06, 0.04, peu);
  }
  {
    const a = [HX, P.porchLedgerY, 0], b = [P.porchRoofX, P.porchLedgerY - (P.porchRoofX - HX) * ps, 0];
    B.porchRoof.obox(add(mul(add(a, b), 0.5), mul(pen, -P.porchRoofT / 2)), peu, pen, [0, 0, 1],
      len(sub(b, a)) / 2, P.porchRoofT / 2, PZ + 0.1, [0, 0, 1]);
  }
  // Porch soffit: boarded ceiling under the rafter line, split around the beam (posts, beam
  // and braces stay exposed), bay closures at both ends and a fascia over the rafter tails.
  const rafterD = 0.12, soffitT = 0.025, rafterEnd = P.porchRoofX - 0.05;
  const rafterBot = (x) => add([x, underY(x), 0], mul(pen, -rafterD));
  const soffit = (xa, xb) => {
    const a = rafterBot(xa), b = rafterBot(xb);
    W.obox(add(mul(add(a, b), 0.5), mul(pen, -soffitT / 2)), peu, pen, [0, 0, 1], len(sub(b, a)) / 2, soffitT / 2, PZ, [0, 0, 1]);
  };
  soffit(HX, PX - 0.08); soffit(PX + 0.08, rafterEnd);
  for (const zc of [-PZ + 0.02, PZ - 0.02]) {
    const a = [HX, underY(HX), zc], b = [rafterEnd, underY(rafterEnd), zc];
    W.obox(add(mul(add(a, b), 0.5), mul(pen, -(rafterD + soffitT) / 2)), peu, pen, [0, 0, 1], len(sub(b, a)) / 2, (rafterD + soffitT) / 2, 0.02, peu);
  }
  W.aabb(rafterEnd, rafterEnd + 0.03, rafterBot(rafterEnd)[1] - soffitT, underY(rafterEnd + 0.03), -PZ, PZ, [0, 0, 1]);
  function railing(axis, fixed, a0, a1) {
    const R = (lo, hi, y0, y1, w) => axis === 'z'
      ? W.aabb(fixed - w, fixed + w, y0, y1, lo, hi) : W.aabb(lo, hi, y0, y1, fixed - w, fixed + w);
    R(a0, a1, 1.25, 1.33, 0.045); R(a0, a1, 0.5, 0.56, 0.035);
    const n = Math.max(1, Math.round((a1 - a0) / 0.22));
    for (let i = 1; i < n; i++) { const t = a0 + (a1 - a0) * i / n; R(t - 0.022, t + 0.022, 0.56, 1.25, 0.022); }
  }
  railing('z', PX, -3.3 + 0.06, -0.95 - 0.06); railing('z', PX, 0.95 + 0.06, 3.3 - 0.06);
  railing('x', -3.3, HX, PX - 0.06); railing('x', 3.3, HX, PX - 0.06);

  // ---------- Steps: three risers from ground to deck ----------
  const rise = FY / 3;
  B.steps.aabb(P.porchX, P.porchX + 0.65, 0, rise, -P.stepHalfZ, P.stepHalfZ, [0, 0, 1]);
  B.steps.aabb(P.porchX, P.porchX + 0.35, rise, 2 * rise, -P.stepHalfZ, P.stepHalfZ, [0, 0, 1]);
  B.porchStone.aabb(P.porchX + 0.05, P.porchX + 0.65, 0, rise - 0.001, -P.stepHalfZ - 0.1, -P.stepHalfZ);
  B.porchStone.aabb(P.porchX + 0.05, P.porchX + 0.65, 0, rise - 0.001, P.stepHalfZ, P.stepHalfZ + 0.1);

  // ---------- Door leaf on a hinge pivot (static, closed; pivot ready for a later clip) ----------
  const L = { x0: -0.025, x1: 0.025, h: P.doorTop - FY - 0.02, w: 2 * P.doorHalfW - 0.02 };
  B.doorRed.aabb(L.x0, L.x1, 0.01, 0.01 + L.h, 0, L.w, [0, 1, 0]);
  for (const [za, zb] of [[0.1, 0.44], [0.54, 0.88]]) for (const [ya, yb] of [[0.22, 1.0], [1.18, 1.98]])
    B.doorRed.aabb(L.x1, L.x1 + 0.018, ya, yb, za, zb, [0, 1, 0]);
  for (const yb of [0.12, 2.02]) {
    B.doorMetal.aabb(L.x1, L.x1 + 0.012, yb - 0.025, yb + 0.025, -0.01, 0.46);
    B.doorMetal.aabb(-0.03, 0.03, yb - 0.08, yb + 0.08, -0.012, 0.012);
  }
  B.doorMetal.aabb(L.x1, L.x1 + 0.012, 0.95, 1.2, 0.84, 0.92);
  B.doorMetal.aabb(L.x1 + 0.012, L.x1 + 0.07, 1.06, 1.1, 0.86, 0.9);

  // ---------- assemble named reusable groups ----------
  const group = (name) => { const g = new THREE.Group(); g.name = name; root.add(g); return g; };
  const shell = group('Shell'), roof = group('Roof'), porch = group('Porch'), steps = group('Steps'),
    shutters = group('Shutters'), doorG = group('Door');
  createPart('ShellMasonry', B.masonry.geo(), M.masonry, { parent: shell });
  createPart('ShellStone', B.stone.geo(), M.stone, { parent: shell });
  createPart('ShellTrim', B.trim.geo(), M.trim, { parent: shell });
  createPart('ShellGlass', B.glass.geo(), M.glass, { parent: shell });
  createPart('ChimneyFlue', B.dark.geo(), M.dark, { parent: shell });
  createPart('InteriorCeiling', B.ceiling.geo(), M.trim, { parent: shell });
  createPart('InteriorFloor', B.floor.geo(), M.wood, { parent: shell });
  createPart('RoofShingles', B.roof.geo(), M.roof, { parent: roof });
  createPart('PorchTimber', B.porchWood.geo(), M.wood, { parent: porch });
  createPart('PorchPlinth', B.porchStone.geo(), M.stone, { parent: porch });
  createPart('PorchRoof', B.porchRoof.geo(), M.roof, { parent: porch });
  createPart('StepTreads', B.steps.geo(), M.wood, { parent: steps });
  createPart('ShutterPanels', B.olive.geo(), M.olive, { parent: shutters });
  createPart('ShutterHinges', B.hinge.geo(), M.metal, { parent: shutters });
  const hinge = createPivot('FrontDoor', [HX - 0.2, FY, -P.doorHalfW + 0.01], doorG);
  createPart('DoorLeaf', B.doorRed.geo(), M.red, { parent: hinge });
  createPart('DoorHardware', B.doorMetal.geo(), M.metal, { parent: hinge });
  return root;
}

Source from the sealed r36-local-review farmhouse, r_01f97f5e943e4c2693b6db8596f3e353.

Six saved review views of Farmhouse.
The saved GPU review sheet: front, right, back, left, top and three-quarter views.
Triangles
4,080
Meshes
16
Materials
10
Bounds · metres
9.12 × 6.97 × 9.05
Named parts
Mesh_RoofShingles
Mesh_PorchTimber

Checks read the file, not the picture.

Every render comes back with a QA report on the exported GLB: whether it is valid glTF, whether the parts the request named are there, and what it will cost to draw. Checks read geometry and export data, never pixels.

exportIntegrity
pass · 0 glTF errors / warnings
requirementReadiness
pass
visualQuality
pass
triangles
4,080
namedParts
16

Your coding agent drives the same tools.

Kiln has no model of its own. Your agent supplies the model; no separate model key is needed.

Create a workspace with kiln-init. It writes the MCP configuration and skills for your harness. Follow the generated START.md to connect.

  • --harness claude
  • --harness codex
  • --harness opencode
  • --harness hermes
  • --harness agy
  • --harness copilot
  • --harness cursor-agent
Create an asset workspace
npx --offline --no -- kiln-init ../my-assets --harness opencode
Recorded farmhouse tool exchange · Kiln 0.8.0 · abridged
kiln_discover({"ids":["compilePortableMaterialSpecV2"]})
// → {"id":"operation:compilePortableMaterialSpecV2"}
// … further catalog fields omitted

kiln_render({"programRef":"p_e7510a92b181","projectId":"farm-production"})
// → {"ok":true,"programRef":"p_e7510a92b181","tris":3696}
// … capture arguments, views and further checks omitted

kiln_edit({
  "programRef": "p_e7510a92b181",
  "edits": [
    {
      "oldString": "masonry: await compilePortableMaterialSpecV2(MAT.masonry),",
      "newString": "masonry: await compilePortableMaterialSpecV2({ ...MAT.masonry, baseColor: 0xeadcbf }),"
    }
  ]
})
// → {"ok":true,"programRef":"p_8393cb60da1a","parentRef":"p_e7510a92b181"}
// … six edits, capture arguments and preservation evidence omitted

kiln_save({
  "programRef": "p_8393cb60da1a",
  "name": "Farmhouse",
  "collection": "project",
  "attribution": {
    "model": "claude-opus-5-5",
    "harness": "claude",
    "author": "claude-opus-5-5"
  }
})
// → asset.revisionId: r_8420afabf8fb4d58997ea50b7d13c4c9
// … project arguments, file hashes and further result fields omitted

Recorded Claude Opus 5.5 calls through Claude Code on 27 September 2026. These excerpts precede the r33 delivery and the later wood-floor refinement. The run used Kiln 0.8.0, so this is a record of that version, not a 1.3 transcript. Ellipses mark omitted payloads; local paths and host identifiers are removed. Read the complete tool contracts.

The local toolchain ships together.

The 1.3 package includes the tools, authoring guidance and local review interface. Add the GPU service when material appearance matters.

MCP server
17 tools on the packaged local host: discover, render, inspect, revise and retain exact work.
CLI
Render source, export a GLB or manage saved revisions from your terminal.
TypeScript library
Use the engine and its host ports in your own application.
Agent skills
Workspace setup, authoring, refinement, QA, scene composition and batch dispatch.
GPU render service
Optional material-faithful views. CPU fallback checks geometry and reports its fidelity.
Local Workshop
Library, optional Projects and Live Review share saved assets and observation records.
Material library
Pin material records and texture maps; keep recipes and provenance with the asset.

The saved work is yours to inspect.

Browse the models, read the source and follow their revisions. Commons assets can be used without installing Kiln.

Earlier examples

Earlier examples from earlier Kiln versions.

Open the archive

Try Kiln 1.3 locally.

Install @instruktlabs/kiln from npm, then connect your coding agent to a local asset workspace. Use Node.js 22 or 24 LTS; the installation guide lists the complete compatibility range.

Local use is free and account-free. Your agent supplies the model; Kiln's tools need no separate model API key. Bun is only needed for engine development.

Read the installation guide
Install Kiln 1.3
mkdir kiln-install
cd kiln-install
npm init -y
npm install @instruktlabs/kiln
Connect your coding agent
npx --offline --no -- kiln-init ../my-assets --harness opencode
cd ../my-assets
# Follow START.md for your harness