Skip to content
Kiln0.10Get Kiln

Tower, keeper’s buildings and a rocky base

Earlier examples from earlier Kiln versions.

Historical gallery render of Lighthouse; see the poster provenance below.

Drag to orbit after opening. The model starts stationary.

For your engine. The download is a standard glTF 2.0 binary (GLB) with PBR metallic-roughness materials. Optional extensions declared by this file: KHR_materials_emissive_strength. Importer support varies; see the Blender and Unity guide. glTF stores materials, not lighting or tone mapping, so your engine decides how they read. This 3D view tone-maps with Review Neutral, the Khronos PBR Neutral construction with a smaller glare offset (0.015 instead of 0.04), at exposure 0.9. Its Tone mapping control also shows ACES and Linear, for comparison.

Build measurements

Triangles
16,472
Estimated draws
260
Materials
15
Textures
0
Animation clips
0
Bounds X × Y × Z
12.2 × 13.33 × 12.2 m
Build warnings
1

Measurements come from this build.

Download this build

Runtime: 334,716 bytes. Original: 334,492 bytes.

These GLBs are build outputs of the MIT-licensed example source; no separate terms are stated for the builds.

Runtime provenance metadata
SHA-256 download hashes
Runtime GLB
776968b91490b066d1bfce6c999a73bdab2963708861b787f1ed9737e5977bf9
Original GLB
d69916e0b02f2e04fe9f5d8344b17be2950d3201030f4cd2b6f730806a1744ab
Source
432847b5d3f7ad1f164602484559379d4d9ff0a98ce05d8b7abd55c229a5db08

The example source is part of the Kiln repository. Repository licence: MIT.

These GLBs are build outputs of the MIT-licensed example source; no separate terms are stated for the builds.

Recorded authorship

Muse Spark 1.3 through OpenCode

Source-header credit

Source access
Source header declares no repository implementation or finished examples supplied
Inherited context
Not independently recorded; source-header declarations only
Starting example
None supplied, according to source header
Human input
preserve rock UVs during subdivision (2026-09-05).
Authoring review
Six-view review declared; renderer fidelity not recorded

Gallery GPU render of this exact source. Source, artifact, image hashes and camera settings are recorded alongside the poster; the artifact hash names the GLB bytes the image was rendered from, which the downloadable rebuild reproduces byte for byte only on the platform that recorded it.

Poster camera and render record

The source behind this build

lighthouse.kiln.js
// Authored by: opencode-go/muse-spark-1.3-contributor, via opencode.
//
// Written by the model itself through the Kiln MCP tools: it wrote the
// program, rendered it, looked at its own six-view contact sheet, and
// revised. The original geometry was authored by that model.
// Maintainer revision: preserve rock UVs during subdivision (2026-09-05).
//
// Dispatched into a clean directory containing only the brief and the Kiln
// skills, with no access to this repository or to any finished example.

const meta = { name: 'Lighthouse', category: 'prop' };

