Gothic gatehouse
Twin towers, arched passage and approach bridge
Earlier examples from earlier Kiln versions.

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. 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
- 10,428
- Estimated draws
- 546
- Materials
- 5
- Textures
- 0
- Animation clips
- 0
- Bounds X × Y × Z
- 13.3 × 16.2 × 13.52 m
- Build warnings
- 0
Measurements come from this build.
Download this build
Runtime: 167,272 bytes. Original: 167,040 bytes.
These GLBs are build outputs of the MIT-licensed example source; no separate terms are stated for the builds.
Runtime provenance metadataSHA-256 download hashes
- Runtime GLB
- 26ceb09d33705cac0e710006806de7ec737e2a677e4762e1cab82625c0e3f3d9
- Original GLB
- 73e77153f1b3972649f243e53406ee35fc0d8f891a6cda592590c71ec26ba1ef
- Source
- cdda0ed41156c36ace847ea4dd0296316f09f0922708fb5f7024c2c212e73a55
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
Gemini 3.8 Flash through Antigravity CLI (agy)
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
- Header declares no hand-authored source; other intervention not recorded
- 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 recordThe source behind this build
// Authored by: gemini-3.8-flash-high, via agy.
//
// 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. Not a line of it is hand-authored.
//
// 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: 'GothicGatehouse', category: 'architecture', role: 'building' };
function build() {
const root = createRoot('GothicGatehouse');
// ==========================================
// MATERIALS (High quality PBR palette, 5 materials)
// ==========================================
const stoneAshlar = gameMaterial(0x8e887d, { roughness: 0.88 }); // Main masonry
const stoneDressing = gameMaterial(0x6e685f, { roughness: 0.84 }); // Trim, voussoirs, corbels, copings
const stoneDark = gameMaterial(0x32302c, { roughness: 0.95 }); // Arrow slits, murder holes, reveals
const oakWood = gameMaterial(0x56402a, { roughness: 0.78 }); // Drawbridge planks, cleats, gates
const wroughtIron = gameMaterial(0x242426, { roughness: 0.52, metalness: 0.85 }); // Chains, portcullis, spikes, straps
// ==========================================
// DIMENSIONS & SPATIAL ANCHORS (+X forward, +Y up, +Z right)
// ==========================================
const towerRadius = 2.4;
const towerHeight = 12.6;
const towerX = 0.6; // Towers project forward for flanking fire
const towerZ = 3.7; // +/- 3.7 along Z
const wallDepth = 4.4; // Extends from X = -2.2 to X = +2.2
const wallWidth = 4.6; // Spans between towers from Z = -2.3 to Z = +2.3
const wallHeight = 8.8; // Central curtain wall height
const passageFloorY = 0.7; // Raised ground/passage level above ditch
// ==========================================
// 1. FOUNDATIONS & SUBSTRUCTURE (Sitting on Y = 0)
// ==========================================
// Central gatehouse foundation block
createPart('GatehouseFoundation', boxGeo(wallDepth + 0.3, passageFloorY, wallWidth + 0.2), stoneDressing, {
position: [0, passageFloorY / 2, 0],
parent: root
});
// Flagstone floor inside passage
createPart('PassageFloor', boxGeo(wallDepth + 0.2, 0.1, 2.4), stoneAshlar, {
position: [0, passageFloorY + 0.05, 0],
parent: root
});
// ==========================================
// 2. CENTRAL GATEHOUSE BLOCK & WALLS
// ==========================================
// Left and Right jamb walls
const jambThickZ = 1.1;
createPart('GateJambL', boxGeo(wallDepth, 3.2, jambThickZ), stoneAshlar, {
position: [0, passageFloorY + 1.6, -1.75],
parent: root
});
createPart('GateJambR', boxGeo(wallDepth, 3.2, jambThickZ), stoneAshlar, {
position: [0, passageFloorY + 1.6, 1.75],
parent: root
});
// Dressed stone jamb quoins (front entrance)
for (let y = passageFloorY + 0.3; y <= passageFloorY + 3.2; y += 0.6) {
createPart('JambQuoinL_' + y.toFixed(1), boxGeo(0.5, 0.26, 0.22), stoneDressing, {
position: [wallDepth / 2 + 0.02, y, -1.2],
parent: root
});
createPart('JambQuoinR_' + y.toFixed(1), boxGeo(0.5, 0.26, 0.22), stoneDressing, {
position: [wallDepth / 2 + 0.02, y, 1.2],
parent: root
});
}
// Upper gatehouse wall (above arch springing)
const upperWallH = wallHeight - (passageFloorY + 3.2);
createPart('WallUpper', boxGeo(wallDepth, upperWallH, wallWidth), stoneAshlar, {
position: [0, passageFloorY + 3.2 + upperWallH / 2, 0],
parent: root
});
// Front spandrel infill above arch
createPart('FrontSpandrel', boxGeo(0.4, 2.2, 2.5), stoneAshlar, {
position: [wallDepth / 2 - 0.2, passageFloorY + 4.6, 0],
parent: root
});
// Rear wall opening spandrel
createPart('RearSpandrel', boxGeo(0.4, 2.2, 2.5), stoneAshlar, {
position: [-wallDepth / 2 + 0.2, passageFloorY + 4.6, 0],
parent: root
});
// Horizontal string-course mouldings on central block
createPart('CentStringCourse1', boxGeo(wallDepth + 0.15, 0.18, wallWidth + 0.1), stoneDressing, {
position: [0, passageFloorY + 3.3, 0],
parent: root
});
createPart('CentStringCourse2', boxGeo(wallDepth + 0.15, 0.18, wallWidth + 0.1), stoneDressing, {
position: [0, wallHeight - 0.1, 0],
parent: root
});
// ==========================================
// 3. POINTED GOTHIC ARCHWAY & PORTAL
// ==========================================
// Equilateral pointed arch:
// Springing points at Z = +/-1.2, Y = passageFloorY + 3.2 = 3.9
// Apex at Z = 0, Y = 3.9 + 2.08 = 5.98
const archSpringY = passageFloorY + 3.2;
const archSpan = 2.4;
const archR = archSpan;
const voussoirsPerSide = 7;
// Outer arch voussoirs (Front portal & Rear portal)
for (let side = -1; side <= 1; side += 2) {
const centerZ = side * (archSpan / 2);
for (let i = 0; i < voussoirsPerSide; i++) {
const theta = ((i + 0.5) / voussoirsPerSide) * (Math.PI / 3);
const zLocal = -side * archR * Math.cos(theta);
const yLocal = archR * Math.sin(theta);
const vz = centerZ + zLocal;
const vy = archSpringY + yLocal;
const rotX = side * (theta * (180 / Math.PI) - 90);
createPart('VoussoirF_' + side + '_' + i, boxGeo(0.45, 0.32, 0.26), stoneDressing, {
position: [wallDepth / 2 + 0.05, vy, vz],
rotation: [rotX, 0, 0],
parent: root
});
createPart('VoussoirB_' + side + '_' + i, boxGeo(0.45, 0.32, 0.26), stoneDressing, {
position: [-wallDepth / 2 - 0.05, vy, vz],
rotation: [rotX, 0, 0],
parent: root
});
}
}
// Pointed arch hood moulding (dripstone) above front arch
for (let side = -1; side <= 1; side += 2) {
const centerZ = side * (archSpan / 2);
for (let i = 0; i < 5; i++) {
const theta = ((i + 0.5) / 5) * (Math.PI / 3);
const vz = centerZ - side * (archR + 0.2) * Math.cos(theta);
const vy = archSpringY + (archR + 0.2) * Math.sin(theta);
const rotX = side * (theta * (180 / Math.PI) - 90);
createPart('HoodMould_' + side + '_' + i, boxGeo(0.18, 0.15, 0.2), stoneDressing, {
position: [wallDepth / 2 + 0.16, vy, vz],
rotation: [rotX, 0, 0],
parent: root
});
}
}
// Vault ribs inside the gate passage ceiling
for (let side = -1; side <= 1; side += 2) {
const centerZ = side * (archSpan / 2);
for (let i = 0; i < 4; i++) {
const theta = ((i + 0.5) / 4) * (Math.PI / 3);
const vz = centerZ - side * (archR - 0.05) * Math.cos(theta);
const vy = archSpringY + (archR - 0.05) * Math.sin(theta);
const rotX = side * (theta * (180 / Math.PI) - 90);
createPart('VaultRib_' + side + '_' + i, boxGeo(wallDepth - 0.6, 0.22, 0.35), stoneAshlar, {
position: [0, vy, vz],
rotation: [rotX, 0, 0],
parent: root
});
}
}
// ==========================================
// 4. PORTCULLIS IN RECESSED GROOVES (RAISED)
// ==========================================
const portX = 0.9;
// Left and right vertical stone grooves in the arch jambs
createPart('PortGrooveL', boxGeo(0.2, 3.8, 0.14), stoneDark, {
position: [portX, passageFloorY + 2.0, -1.18],
parent: root
});
createPart('PortGrooveR', boxGeo(0.2, 3.8, 0.14), stoneDark, {
position: [portX, passageFloorY + 2.0, 1.18],
parent: root
});
// Raised portcullis frame (bottom sits at Y = passageFloorY + 2.4 = 3.1)
const portBaseY = passageFloorY + 2.4;
const portHeight = 2.6;
const portHalfW = 1.05;
// Vertical wooden stiles with iron shoes and sharp spikes
const numPortV = 7;
for (let i = 0; i < numPortV; i++) {
const pz = -portHalfW + (i / (numPortV - 1)) * (portHalfW * 2);
createPart('PortBarV_' + i, boxGeo(0.08, portHeight, 0.08), oakWood, {
position: [portX, portBaseY + portHeight / 2, pz],
parent: root
});
createPart('PortSpike_' + i, coneYGeo(0.05, 0.35, 4), wroughtIron, {
position: [portX, portBaseY - 0.16, pz],
rotation: [180, 0, 0], // Pointing down
parent: root
});
}
// Horizontal oak bars & iron reinforcement straps
const numPortH = 5;
for (let j = 0; j < numPortH; j++) {
const py = portBaseY + 0.3 + j * 0.52;
createPart('PortBarH_' + j, boxGeo(0.1, 0.08, portHalfW * 2 + 0.1), oakWood, {
position: [portX, py, 0],
parent: root
});
createPart('PortBandH_' + j, boxGeo(0.11, 0.03, portHalfW * 2 + 0.1), wroughtIron, {
position: [portX, py, 0],
parent: root
});
}
// Top suspension shackles
createPart('PortShackleL', torusGeo(0.08, 0.02, 5, 8), wroughtIron, {
position: [portX, portBaseY + portHeight + 0.05, -0.6],
rotation: [0, 90, 0],
parent: root
});
createPart('PortShackleR', torusGeo(0.08, 0.02, 5, 8), wroughtIron, {
position: [portX, portBaseY + portHeight + 0.05, 0.6],
rotation: [0, 90, 0],
parent: root
});
// Inner heavy oak gate doors (swung OPEN against jamb walls, welcoming visitors across lowered drawbridge)
createPart('InnerGatePostL', boxGeo(0.2, 3.2, 0.2), oakWood, {
position: [-0.6, passageFloorY + 1.6, -1.1],
parent: root
});
createPart('InnerGatePostR', boxGeo(0.2, 3.2, 0.2), oakWood, {
position: [-0.6, passageFloorY + 1.6, 1.1],
parent: root
});
// Gate leaves folded open flat against passage walls
createPart('GateLeafL', boxGeo(0.95, 2.9, 0.1), oakWood, {
position: [-0.15, passageFloorY + 1.5, -1.05],
parent: root
});
createPart('GateLeafR', boxGeo(0.95, 2.9, 0.1), oakWood, {
position: [-0.15, passageFloorY + 1.5, 1.05],
parent: root
});
// Iron strap hinges on open gates
for (let gy = passageFloorY + 0.6; gy <= passageFloorY + 2.5; gy += 1.4) {
createPart('GateHingeL_' + gy.toFixed(1), boxGeo(0.8, 0.08, 0.12), wroughtIron, {
position: [-0.15, gy, -1.03],
parent: root
});
createPart('GateHingeR_' + gy.toFixed(1), boxGeo(0.8, 0.08, 0.12), wroughtIron, {
position: [-0.15, gy, 1.03],
parent: root
});
}
// ==========================================
// 5. UPPER GUARD ROOM WINDOWS (Gothic Tracery)
// ==========================================
const winY = passageFloorY + 5.6;
createPart('WinRecessF', boxGeo(0.3, 1.7, 1.2), stoneDark, {
position: [wallDepth / 2 - 0.05, winY, 0],
parent: root
});
createPart('WinSillF', boxGeo(0.45, 0.16, 1.4), stoneDressing, {
position: [wallDepth / 2 + 0.05, winY - 0.9, 0],
parent: root
});
createPart('WinMullionF', boxGeo(0.35, 1.5, 0.12), stoneDressing, {
position: [wallDepth / 2 + 0.02, winY - 0.05, 0],
parent: root
});
createPart('WinArchL_F', boxGeo(0.32, 0.12, 0.45), stoneDressing, {
position: [wallDepth / 2 + 0.02, winY + 0.72, -0.26],
rotation: [-25, 0, 0],
parent: root
});
createPart('WinArchR_F', boxGeo(0.32, 0.12, 0.45), stoneDressing, {
position: [wallDepth / 2 + 0.02, winY + 0.72, 0.26],
rotation: [25, 0, 0],
parent: root
});
// Rear central window
createPart('WinRecessB', boxGeo(0.3, 1.7, 1.2), stoneDark, {
position: [-wallDepth / 2 + 0.05, winY, 0],
parent: root
});
createPart('WinSillB', boxGeo(0.45, 0.16, 1.4), stoneDressing, {
position: [-wallDepth / 2 - 0.05, winY - 0.9, 0],
parent: root
});
createPart('WinMullionB', boxGeo(0.35, 1.5, 0.12), stoneDressing, {
position: [-wallDepth / 2 - 0.02, winY - 0.05, 0],
parent: root
});
// ==========================================
// 6. TWIN DRUM TOWERS (Mass, Batters, Storeys)
// ==========================================
[-towerZ, towerZ].forEach((tz, tIdx) => {
const side = tz < 0 ? 'L' : 'R';
const sideSign = tz < 0 ? -1 : 1;
// A. Battered plinth / talus foundation (Y = 0 to 2.2)
createPart('TowerPlinth_' + side, taperConeGeo(towerRadius + 0.65, towerRadius + 0.1, 2.2, 'y', 20), stoneDressing, {
position: [towerX, 1.1, tz],
parent: root
});
// Plinth drip mould
createPart('PlinthMould_' + side, cylinderGeo(towerRadius + 0.16, towerRadius + 0.16, 0.2, 20), stoneDressing, {
position: [towerX, 2.2, tz],
parent: root
});
// B. Lower Tower Drum (Stage 1: Y = 2.2 to 6.2)
createPart('TowerDrumLower_' + side, cylinderGeo(towerRadius, towerRadius, 4.0, 20), stoneAshlar, {
position: [towerX, 4.2, tz],
parent: root
});
// Mid-height string course belt
createPart('TowerBeltCourse_' + side, cylinderGeo(towerRadius + 0.14, towerRadius + 0.14, 0.28, 20), stoneDressing, {
position: [towerX, 6.2, tz],
parent: root
});
// C. Upper Tower Drum (Stage 2: Y = 6.2 to 11.2)
createPart('TowerDrumUpper_' + side, cylinderGeo(towerRadius, towerRadius, 5.0, 20), stoneAshlar, {
position: [towerX, 8.7, tz],
parent: root
});
// D. Cornice belt below machicolation corbels
createPart('TowerSubCorbelBelt_' + side, cylinderGeo(towerRadius + 0.08, towerRadius + 0.08, 0.25, 20), stoneDressing, {
position: [towerX, 11.2, tz],
parent: root
});
// E. Machicolations: Stepped corbel brackets around the drum tower
const numTowerCorbels = 14;
const corbelBaseY = 11.35;
for (let c = 0; c < numTowerCorbels; c++) {
const angle = (c / numTowerCorbels) * Math.PI * 2;
const cosA = Math.cos(angle);
const sinA = Math.sin(angle);
const rotY = -(angle * 180) / Math.PI;
// Triple-stepped stone corbel bracket
const r1 = towerRadius + 0.12;
createPart('Corbel1_' + side + '_' + c, boxGeo(0.24, 0.24, 0.22), stoneDressing, {
position: [towerX + cosA * r1, corbelBaseY + 0.12, tz + sinA * r1],
rotation: [0, rotY, 0],
parent: root
});
const r2 = towerRadius + 0.26;
createPart('Corbel2_' + side + '_' + c, boxGeo(0.28, 0.24, 0.24), stoneDressing, {
position: [towerX + cosA * r2, corbelBaseY + 0.36, tz + sinA * r2],
rotation: [0, rotY, 0],
parent: root
});
const r3 = towerRadius + 0.42;
createPart('Corbel3_' + side + '_' + c, boxGeo(0.34, 0.26, 0.26), stoneDressing, {
position: [towerX + cosA * r3, corbelBaseY + 0.61, tz + sinA * r3],
rotation: [0, rotY, 0],
parent: root
});
// Recessed murder hole chute between corbels
const midAngle = angle + (Math.PI / numTowerCorbels);
const midCos = Math.cos(midAngle);
const midSin = Math.sin(midAngle);
createPart('ChuteHole_' + side + '_' + c, boxGeo(0.2, 0.08, 0.2), stoneDark, {
position: [towerX + midCos * (towerRadius + 0.35), corbelBaseY + 0.72, tz + midSin * (towerRadius + 0.35)],
rotation: [0, -(midAngle * 180) / Math.PI, 0],
parent: root
});
}
// Projecting parapet floor ring supported by corbels
createPart('TowerGalleryRing_' + side, cylinderGeo(towerRadius + 0.58, towerRadius + 0.58, 0.32, 20), stoneDressing, {
position: [towerX, 12.18, tz],
parent: root
});
// Gallery wall walk floor
createPart('TowerWalkFloor_' + side, cylinderGeo(towerRadius + 0.35, towerRadius + 0.35, 0.05, 20), stoneAshlar, {
position: [towerX, 12.35, tz],
parent: root
});
// F. Crenellated Parapet (Merlons and Coping Caps)
const numMerlons = 10;
const parapetY = 12.35;
for (let m = 0; m < numMerlons; m++) {
const angle = (m / numMerlons) * Math.PI * 2;
const cosA = Math.cos(angle);
const sinA = Math.sin(angle);
const rotY = -(angle * 180) / Math.PI;
const mRad = towerRadius + 0.48;
createPart('Merlon_' + side + '_' + m, boxGeo(0.32, 1.05, 0.85), stoneAshlar, {
position: [towerX + cosA * mRad, parapetY + 0.52, tz + sinA * mRad],
rotation: [0, rotY, 0],
parent: root
});
createPart('MerlonCap_' + side + '_' + m, boxGeo(0.38, 0.14, 0.9), stoneDressing, {
position: [towerX + cosA * mRad, parapetY + 1.11, tz + sinA * mRad],
rotation: [0, rotY, 0],
parent: root
});
if (cosA > -0.2) {
createPart('MerlonSlit_' + side + '_' + m, boxGeo(0.34, 0.55, 0.08), stoneDark, {
position: [towerX + cosA * mRad, parapetY + 0.55, tz + sinA * mRad],
rotation: [0, rotY, 0],
parent: root
});
}
}
// Doorway from wall-walk into tower chamber
createPart('TowerDoorway_' + side, boxGeo(0.3, 1.8, 0.75), stoneDark, {
position: [towerX - 0.8, wallHeight + 1.0, tz - sideSign * (towerRadius - 0.4)],
rotation: [0, sideSign * 45, 0],
parent: root
});
// G. Arrow Loops in Tower Faces (3 Storeys of authentic cruciform balistraria)
// Tier 1
const loopAnglesTier1 = [10, 45, -25];
loopAnglesTier1.forEach((deg, lIdx) => {
const rad = (deg * Math.PI) / 180;
const cosA = Math.cos(rad);
const sinA = Math.sin(rad) * sideSign;
const lx = towerX + cosA * (towerRadius + 0.02);
const lz = tz + sinA * (towerRadius + 0.02);
const rotY = -deg * sideSign;
const ly = 3.6;
createPart('LoopSurround1_' + side + '_' + lIdx, boxGeo(0.12, 1.5, 0.45), stoneDressing, {
position: [lx, ly, lz],
rotation: [0, rotY, 0],
parent: root
});
createPart('LoopV1_' + side + '_' + lIdx, boxGeo(0.14, 1.25, 0.09), stoneDark, {
position: [lx + 0.01 * cosA, ly, lz + 0.01 * sinA],
rotation: [0, rotY, 0],
parent: root
});
createPart('LoopH1_' + side + '_' + lIdx, boxGeo(0.14, 0.09, 0.32), stoneDark, {
position: [lx + 0.01 * cosA, ly + 0.2, lz + 0.01 * sinA],
rotation: [0, rotY, 0],
parent: root
});
});
// Tier 2
const loopAnglesTier2 = [0, 35, 70, -35];
loopAnglesTier2.forEach((deg, lIdx) => {
const rad = (deg * Math.PI) / 180;
const cosA = Math.cos(rad);
const sinA = Math.sin(rad) * sideSign;
const lx = towerX + cosA * (towerRadius + 0.02);
const lz = tz + sinA * (towerRadius + 0.02);
const rotY = -deg * sideSign;
const ly = 8.0;
createPart('LoopSurround2_' + side + '_' + lIdx, boxGeo(0.12, 1.5, 0.45), stoneDressing, {
position: [lx, ly, lz],
rotation: [0, rotY, 0],
parent: root
});
createPart('LoopV2_' + side + '_' + lIdx, boxGeo(0.14, 1.25, 0.09), stoneDark, {
position: [lx + 0.01 * cosA, ly, lz + 0.01 * sinA],
rotation: [0, rotY, 0],
parent: root
});
createPart('LoopH2_' + side + '_' + lIdx, boxGeo(0.14, 0.09, 0.32), stoneDark, {
position: [lx + 0.01 * cosA, ly + 0.2, lz + 0.01 * sinA],
rotation: [0, rotY, 0],
parent: root
});
});
// Tier 3
const loopAnglesTier3 = [15, 55, -20];
loopAnglesTier3.forEach((deg, lIdx) => {
const rad = (deg * Math.PI) / 180;
const cosA = Math.cos(rad);
const sinA = Math.sin(rad) * sideSign;
const lx = towerX + cosA * (towerRadius + 0.02);
const lz = tz + sinA * (towerRadius + 0.02);
const rotY = -deg * sideSign;
const ly = 10.3;
createPart('LoopV3_' + side + '_' + lIdx, boxGeo(0.12, 1.1, 0.08), stoneDark, {
position: [lx, ly, lz],
rotation: [0, rotY, 0],
parent: root
});
});
});
// Watch turret / Cap-house on Right Tower
createPart('TurretDrum', cylinderGeo(1.0, 1.0, 2.2, 16), stoneAshlar, {
position: [towerX + 0.6, towerHeight + 0.7, towerZ + 0.6],
parent: root
});
createPart('TurretConicalRoof', coneGeo(1.2, 1.4, 16), stoneDressing, {
position: [towerX + 0.6, towerHeight + 2.4, towerZ + 0.6],
parent: root
});
createPart('TurretFinial', cylinderGeo(0.04, 0.04, 0.6, 6), wroughtIron, {
position: [towerX + 0.6, towerHeight + 3.3, towerZ + 0.6],
parent: root
});
// ==========================================
// 7. CENTRAL MACHICOLATIONS & PARAPET
// ==========================================
const centCorbelY = wallHeight;
// Corbel table across front and rear central walls
const corbelZPositions = [-1.8, -1.2, -0.6, 0.0, 0.6, 1.2, 1.8];
corbelZPositions.forEach((cz, cIdx) => {
// Front corbel brackets
createPart('CentCorbel1_F_' + cIdx, boxGeo(0.24, 0.22, 0.22), stoneDressing, {
position: [wallDepth / 2 + 0.12, centCorbelY + 0.11, cz],
parent: root
});
createPart('CentCorbel2_F_' + cIdx, boxGeo(0.38, 0.22, 0.25), stoneDressing, {
position: [wallDepth / 2 + 0.25, centCorbelY + 0.33, cz],
parent: root
});
createPart('CentCorbel3_F_' + cIdx, boxGeo(0.52, 0.24, 0.28), stoneDressing, {
position: [wallDepth / 2 + 0.38, centCorbelY + 0.56, cz],
parent: root
});
// Rear corbel brackets
createPart('CentCorbel1_B_' + cIdx, boxGeo(0.24, 0.22, 0.22), stoneDressing, {
position: [-wallDepth / 2 - 0.12, centCorbelY + 0.11, cz],
parent: root
});
createPart('CentCorbel2_B_' + cIdx, boxGeo(0.38, 0.22, 0.25), stoneDressing, {
position: [-wallDepth / 2 - 0.25, centCorbelY + 0.33, cz],
parent: root
});
createPart('CentCorbel3_B_' + cIdx, boxGeo(0.52, 0.24, 0.28), stoneDressing, {
position: [-wallDepth / 2 - 0.38, centCorbelY + 0.56, cz],
parent: root
});
// Murder hole slots between front and rear corbels
if (cIdx < corbelZPositions.length - 1) {
const midZ = (cz + corbelZPositions[cIdx + 1]) / 2;
createPart('CentChute_F_' + cIdx, boxGeo(0.25, 0.08, 0.25), stoneDark, {
position: [wallDepth / 2 + 0.38, centCorbelY + 0.64, midZ],
parent: root
});
createPart('CentChute_B_' + cIdx, boxGeo(0.25, 0.08, 0.25), stoneDark, {
position: [-wallDepth / 2 - 0.38, centCorbelY + 0.64, midZ],
parent: root
});
}
});
// Projecting gallery floor slab
createPart('CentGalleryFloor', boxGeo(wallDepth + 1.2, 0.28, wallWidth + 0.2), stoneDressing, {
position: [0, centCorbelY + 0.76, 0],
parent: root
});
// Central roof / wall-walk floor
createPart('CentWalkFloor', boxGeo(wallDepth + 0.6, 0.06, wallWidth), stoneAshlar, {
position: [0, centCorbelY + 0.92, 0],
parent: root
});
// Central Merlons & Crenellations (Front and Rear)
const centMerlonZ = [-1.8, -0.9, 0.0, 0.9, 1.8];
centMerlonZ.forEach((mz, mIdx) => {
// Front merlons
createPart('CentMerlon_F_' + mIdx, boxGeo(0.3, 0.95, 0.54), stoneAshlar, {
position: [wallDepth / 2 + 0.52, centCorbelY + 1.4, mz],
parent: root
});
createPart('CentMerlonCap_F_' + mIdx, boxGeo(0.36, 0.12, 0.58), stoneDressing, {
position: [wallDepth / 2 + 0.52, centCorbelY + 1.93, mz],
parent: root
});
createPart('CentMerlonSlit_F_' + mIdx, boxGeo(0.32, 0.5, 0.07), stoneDark, {
position: [wallDepth / 2 + 0.52, centCorbelY + 1.4, mz],
parent: root
});
// Rear merlons
createPart('CentMerlon_B_' + mIdx, boxGeo(0.3, 0.95, 0.54), stoneAshlar, {
position: [-wallDepth / 2 - 0.52, centCorbelY + 1.4, mz],
parent: root
});
createPart('CentMerlonCap_B_' + mIdx, boxGeo(0.36, 0.12, 0.58), stoneDressing, {
position: [-wallDepth / 2 - 0.52, centCorbelY + 1.93, mz],
parent: root
});
});
// Low crenel sills (leaves deep, clear embrasure gaps for archers)
for (let e = 0; e < centMerlonZ.length - 1; e++) {
const ez = (centMerlonZ[e] + centMerlonZ[e + 1]) / 2;
createPart('CentCrenelSill_F_' + e, boxGeo(0.3, 0.12, 0.36), stoneDressing, {
position: [wallDepth / 2 + 0.52, centCorbelY + 0.98, ez],
parent: root
});
createPart('CentCrenelSill_B_' + e, boxGeo(0.3, 0.12, 0.36), stoneDressing, {
position: [-wallDepth / 2 - 0.52, centCorbelY + 0.98, ez],
parent: root
});
}
// ==========================================
// 8. TIMBER DRAWBRIDGE & STONE ABUTMENT
// ==========================================
const bridgeStartX = wallDepth / 2 - 0.3; // X = 1.9 (gate threshold)
const bridgeLength = 4.4;
const bridgeEndX = bridgeStartX + bridgeLength; // X = 6.3
const bridgeWidth = 2.4;
const bridgeThick = 0.22;
const deckY = passageFloorY + 0.12;
// A. Stone Abutment (Pier across the moat)
const abutmentLength = 3.6;
const abutmentCenterX = bridgeEndX + abutmentLength / 2; // X = 8.1
// Abutment battered foundation & body (sitting on Y = 0)
createPart('AbutmentPlinth', taperConeGeo(bridgeWidth / 2 + 0.8, bridgeWidth / 2 + 0.5, deckY, 'y', 16), stoneDressing, {
position: [abutmentCenterX, deckY / 2, 0],
parent: root
});
createPart('AbutmentBody', boxGeo(abutmentLength, deckY, bridgeWidth + 0.8), stoneAshlar, {
position: [abutmentCenterX, deckY / 2, 0],
parent: root
});
// Abutment forward cutwater buttress
createPart('AbutmentButtress', boxGeo(0.6, deckY, 1.4), stoneDressing, {
position: [bridgeEndX + abutmentLength + 0.2, deckY / 2, 0],
parent: root
});
// Abutment road surface
createPart('AbutmentRoad', boxGeo(abutmentLength + 0.1, 0.08, bridgeWidth + 0.6), stoneDressing, {
position: [abutmentCenterX, deckY + 0.04, 0],
parent: root
});
// Timber landing curb/bumper where the lowered drawbridge rests
createPart('AbutmentBumper', boxGeo(0.24, 0.16, bridgeWidth + 0.2), oakWood, {
position: [bridgeEndX + 0.12, deckY + 0.08, 0],
parent: root
});
// Abutment stone parapets with coping
createPart('AbutmentParapetL', boxGeo(abutmentLength - 0.4, 0.65, 0.35), stoneAshlar, {
position: [abutmentCenterX + 0.2, deckY + 0.38, -bridgeWidth / 2 - 0.3],
parent: root
});
createPart('AbutmentParapetR', boxGeo(abutmentLength - 0.4, 0.65, 0.35), stoneAshlar, {
position: [abutmentCenterX + 0.2, deckY + 0.38, bridgeWidth / 2 + 0.3],
parent: root
});
createPart('AbutmentParapetCapL', boxGeo(abutmentLength - 0.3, 0.1, 0.42), stoneDressing, {
position: [abutmentCenterX + 0.2, deckY + 0.74, -bridgeWidth / 2 - 0.3],
parent: root
});
createPart('AbutmentParapetCapR', boxGeo(abutmentLength - 0.3, 0.1, 0.42), stoneDressing, {
position: [abutmentCenterX + 0.2, deckY + 0.74, bridgeWidth / 2 + 0.3],
parent: root
});
// B. Timber Drawbridge Structure (Lowered)
const bridgeCenterX = (bridgeStartX + bridgeEndX) / 2;
const numPlanks = 6;
const plankW = (bridgeWidth - 0.1) / numPlanks;
for (let p = 0; p < numPlanks; p++) {
const pz = -bridgeWidth / 2 + plankW / 2 + 0.05 + p * plankW;
createPart('BridgePlank_' + p, boxGeo(bridgeLength - 0.1, bridgeThick, plankW - 0.02), oakWood, {
position: [bridgeCenterX, deckY, pz],
parent: root
});
}
// Transverse wood cleats (traction bars every 0.6m)
const numCleats = 7;
for (let c = 1; c < numCleats; c++) {
const cx = bridgeStartX + (c / numCleats) * bridgeLength;
createPart('DeckCleat_' + c, boxGeo(0.1, 0.06, bridgeWidth - 0.12), oakWood, {
position: [cx, deckY + bridgeThick / 2 + 0.03, 0],
parent: root
});
createPart('CleatSpikeL_' + c, boxGeo(0.04, 0.02, 0.04), wroughtIron, {
position: [cx, deckY + bridgeThick / 2 + 0.065, -0.9],
parent: root
});
createPart('CleatSpikeR_' + c, boxGeo(0.04, 0.02, 0.04), wroughtIron, {
position: [cx, deckY + bridgeThick / 2 + 0.065, 0.9],
parent: root
});
}
// Heavy wrought-iron edge binding straps
createPart('BridgeStrapL', boxGeo(bridgeLength, 0.04, 0.08), wroughtIron, {
position: [bridgeCenterX, deckY + bridgeThick / 2 + 0.02, -bridgeWidth / 2 + 0.05],
parent: root
});
createPart('BridgeStrapR', boxGeo(bridgeLength, 0.04, 0.08), wroughtIron, {
position: [bridgeCenterX, deckY + bridgeThick / 2 + 0.02, bridgeWidth / 2 - 0.05],
parent: root
});
// Heavy hinge pintles and barrels at gate threshold
createPart('HingeBarrelL', cylinderZGeo(0.1, 0.1, 0.32, 10), wroughtIron, {
position: [bridgeStartX + 0.08, deckY, -1.05],
parent: root
});
createPart('HingeBarrelR', cylinderZGeo(0.1, 0.1, 0.32, 10), wroughtIron, {
position: [bridgeStartX + 0.08, deckY, 1.05],
parent: root
});
createPart('HingeBracketL', boxGeo(0.4, 0.12, 0.1), wroughtIron, {
position: [bridgeStartX - 0.05, deckY, -1.05],
parent: root
});
createPart('HingeBracketR', boxGeo(0.4, 0.12, 0.1), wroughtIron, {
position: [bridgeStartX - 0.05, deckY, 1.05],
parent: root
});
// Heavy lifting eyes / ring brackets at outer nose of drawbridge
const eyeX = bridgeEndX - 0.15;
const eyeZ = 1.05;
createPart('LiftingEyePlateL', boxGeo(0.3, 0.06, 0.18), wroughtIron, {
position: [eyeX, deckY + bridgeThick / 2 + 0.03, -eyeZ],
parent: root
});
createPart('LiftingEyePlateR', boxGeo(0.3, 0.06, 0.18), wroughtIron, {
position: [eyeX, deckY + bridgeThick / 2 + 0.03, eyeZ],
parent: root
});
createPart('LiftingRingL', torusGeo(0.1, 0.026, 6, 10), wroughtIron, {
position: [eyeX, deckY + bridgeThick / 2 + 0.14, -eyeZ],
rotation: [0, 90, 45],
parent: root
});
createPart('LiftingRingR', torusGeo(0.1, 0.026, 6, 10), wroughtIron, {
position: [eyeX, deckY + bridgeThick / 2 + 0.14, eyeZ],
rotation: [0, 90, -45],
parent: root
});
// ==========================================
// 9. WINCH HOUSINGS & BALANCED CHAINS
// ==========================================
const winchY = passageFloorY + 5.2; // Y = 5.9
const winchX = wallDepth / 2 + 0.22; // X = 2.42
const winchZ = 1.05;
[-winchZ, winchZ].forEach((wz, wIdx) => {
const wSide = wz < 0 ? 'L' : 'R';
// Stone housing block
createPart('WinchHousing_' + wSide, boxGeo(0.65, 0.85, 0.55), stoneAshlar, {
position: [winchX, winchY, wz],
parent: root
});
// Corbel support beneath housing
createPart('WinchCorbel_' + wSide, boxGeo(0.5, 0.35, 0.45), stoneDressing, {
position: [winchX - 0.05, winchY - 0.55, wz],
parent: root
});
// Sloped weather coping on housing top
createPart('WinchCoping_' + wSide, boxGeo(0.72, 0.16, 0.62), stoneDressing, {
position: [winchX, winchY + 0.48, wz],
parent: root
});
// Dark chain aperture slit into wall
createPart('WinchAperture_' + wSide, boxGeo(0.2, 0.36, 0.16), stoneDark, {
position: [winchX + 0.26, winchY - 0.05, wz],
parent: root
});
// Iron guide roller
createPart('WinchRoller_' + wSide, cylinderZGeo(0.08, 0.08, 0.2, 8), wroughtIron, {
position: [winchX + 0.22, winchY - 0.05, wz],
parent: root
});
});
// Wrought Iron Chains running from winch housings down to drawbridge lifting rings
// Budget-optimized interlocking torus links (5x8 = 80 tris per link, 14 links = 1120 tris per chain)
[-winchZ, winchZ].forEach((wz, cIdx) => {
const cSide = wz < 0 ? 'L' : 'R';
const startPt = [winchX + 0.28, winchY - 0.05, wz];
const endPt = [eyeX, deckY + bridgeThick / 2 + 0.16, wz];
const dx = endPt[0] - startPt[0];
const dy = endPt[1] - startPt[1];
const dz = endPt[2] - startPt[2];
const pitchDeg = Math.atan2(dy, Math.hypot(dx, dz)) * (180 / Math.PI);
const yawDeg = -Math.atan2(dz, dx) * (180 / Math.PI);
// A chain is interlocking links, not beads on a string, and the difference
// is arithmetic. A torus link is 2 * (R + tube) long, so consecutive centres
// have to sit closer together than that or the rings stop touching. This run
// is 5.88 m from winch to lifting ring and it was being divided into a fixed
// 13 steps, which put the centres 0.20 m apart and the links only 0.194 m
// long: 24 separated rings, a floating-part warning on every one of them,
// and a chain that read as a dotted line.
//
// So the pitch is chosen first, at a link size that suits a drawbridge, and
// the count follows from the span. Keeping tube at 0.29 R makes each link
// 1.29 pitches long, which is 0.29 of a pitch of overlap at every joint --
// enough to read as engaged from any angle. The tessellation drops to 4 x 6
// because there are now thirty links a side and none of them is ever more
// than a few pixels across.
const pitch = 0.193;
const linkR = pitch / 2;
const linkTube = linkR * 0.29;
const span = Math.hypot(dx, dy, dz);
const numLinks = Math.max(2, Math.round(span / pitch));
for (let l = 0; l <= numLinks; l++) {
const t = l / numLinks;
const sag = Math.sin(t * Math.PI) * 0.08;
const lx = startPt[0] + dx * t;
const ly = startPt[1] + dy * t - sag;
const lz = startPt[2] + dz * t;
const rollDeg = (l % 2 === 0) ? 0 : 90;
createPart('ChainLink_' + cSide + '_' + l, torusGeo(linkR, linkTube, 4, 6), wroughtIron, {
position: [lx, ly, lz],
rotation: [rollDeg, yawDeg, pitchDeg],
parent: root
});
}
// Terminal iron shackle / clevis at drawbridge ring
createPart('ChainShackle_' + cSide, boxGeo(0.12, 0.08, 0.08), wroughtIron, {
position: [endPt[0] - 0.08, endPt[1] + 0.04, wz],
parent: root
});
});
return root;
}Scroll code horizontally
Brief and retained revisions
No brief or earlier revisions are recorded. The source download contains the version built for this page.