async function build() {
  const root = createRoot('Lighthouse');

  const stone = gameMaterial(0xb8ad98, { roughness: 0.93 });
  const stoneDark = gameMaterial(0x8d8471, { roughness: 0.95 });
  const trimWhite = gameMaterial(0xe8e2d4, { roughness: 0.85 });
  const plinthGray = gameMaterial(0x7d7a74, { roughness: 0.95 });
  const iron = gameMaterial(0x1e2126, { roughness: 0.55, metalness: 0.75 });
  const copper = gameMaterial(0x8a5a33, { roughness: 0.45, metalness: 0.85 });
  const copperDark = gameMaterial(0x5e3d24, { roughness: 0.6, metalness: 0.7 });
  const glassMat = glassMaterial(0xbfe3ef, { opacity: 0.35, roughness: 0.1 });
  const lensMat = gameMaterial(0xffe9a8, { emissive: 0xffc63a, emissiveIntensity: 2.2, roughness: 0.3 });
  const lampMat = gameMaterial(0xfff6d8, { emissive: 0xfff2b0, emissiveIntensity: 3.5, roughness: 0.4 });
  const woodDoor = gameMaterial(0x4a3220, { roughness: 0.85 });
  const slate = gameMaterial(0x46505a, { roughness: 0.85 });
  const rockMat = gameMaterial(0x6f6e68, { roughness: 0.98 });
  const rockDark = gameMaterial(0x55544f, { roughness: 0.98 });
  const grassMat = gameMaterial(0x5a6b46, { roughness: 0.95 });

  // rock island
  createPart('BasePlatform', cylinderGeo(5.6, 6.1, 0.5, 28), rockDark, { position: [0, 0.25, 0], parent: root });
  createPart('GrassTop', cylinderGeo(5.35, 5.5, 0.12, 28), grassMat, { position: [0, 0.55, 0], parent: root });

  function makeRock(name, px, pz, s, sy, rotY, mat, seed) {
    let g = boxGeo(s, sy, s * 0.8);
    g = mergeVertices(g);
    const pos = g.getAttribute('position');
    for (let i = 0; i < pos.count; i++) {
      const vx = pos.getX(i), vy = pos.getY(i), vz = pos.getZ(i);
      const j = Math.sin(vx * 12.9898 + seed * 78.233 + vz * 37.719) * 0.16 + Math.cos(vy * 9.5 + seed * 13.7) * 0.1;
      pos.setXYZ(i, vx * (1 + j * 0.5), vy * (1 + j * 0.35), vz * (1 + j * 0.5));
    }
    g.computeVertexNormals();
    g = subdivide(g, 2, { preserveUV: true });
    createPart(name, g, mat, { position: [px, 0.35 + sy * 0.25, pz], rotation: [0, rotY, 0], parent: root });
  }
  makeRock('Rock1', 4.4, 1.2, 1.5, 1.1, 20, rockMat, 1);
  makeRock('Rock2', -4.2, 2.2, 1.8, 1.3, 65, rockMat, 2);
  makeRock('Rock3', 3.4, -3.6, 1.6, 1.0, 110, rockDark, 3);
  makeRock('Rock4', -3.6, -3.4, 2.0, 1.4, 40, rockMat, 4);
  makeRock('Rock5', 0.8, 4.7, 1.3, 0.9, 80, rockDark, 5);
  makeRock('Rock6', -1.2, -4.9, 1.4, 1.0, 15, rockMat, 6);
  makeRock('Rock7', 5.3, -1.1, 1.1, 0.8, 130, rockMat, 7);
  makeRock('Rock8', -5.3, -0.4, 1.2, 0.9, 95, rockDark, 8);

  // tower plinth
  createPart('PlinthStep1', cylinderGeo(2.15, 2.3, 0.35, 28), plinthGray, { position: [0, 0.78, 0], parent: root });
  createPart('PlinthStep2', cylinderGeo(1.9, 2.05, 0.35, 28), stoneDark, { position: [0, 1.1, 0], parent: root });

  // tapered tower
  const towerH = 7.2;
  const towerY = 1.275 + towerH / 2;
  createPart('Tower', cylinderGeo(1.15, 1.68, towerH, 40), stone, { position: [0, towerY, 0], parent: root });

  for (let i = 0; i < 12; i++) {
    const t = i / 11;
    const r = 1.68 + (1.15 - 1.68) * t;
    const y = 1.275 + 0.35 + t * (towerH - 0.5);
    createPart('Course' + i, torusGeo(r + 0.015, 0.028, 6, 36), stoneDark, { position: [0, y, 0], rotation: [90, 0, 0], parent: root });
  }
  for (let c = 0; c < 10; c++) {
    const t = (c + 0.5) / 10;
    const r = 1.68 + (1.15 - 1.68) * t;
    const y = 1.275 + 0.35 + t * (towerH - 0.5);
    const n = 18;
    for (let k = 0; k < n; k++) {
      if ((k + c) % 2 === 0) continue;
      const a = (k / n) * Math.PI * 2 + c * 0.17;
      createPart('Joint_c' + c + '_' + k, boxGeo(0.03, 0.5, 0.035), stoneDark, { position: [Math.cos(a) * (r + 0.008), y + 0.3, Math.sin(a) * (r + 0.008)], rotation: [0, -a * 180 / Math.PI, 0], parent: root });
    }
  }

  createPart('TowerBaseTrim', torusGeo(1.78, 0.07, 8, 36), trimWhite, { position: [0, 1.65, 0], rotation: [90, 0, 0], parent: root });
  createPart('TowerMidTrim', torusGeo(1.42, 0.055, 8, 36), trimWhite, { position: [0, 5.2, 0], rotation: [90, 0, 0], parent: root });

  // door (+X)
  const doorX = 1.62;
  createPart('DoorFrame', boxGeo(0.18, 2.1, 1.2), trimWhite, { position: [doorX, 2.2, 0], parent: root });
  createPart('Door', boxGeo(0.16, 1.85, 0.9), woodDoor, { position: [doorX + 0.04, 2.1, 0], parent: root });
  createPart('DoorLintel', boxGeo(0.3, 0.18, 1.35), stoneDark, { position: [doorX, 3.32, 0], parent: root });
  createPart('DoorHood', boxGeo(0.5, 0.08, 1.4), iron, { position: [doorX + 0.15, 3.48, 0], parent: root });
  createPart('DoorStep', boxGeo(0.7, 0.18, 1.3), plinthGray, { position: [doorX + 0.35, 1.2, 0], parent: root });
  createPart('DoorKnob', sphereGeo(0.05, 8, 6), iron, { position: [doorX + 0.14, 2.05, 0.3], parent: root });

  // windows spiral
  const winAngles = [0.9, 2.6, 4.4];
  const winHeights = [3.6, 5.1, 6.6];
  for (let i = 0; i < 3; i++) {
    const a = winAngles[i], y = winHeights[i];
    const r = 1.68 + (1.15 - 1.68) * ((y - 1.275) / towerH);
    const px = Math.cos(a) * (r - 0.02), pz = Math.sin(a) * (r - 0.02);
    const deg = -a * 180 / Math.PI;
    createPart('WinFrame' + i, boxGeo(0.12, 0.9, 0.64), trimWhite, { position: [px, y, pz], rotation: [0, deg, 0], parent: root });
    createPart('WinGlass' + i, boxGeo(0.14, 0.68, 0.44), iron, { position: [px, y, pz], rotation: [0, deg, 0], parent: root });
    createPart('WinMullV' + i, boxGeo(0.15, 0.68, 0.05), trimWhite, { position: [px, y, pz], rotation: [0, deg, 0], parent: root });
    createPart('WinSill' + i, boxGeo(0.26, 0.09, 0.74), trimWhite, { position: [Math.cos(a) * (r + 0.02), y - 0.5, Math.sin(a) * (r + 0.02)], rotation: [0, deg, 0], parent: root });
  }

  // corbelled gallery
  const galBase = 1.275 + towerH;
  createPart('Corbel1', cylinderGeo(1.3, 1.12, 0.28, 28), stoneDark, { position: [0, galBase + 0.14, 0], parent: root });
  createPart('Corbel2', cylinderGeo(1.55, 1.28, 0.28, 28), stone, { position: [0, galBase + 0.42, 0], parent: root });
  createPart('Corbel3', cylinderGeo(1.8, 1.53, 0.3, 28), stoneDark, { position: [0, galBase + 0.7, 0], parent: root });
  createPart('GalleryDeck', cylinderGeo(1.92, 1.92, 0.16, 28), plinthGray, { position: [0, galBase + 0.9, 0], parent: root });
  for (let i = 0; i < 16; i++) {
    const a = (i / 16) * Math.PI * 2;
    createPart('CorbelBlock' + i, boxGeo(0.22, 0.3, 0.22), stoneDark, { position: [Math.cos(a) * 1.35, galBase + 0.3, Math.sin(a) * 1.35], rotation: [0, -a * 180 / Math.PI, 0], parent: root });
  }
  // deck anchor bolts
  for (let i = 0; i < 12; i++) {
    const a = (i / 12) * Math.PI * 2;
    createPart('DeckBolt' + i, cylinderGeo(0.035, 0.035, 0.07, 6), iron, { position: [Math.cos(a) * 1.65, galBase + 1.0, Math.sin(a) * 1.65], parent: root });
  }

  // railing
  const railY = galBase + 0.98;
  const railR = 1.8;
  for (let i = 0; i < 20; i++) {
    const a = (i / 20) * Math.PI * 2;
    createPart('RailPost' + i, cylinderGeo(0.03, 0.03, 1.0, 8), iron, { position: [Math.cos(a) * railR, railY + 0.5, Math.sin(a) * railR], parent: root });
  }
  createPart('RailTop', torusGeo(railR, 0.045, 8, 36), iron, { position: [0, railY + 1.0, 0], rotation: [90, 0, 0], parent: root });
  createPart('RailMid', torusGeo(railR, 0.03, 6, 36), iron, { position: [0, railY + 0.55, 0], rotation: [90, 0, 0], parent: root });
  // gallery access hatch (iron trap on deck)
  createPart('DeckHatch', boxGeo(0.6, 0.06, 0.6), iron, { position: [0.9, galBase + 1.0, 0.9], parent: root });

  // lantern room
  const lantBase = galBase + 0.98;
  createPart('LanternFloor', cylinderGeo(1.32, 1.32, 0.14, 24), iron, { position: [0, lantBase + 0.05, 0], parent: root });
  createPart('LanternSillRing', torusGeo(1.3, 0.06, 8, 24), iron, { position: [0, lantBase + 0.14, 0], rotation: [90, 0, 0], parent: root });
  for (let i = 0; i < 8; i++) {
    const a = (i / 8) * Math.PI * 2;
    createPart('Mullion' + i, boxGeo(0.09, 1.6, 0.09), iron, { position: [Math.cos(a) * 1.28, lantBase + 0.93, Math.sin(a) * 1.28], rotation: [0, -a * 180 / Math.PI, 0], parent: root });
  }
  createPart('LanternGlass', cylinderGeo(1.28, 1.28, 1.55, 8), glassMat, { position: [0, lantBase + 0.93, 0], parent: root });
  createPart('LanternTopRing', torusGeo(1.3, 0.06, 8, 24), iron, { position: [0, lantBase + 1.72, 0], rotation: [90, 0, 0], parent: root });

  createPart('FresnelLens', cylinderGeo(0.55, 0.62, 1.15, 8), lensMat, { position: [0, lantBase + 0.93, 0], parent: root });
  createPart('FresnelRing1', torusGeo(0.6, 0.035, 6, 8), iron, { position: [0, lantBase + 0.65, 0], rotation: [90, 22.5, 0], parent: root });
  createPart('FresnelRing2', torusGeo(0.585, 0.035, 6, 8), iron, { position: [0, lantBase + 0.93, 0], rotation: [90, 22.5, 0], parent: root });
  createPart('FresnelRing3', torusGeo(0.565, 0.035, 6, 8), iron, { position: [0, lantBase + 1.2, 0], rotation: [90, 22.5, 0], parent: root });
  createPart('LensCapTop', cylinderGeo(0.58, 0.58, 0.08, 8), iron, { position: [0, lantBase + 1.53, 0], parent: root });
  createPart('LensCapBot', cylinderGeo(0.65, 0.65, 0.08, 8), iron, { position: [0, lantBase + 0.33, 0], parent: root });
  createPart('LampBulb', sphereGeo(0.22, 12, 8), lampMat, { position: [0, lantBase + 0.93, 0], parent: root });
  createPart('LampPedestal', cylinderGeo(0.12, 0.2, 0.35, 12), iron, { position: [0, lantBase + 0.28, 0], parent: root });

  // copper dome + weathervane
  const roofY = lantBase + 1.78;
  const dome = createPart('CopperDome', sphereGeo(1.5, 20, 12), copper, { position: [0, roofY + 0.1, 0], parent: root });
  dome.scale.set(1, 0.62, 1);
  createPart('DomeRim', torusGeo(1.48, 0.07, 8, 28), copperDark, { position: [0, roofY + 0.12, 0], rotation: [90, 0, 0], parent: root });
  for (let i = 0; i < 8; i++) {
    const a = (i / 8) * Math.PI * 2;
    beamBetween('DomeRib' + i, [0, roofY + 1.0, 0], [Math.cos(a) * 1.42, roofY + 0.12, Math.sin(a) * 1.42], 0.028, copperDark, { parent: root });
  }
  createPart('FinialBall', sphereGeo(0.14, 12, 8), copperDark, { position: [0, roofY + 1.12, 0], parent: root });
  createPart('VaneRod', cylinderGeo(0.025, 0.025, 1.0, 8), iron, { position: [0, roofY + 1.6, 0], parent: root });
  createPart('VaneArrow', boxGeo(0.7, 0.04, 0.04), iron, { position: [0.05, roofY + 1.85, 0], parent: root });
  createPart('VaneHead', coneXGeo(0.09, 0.22, 8), iron, { position: [0.5, roofY + 1.85, 0], parent: root });
  createPart('VaneTail', boxGeo(0.04, 0.22, 0.16), iron, { position: [-0.3, roofY + 1.85, 0], parent: root });
  createPart('VaneCrossX', boxGeo(0.04, 0.04, 0.5), iron, { position: [0, roofY + 1.65, 0], parent: root });
  createPart('VaneCrossZ', boxGeo(0.5, 0.04, 0.04), iron, { position: [0, roofY + 1.55, 0], parent: root });

  // keeper's cottage
  //
  // The gable ends are pentagons cut to the roof, not rectangles. The first
  // version stood a plain 1.1 m slab on each end wall and let the roof pass
  // across it, which left most of a metre of tan stone standing above the
  // slate at both eaves -- the cottage read as a roofless shell with a roof
  // dropped inside it. A gable is the wall that FILLS the triangle under the
  // pitch, so it is derived from the pitch rather than guessed: same ridge
  // line, same slope, tucked 20 mm under the roof underside so the two can
  // never fight for the same pixel.
  const cx = -0.6, cz = 3.6;
  const roofPitch = 30;
  const roofSlope = Math.tan((roofPitch * Math.PI) / 180);
  const roofT = 0.1;                                  // slate slab thickness
  const ridgeMidY = 3.38 + 0.975 * Math.sin((roofPitch * Math.PI) / 180);
  const roofUnderY = (dz) =>
    ridgeMidY - roofT / 2 / Math.cos((roofPitch * Math.PI) / 180) - roofSlope * Math.abs(dz);
  const gableHalfZ = 1.6;                             // outer face of the end walls
  const gableGeo = await extrudeProfile(
    [
      [-gableHalfZ, 2.70],                            // buried 0.2 m into the wall below
      [gableHalfZ, 2.70],
      [gableHalfZ, roofUnderY(gableHalfZ) - 0.02],
      [0, roofUnderY(0) - 0.02],
      [-gableHalfZ, roofUnderY(gableHalfZ) - 0.02],
    ],
    { depth: 0.18, axis: 'x' },
  );
  createPart('CottageWallN', boxGeo(3.6, 2.3, 0.18), stone, { position: [cx, 1.75, cz + 1.5], parent: root });
  createPart('CottageWallS', boxGeo(3.6, 2.3, 0.18), stone, { position: [cx, 1.75, cz - 1.5], parent: root });
  createPart('CottageWallW', boxGeo(0.18, 2.3, 3.2), stone, { position: [cx - 1.8, 1.75, cz], parent: root });
  createPart('CottageWallE', boxGeo(0.18, 2.3, 3.2), stone, { position: [cx + 1.8, 1.75, cz], parent: root });
  createPart('CottageFloor', boxGeo(3.8, 0.15, 3.4), plinthGray, { position: [cx, 0.68, cz], parent: root });
  createPart('CottageGableW', gableGeo, stoneDark, { position: [cx - 1.8, 0, cz], parent: root });
  createPart('CottageGableE', gableGeo, stoneDark, { position: [cx + 1.8, 0, cz], parent: root });
  createPart('CottageRoofL', boxGeo(4.2, 0.1, 1.95), slate, { position: [cx, 3.38, cz + 0.82], rotation: [30, 0, 0], parent: root });
  createPart('CottageRoofR', boxGeo(4.2, 0.1, 1.95), slate, { position: [cx, 3.38, cz - 0.82], rotation: [-30, 0, 0], parent: root });
  createPart('CottageRidge', boxGeo(4.20, 0.09, 0.2), stoneDark, { position: [cx, 3.86, cz], parent: root });
  // roof battens (slate courses)
  for (let i = 0; i < 3; i++) {
    createPart('BattenL' + i, boxGeo(4.20, 0.05, 0.08), stoneDark, { position: [cx, 3.18 + i * 0.24, cz + 1.18 - i * 0.4], rotation: [30, 0, 0], parent: root });
    createPart('BattenR' + i, boxGeo(4.20, 0.05, 0.08), stoneDark, { position: [cx, 3.18 + i * 0.24, cz - 1.18 + i * 0.4], rotation: [-30, 0, 0], parent: root });
  }
  createPart('Chimney', boxGeo(0.5, 1.7, 0.5), stoneDark, { position: [cx - 1.0, 3.9, cz + 0.3], parent: root });
  createPart('ChimneyCap', boxGeo(0.66, 0.12, 0.66), plinthGray, { position: [cx - 1.0, 4.78, cz + 0.3], parent: root });
  createPart('ChimneyPot', cylinderGeo(0.12, 0.16, 0.35, 10), stoneDark, { position: [cx - 1.0, 5.0, cz + 0.3], parent: root });
  createPart('CottageDoor', boxGeo(0.1, 1.7, 0.85), woodDoor, { position: [cx + 1.92, 1.55, cz], parent: root });
  createPart('CottageDoorTrim', boxGeo(0.08, 1.9, 1.05), trimWhite, { position: [cx + 1.88, 1.6, cz], parent: root });
  createPart('CottageDoorStep', boxGeo(0.5, 0.14, 1.1), plinthGray, { position: [cx + 2.2, 0.72, cz], parent: root });
  createPart('CottageWinN', boxGeo(1.0, 0.7, 0.1), iron, { position: [cx + 0.3, 2.0, cz + 1.55], parent: root });
  createPart('CottageWinTrimN', boxGeo(1.15, 0.85, 0.06), trimWhite, { position: [cx + 0.3, 2.0, cz + 1.52], parent: root });
  createPart('CottageMullVN', boxGeo(0.06, 0.7, 0.12), trimWhite, { position: [cx + 0.3, 2.0, cz + 1.56], parent: root });
  createPart('CottageWinS', boxGeo(1.0, 0.7, 0.1), iron, { position: [cx + 0.3, 2.0, cz - 1.55], parent: root });
  createPart('CottageWinTrimS', boxGeo(1.15, 0.85, 0.06), trimWhite, { position: [cx + 0.3, 2.0, cz - 1.52], parent: root });
  createPart('CottageMullVS', boxGeo(0.06, 0.7, 0.12), trimWhite, { position: [cx + 0.3, 2.0, cz - 1.56], parent: root });
  createPart('Passage', boxGeo(1.6, 1.9, 1.4), stone, { position: [-0.3, 1.6, 2.2], rotation: [0, 25, 0], parent: root });
  // stone path: tower door to cottage + edge
  createPart('Path1', boxGeo(1.0, 0.08, 1.1), plinthGray, { position: [2.6, 0.65, 0.4], parent: root });
  createPart('Path2', boxGeo(0.9, 0.08, 1.0), plinthGray, { position: [2.7, 0.65, 1.6], parent: root });
  createPart('Path3', boxGeo(0.9, 0.08, 1.0), plinthGray, { position: [2.2, 0.65, 2.7], parent: root });
  createPart('Path4', boxGeo(0.8, 0.08, 0.9), plinthGray, { position: [1.6, 0.65, 3.4], parent: root });

  return root;
}

Brief and retained revisions

No brief or earlier revisions are recorded. The source download contains the version built for this page.