Fighter jet
Lofted fuselage and tapered wing sections
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
- 17,532
- Estimated draws
- 299
- Materials
- 21
- Textures
- 0
- Animation clips
- 0
- Bounds X × Y × Z
- 16.54 × 4.7 × 11.15 m
- Build warnings
- 0
Measurements come from this build.
Download this build
Runtime: 351,684 bytes. Original: 351,460 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
- e7b5a1e677250bd3823c823459a1b10652cf9e8388aacaf5c1368c21f917118b
- Original GLB
- d48dd684d35e18a7319259f9e5519d933ffd0b4c549b7e447ef74f0f98eb031c
- Source
- 08813f64ca19872516d3ecaf5c10e0175cd3e8608289137b2e19d9dfe2f8493e
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
Claude Opus 5 through Claude Code
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
- Not recorded
- Authoring review
- 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: opus, via claude.
//
// Written by the model itself through the Kiln MCP tools, and cut off
// mid-run rather than finished -- by a provider limit, or by the
// dispatch deadline. The program below is what was on disk when the
// session ended; how many times it had looked at its own contact sheet
// by then is not recorded, so this one does not make the claim the
// others do.
//
// Dispatched into a clean directory containing only the brief and the Kiln
// skills, with no access to this repository or to any finished example.
//
// Refined later, in this repository, with `kiln_edit`: the wing was split at
// its own hinge lines. It had been lofted full chord underneath the flap, the
// flaperon and the aileron, which are lofted from the same stations over the
// same aerofoil, so both wings carried a hatched wedge from top where two
// identical surfaces sat at one depth. The skin was repainted later still;
// the comment at the material says why.
const meta = { name: 'FighterJet', category: 'prop', role: 'vehicle' };
// ---------------------------------------------------------------------------
// Frame: +X forward (nose), +Y up, +Z asset right. Gear down, tyres on Y = 0.
// Overall: 16.5 m nose to tail, 11.0 m span, 4.6 m to the fin tips.
// ---------------------------------------------------------------------------
const D2R = Math.PI / 180;
function lerp(a, b, t) { return a + (b - a) * t; }
// Generic skinned loft. `sections` is an array of equal-length rings of
// [x,y,z] points, ordered consistently around the loop. Both ends are capped
// with a centroid fan, then the winding is normalised by signed volume so the
// surface always faces outward (or inward when opts.invert is set).
function loftGeo(sections, opts) {
opts = opts || {};
const M = sections.length;
const N = sections[0].length;
const verts = [];
for (let s = 0; s < M; s++) {
const sec = sections[s];
for (let i = 0; i < N; i++) verts.push(sec[i][0], sec[i][1], sec[i][2]);
}
const idx = [];
for (let s = 0; s < M - 1; s++) {
for (let i = 0; i < N; i++) {
const j = (i + 1) % N;
const a = s * N + i, b = s * N + j, c = (s + 1) * N + i, d = (s + 1) * N + j;
idx.push(a, c, d);
idx.push(a, d, b);
}
}
if (opts.capStart !== false) {
let cx = 0, cy = 0, cz = 0;
for (let i = 0; i < N; i++) { cx += sections[0][i][0]; cy += sections[0][i][1]; cz += sections[0][i][2]; }
const ci = verts.length / 3;
verts.push(cx / N, cy / N, cz / N);
for (let i = 0; i < N; i++) { const j = (i + 1) % N; idx.push(ci, j, i); }
}
if (opts.capEnd !== false) {
const base = (M - 1) * N;
let cx = 0, cy = 0, cz = 0;
for (let i = 0; i < N; i++) { cx += sections[M - 1][i][0]; cy += sections[M - 1][i][1]; cz += sections[M - 1][i][2]; }
const ci = verts.length / 3;
verts.push(cx / N, cy / N, cz / N);
for (let i = 0; i < N; i++) { const j = (i + 1) % N; idx.push(ci, base + i, base + j); }
}
let vol = 0;
for (let t = 0; t < idx.length; t += 3) {
const i0 = idx[t] * 3, i1 = idx[t + 1] * 3, i2 = idx[t + 2] * 3;
const ax = verts[i0], ay = verts[i0 + 1], az = verts[i0 + 2];
const bx = verts[i1], by = verts[i1 + 1], bz = verts[i1 + 2];
const cx2 = verts[i2], cy2 = verts[i2 + 1], cz2 = verts[i2 + 2];
vol += ax * (by * cz2 - bz * cy2) + ay * (bz * cx2 - bx * cz2) + az * (bx * cy2 - by * cx2);
}
const wantPositive = !opts.invert;
if ((vol > 0) !== wantPositive) {
for (let t = 0; t < idx.length; t += 3) { const tmp = idx[t + 1]; idx[t + 1] = idx[t + 2]; idx[t + 2] = tmp; }
}
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.Float32BufferAttribute(verts, 3));
geo.setIndex(idx);
geo.computeVertexNormals();
return geo;
}
// Surface of revolution about a cardinal axis from a CLOSED 2D outline of
// [axial, radial] pairs -> watertight tube/solid. Used for nozzles and tyres.
function revolveClosed(profile, segments, axis) {
const sections = [];
for (let s = 0; s < segments; s++) {
const a = (s / segments) * Math.PI * 2;
const ca = Math.cos(a), sa = Math.sin(a);
const ring = [];
for (let i = 0; i < profile.length; i++) {
const p = profile[i][0], r = profile[i][1];
if (axis === 'x') ring.push([p, r * ca, r * sa]);
else if (axis === 'z') ring.push([r * ca, r * sa, p]);
else ring.push([r * ca, p, r * sa]);
}
sections.push(ring);
}
// wrap around: treat as closed in the sweep direction too
const M = sections.length, N = profile.length;
const verts = [];
for (let s = 0; s < M; s++) for (let i = 0; i < N; i++) verts.push(sections[s][i][0], sections[s][i][1], sections[s][i][2]);
const idx = [];
for (let s = 0; s < M; s++) {
const s2 = (s + 1) % M;
for (let i = 0; i < N; i++) {
const j = (i + 1) % N;
const a = s * N + i, b = s * N + j, c = s2 * N + i, d = s2 * N + j;
idx.push(a, c, d);
idx.push(a, d, b);
}
}
let vol = 0;
for (let t = 0; t < idx.length; t += 3) {
const i0 = idx[t] * 3, i1 = idx[t + 1] * 3, i2 = idx[t + 2] * 3;
const ax = verts[i0], ay = verts[i0 + 1], az = verts[i0 + 2];
const bx = verts[i1], by = verts[i1 + 1], bz = verts[i1 + 2];
const cx = verts[i2], cy = verts[i2 + 1], cz = verts[i2 + 2];
vol += ax * (by * cz - bz * cy) + ay * (bz * cx - bx * cz) + az * (bx * cy - by * cx);
}
if (vol < 0) for (let t = 0; t < idx.length; t += 3) { const tmp = idx[t + 1]; idx[t + 1] = idx[t + 2]; idx[t + 2] = tmp; }
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.Float32BufferAttribute(verts, 3));
geo.setIndex(idx);
geo.computeVertexNormals();
return geo;
}
// NACA symmetric half-thickness shape function, normalised so the peak is ~0.5.
function foilF(u) {
const t = Math.max(0, Math.min(1, u));
return 5 * (0.2969 * Math.sqrt(t) - 0.1260 * t - 0.3516 * t * t + 0.2843 * t * t * t - 0.1036 * t * t * t * t);
}
// Lofts a chordwise SLICE (u0..u1 of chord) of an aerofoil surface across a
// list of span stations. `mapper(x, station, offset)` puts a point in world space.
function foilLoft(stations, u0, u1, n, mapper) {
const sections = [];
const us = [];
for (let i = 0; i <= n; i++) { const p = (1 - Math.cos(Math.PI * i / n)) / 2; us.push(u0 + (u1 - u0) * p); }
for (let s = 0; s < stations.length; s++) {
const st = stations[s];
const chord = st.le - st.te;
const loop = [];
for (let i = 0; i <= n; i++) {
const u = us[i];
loop.push(mapper(st.le - u * chord, st, foilF(u) * st.t * chord));
}
for (let i = n; i >= 0; i--) {
const u = us[i];
loop.push(mapper(st.le - u * chord, st, -foilF(u) * st.t * chord));
}
sections.push(loop);
}
return loftGeo(sections, {});
}
// Chined fuselage cross-section: superellipse above and below a hard chine
// edge that carries a small vertical facet so it stays crisp when smoothed.
function fuseRing(st, nTop, nBot) {
const pts = [];
const eps = st.chine;
for (let i = 0; i <= nTop; i++) {
const t = (i / nTop) * Math.PI;
const c = Math.cos(t), s = Math.sin(t);
const z = st.w * Math.sign(c) * Math.pow(Math.abs(c), 2 / st.eTop);
const y = st.cy + eps + st.hu * Math.pow(Math.abs(s), 2 / st.eTop);
pts.push([st.x, y, z]);
}
for (let i = 0; i <= nBot; i++) {
const t = Math.PI + (i / nBot) * Math.PI;
const c = Math.cos(t), s = Math.sin(t);
const z = st.w * Math.sign(c) * Math.pow(Math.abs(c), 2 / st.eBot);
const y = st.cy - eps - st.hl * Math.pow(Math.abs(s), 2 / st.eBot);
pts.push([st.x, y, z]);
}
return pts;
}
function roundRect(w, h, r, n) {
// n points around a rounded rectangle in (z,y), centred on origin
const pts = [];
const cx = [w - r, w - r, -(w - r), -(w - r)];
const cy = [h - r, -(h - r), -(h - r), h - r];
const a0 = [0, -90, 180, 90];
const per = Math.max(2, Math.round(n / 4));
for (let q = 0; q < 4; q++) {
for (let i = 0; i < per; i++) {
const a = (a0[q] + (q === 0 ? 0 : 0) + (i / per) * 90 + (q === 0 ? 0 : 0)) * D2R;
const ang = (a0[q] - (i / per) * 90) * D2R;
pts.push([cx[q] + r * Math.cos(ang), cy[q] + r * Math.sin(ang)]);
}
}
return pts;
}
async function build() {
const root = createRoot('FighterJet');
// ------------------------------------------------------------ materials
// Low-visibility tactical paint, which is what an airframe is finished in --
// it is not bare metal, and metalness 0.32 on a light grey was enough to make
// it act like some. Under the studio dome the whole aircraft came back a
// uniform near-white: not one panel line, not one shadow across the fuselage
// spine, and no difference at all between the two tones the scheme is built
// from. Paint is dielectric, so the metalness goes to almost nothing, and the
// two greys move apart far enough that the upper surfaces read as a scheme.
const skin = gameMaterial(0x5c646f, { metalness: 0.06, roughness: 0.62, flatShading: false });
const skinDark = gameMaterial(0x424951, { metalness: 0.06, roughness: 0.66, flatShading: false });
const radomeMat = gameMaterial(0x4e545b, { metalness: 0.10, roughness: 0.80, flatShading: false });
const nozzleMat = gameMaterial(0xa89e90, { metalness: 0.92, roughness: 0.38, flatShading: false });
const burnt = gameMaterial(0x4b433a, { metalness: 0.85, roughness: 0.62, flatShading: false });
const black = gameMaterial(0x1b1e21, { roughness: 0.88, metalness: 0.15 });
const rubber = gameMaterial(0x131416, { roughness: 0.98, metalness: 0.02 });
const steel = gameMaterial(0xc9ced3, { metalness: 0.95, roughness: 0.18 });
const duct = gameMaterial(0x8f949a, { metalness: 0.10, roughness: 0.55, flatShading: false });
const glass = glassMaterial(0x8fb8cc, { opacity: 0.34, roughness: 0.06, metalness: 0.35 });
const lightR = gameMaterial(0xd21f2a, { emissive: 0xd21f2a, emissiveIntensity: 0.9, roughness: 0.4 });
const lightG = gameMaterial(0x21c04a, { emissive: 0x21c04a, emissiveIntensity: 0.9, roughness: 0.4 });
const lightW = gameMaterial(0xf2f4f0, { emissive: 0xf2f4f0, emissiveIntensity: 0.8, roughness: 0.4 });
// ------------------------------------------------------------- fuselage
// x, half-width, chine height, upper height, lower depth, superellipse exps
const fuseTable = [
[8.62, 0.030, 2.000, 0.030, 0.030, 2.4, 2.4],
[8.45, 0.140, 2.000, 0.125, 0.120, 2.5, 2.4],
[8.10, 0.320, 2.000, 0.250, 0.240, 2.6, 2.4],
[7.40, 0.500, 2.000, 0.380, 0.360, 2.7, 2.5],
[6.60, 0.660, 2.000, 0.480, 0.460, 2.8, 2.5],
[5.80, 0.780, 2.000, 0.540, 0.540, 2.9, 2.5],
[5.00, 0.860, 2.000, 0.575, 0.600, 3.0, 2.6],
[4.20, 0.920, 2.000, 0.600, 0.660, 3.0, 2.6],
[3.40, 0.980, 2.000, 0.620, 0.720, 3.0, 2.7],
[2.60, 1.040, 2.000, 0.640, 0.780, 3.0, 2.7],
[1.80, 1.100, 1.990, 0.660, 0.820, 3.0, 2.8],
[1.00, 1.160, 1.980, 0.680, 0.840, 3.0, 2.8],
[0.10, 1.220, 1.970, 0.700, 0.860, 3.0, 2.8],
[-0.80, 1.250, 1.960, 0.700, 0.860, 3.0, 2.8],
[-1.70, 1.250, 1.955, 0.685, 0.845, 3.0, 2.8],
[-2.60, 1.235, 1.950, 0.665, 0.825, 2.9, 2.8],
[-3.50, 1.205, 1.950, 0.625, 0.785, 2.9, 2.7],
[-4.40, 1.165, 1.950, 0.585, 0.745, 2.8, 2.7],
[-5.30, 1.125, 1.950, 0.545, 0.690, 2.8, 2.6],
[-6.20, 1.075, 1.950, 0.500, 0.625, 2.7, 2.6],
[-6.70, 1.040, 1.950, 0.475, 0.585, 2.7, 2.5],
[-7.05, 1.010, 1.950, 0.460, 0.560, 2.6, 2.5],
];
const fuseSections = fuseTable.map(function (r) {
return fuseRing({ x: r[0], w: r[1], cy: r[2], hu: r[3], hl: r[4], eTop: r[5], eBot: r[6], chine: 0.018 }, 11, 11);
});
createPart('Fuselage', loftGeo(fuseSections, {}), skin, { parent: root });
// Radome: a darker dielectric cap over the forward 2 m of the nose.
const radomeSections = fuseTable.filter(function (r) { return r[0] >= 6.55; }).map(function (r) {
return fuseRing({ x: r[0], w: r[1] * 1.006, cy: r[2], hu: r[3] * 1.006, hl: r[4] * 1.006, eTop: r[5], eBot: r[6], chine: 0.019 }, 11, 11);
});
createPart('Radome', loftGeo(radomeSections, {}), radomeMat, { parent: root });
// Pitot boom on the radome tip: nose datum is X = 9.00.
createPart('PitotBoom', cylinderXGeo(0.016, 0.030, 0.40, 8), steel, { position: [8.80, 2.000, 0], parent: root });
createPart('PitotTip', coneXGeo(0.016, 0.06, 8), black, { position: [8.97, 2.000, 0], parent: root });
// Dorsal spine fairing behind the canopy.
const spineSections = [];
const spineTable = [[2.95, 0.30, 0.10], [2.20, 0.36, 0.16], [1.20, 0.40, 0.19], [0.00, 0.42, 0.20], [-1.40, 0.40, 0.18], [-2.60, 0.35, 0.14], [-3.40, 0.28, 0.08]];
for (let i = 0; i < spineTable.length; i++) {
const x = spineTable[i][0], w = spineTable[i][1], h = spineTable[i][2];
// ride on the fuselage upper surface at that station
let top = 2.60;
for (let k = 0; k < fuseTable.length - 1; k++) {
if (x <= fuseTable[k][0] && x >= fuseTable[k + 1][0]) {
const t = (fuseTable[k][0] - x) / (fuseTable[k][0] - fuseTable[k + 1][0]);
top = lerp(fuseTable[k][2] + fuseTable[k][3], fuseTable[k + 1][2] + fuseTable[k + 1][3], t);
}
}
const ring = [];
const n = 9;
for (let j = 0; j <= n; j++) {
const a = (j / n) * Math.PI;
ring.push([x, top - 0.14 + h * Math.pow(Math.sin(a), 0.75), w * Math.cos(a)]);
}
for (let j = n - 1; j >= 1; j--) {
const a = (j / n) * Math.PI;
ring.push([x, top - 0.16, w * Math.cos(a) * 0.98]);
}
spineSections.push(ring);
}
createPart('DorsalSpine', loftGeo(spineSections, {}), skin, { parent: root });
// ------------------------------------------------- leading-edge root ext
const lerxTable = [
[5.90, 0.86, 0.045], [5.40, 0.97, 0.070], [4.60, 1.06, 0.100],
[3.80, 1.15, 0.125], [3.00, 1.25, 0.150], [2.40, 1.34, 0.165], [1.90, 1.40, 0.170],
];
for (let s = -1; s <= 1; s += 2) {
const secs = [];
for (let i = 0; i < lerxTable.length; i++) {
const x = lerxTable[i][0], zo = lerxTable[i][1], th = lerxTable[i][2];
const zi = 0.45;
const nv = 7;
const ring = [];
for (let j = 0; j <= nv; j++) {
const v = j / nv;
const z = lerp(zi, zo, v);
ring.push([x, 2.000 + (th * Math.pow(1 - v, 0.55) + 0.006), s * z]);
}
for (let j = nv; j >= 0; j--) {
const v = j / nv;
const z = lerp(zi, zo, v);
ring.push([x, 2.000 - (th * Math.pow(1 - v, 0.55) + 0.006), s * z]);
}
secs.push(ring);
}
createPart(s > 0 ? 'LerxRight' : 'LerxLeft', loftGeo(secs, {}), skin, { parent: root });
}
// ------------------------------------------------------------ main wing
const wingStations = [
{ s: 0.95, le: 2.79, te: -4.15, t: 0.062, y: 1.920 },
{ s: 1.60, le: 2.38, te: -4.10, t: 0.060, y: 1.950 },
{ s: 2.40, le: 1.87, te: -4.03, t: 0.058, y: 1.985 },
{ s: 3.30, le: 1.30, te: -3.60, t: 0.056, y: 2.020 },
{ s: 4.20, le: 0.73, te: -3.10, t: 0.052, y: 2.060 },
{ s: 5.10, le: 0.16, te: -2.55, t: 0.048, y: 2.100 },
{ s: 5.50, le: -0.09, te: -2.30, t: 0.046, y: 2.115 },
];
function wingStationsBetween(a, b) {
const out = [];
for (let i = 0; i < wingStations.length; i++) if (wingStations[i].s >= a - 1e-6 && wingStations[i].s <= b + 1e-6) out.push(wingStations[i]);
return out;
}
for (let side = -1; side <= 1; side += 2) {
const nm = side > 0 ? 'Right' : 'Left';
const map = function (x, st, off) { return [x, st.y + off, side * st.s]; };
// Split at the hinge lines instead of lofted full chord underneath them.
// The flap, flaperon and aileron below come off the same stations and the
// same aerofoil, so a wing that also spans their chord bands lays a second
// identical surface at the same depth: the top view came back with a
// hatched wedge across both wings where the rasterizer could not decide
// which of the two it was looking at. Each band now belongs to one part,
// and the seams fall exactly where a real hinge line does.
createPart('WingInboard' + nm, foilLoft(wingStationsBetween(0.95, 1.60), 0.002, 0.985, 13, map), skin, { parent: root });
createPart('WingBox' + nm, foilLoft(wingStationsBetween(1.60, 5.50), 0.150, 0.735, 13, map), skin, { parent: root });
// leading-edge flap, outboard of the body side
createPart('LEFlap' + nm, foilLoft(wingStationsBetween(1.60, 5.50), 0.004, 0.150, 8, map), skinDark, { parent: root });
// inboard flaperon and outboard aileron
createPart('Flaperon' + nm, foilLoft(wingStationsBetween(1.60, 3.30), 0.735, 0.985, 8, map), skinDark, { parent: root });
createPart('Aileron' + nm, foilLoft(wingStationsBetween(3.30, 5.50), 0.735, 0.985, 8, map), skinDark, { parent: root });
// wingtip nav light + static dischargers
createPart('NavLight' + nm, capsuleXGeo(0.055, 0.20, 8), side > 0 ? lightG : lightR, { position: [-0.60, 2.118, side * 5.52], parent: root });
for (let i = 0; i < 3; i++) {
createPart('StaticWick' + nm + i, cylinderXGeo(0.008, 0.012, 0.28, 5), black, { position: [-2.55 - i * 0.02, 2.10 + i * 0.005, side * (4.55 + i * 0.42)], rotation: [0, -12 * side, 0], parent: root });
}
}
// -------------------------------------------------------- vertical tails
const finStations = [
{ s: 0.00, le: -3.05, te: -6.75, t: 0.055 },
{ s: 0.65, le: -3.48, te: -6.80, t: 0.050 },
{ s: 1.40, le: -3.98, te: -6.86, t: 0.045 },
{ s: 2.10, le: -4.45, te: -6.91, t: 0.040 },
{ s: 2.54, le: -4.68, te: -6.93, t: 0.038 },
];
const cant = 25 * D2R, finRootY = 2.30, finRootZ = 1.02;
for (let side = -1; side <= 1; side += 2) {
const nm = side > 0 ? 'Right' : 'Left';
const map = function (x, st, off) {
return [x, finRootY + st.s * Math.cos(cant) - off * Math.sin(cant), side * (finRootZ + st.s * Math.sin(cant) + off * Math.cos(cant))];
};
createPart('VerticalTail' + nm, foilLoft(finStations, 0.002, 0.700, 10, map), skin, { parent: root });
createPart('Rudder' + nm, foilLoft(finStations, 0.712, 0.985, 8, map), skinDark, { parent: root });
// fin-tip antenna fairing
const tipY = finRootY + 2.54 * Math.cos(cant), tipZ = side * (finRootZ + 2.54 * Math.sin(cant));
createPart('FinTipPod' + nm, capsuleXGeo(0.048, 0.55, 8), skinDark, { position: [-5.75, tipY + 0.03, tipZ], rotation: [side * 25, 0, -6], parent: root });
createPart('FinTipLight' + nm, capsuleXGeo(0.040, 0.10, 8), lightW, { position: [-6.16, tipY - 0.02, tipZ], rotation: [side * 25, 0, -6], parent: root });
}
// ----------------------------------------------------------- stabilators
const stabStations = [
{ s: 0.00, le: -3.70, te: -6.40, t: 0.060 },
{ s: 0.90, le: -4.25, te: -6.42, t: 0.055 },
{ s: 1.70, le: -4.75, te: -6.45, t: 0.048 },
{ s: 2.35, le: -5.15, te: -6.48, t: 0.042 },
];
const anh = 6 * D2R, stabRootY = 1.985, stabRootZ = 0.95;
for (let side = -1; side <= 1; side += 2) {
const nm = side > 0 ? 'Right' : 'Left';
const map = function (x, st, off) {
return [x, stabRootY - st.s * Math.sin(anh) + off * Math.cos(anh), side * (stabRootZ + st.s * Math.cos(anh) + off * Math.sin(anh))];
};
createPart('Stabilator' + nm, foilLoft(stabStations, 0.002, 0.985, 11, map), skin, { parent: root });
// pivot fairing where the stabilator meets the boom
createPart('StabPivot' + nm, capsuleXGeo(0.115, 0.70, 10), skinDark, { position: [-5.10, 1.965, side * 1.16], rotation: [0, 0, 0], parent: root });
}
// ------------------------------------------------------- caret intakes
const intakeTable = [
{ x: 2.50, w: 0.455, h: 0.345, r: 0.075, zc: 1.245, yc: 1.505 },
{ x: 2.05, w: 0.460, h: 0.350, r: 0.085, zc: 1.250, yc: 1.510 },
{ x: 1.30, w: 0.470, h: 0.365, r: 0.110, zc: 1.255, yc: 1.530 },
{ x: 0.40, w: 0.470, h: 0.375, r: 0.140, zc: 1.245, yc: 1.560 },
{ x: -0.60, w: 0.450, h: 0.370, r: 0.170, zc: 1.215, yc: 1.590 },
{ x: -1.50, w: 0.410, h: 0.350, r: 0.190, zc: 1.180, yc: 1.620 },
];
for (let side = -1; side <= 1; side += 2) {
const nm = side > 0 ? 'Right' : 'Left';
const secs = [];
for (let i = 0; i < intakeTable.length; i++) {
const st = intakeTable[i];
const rr = roundRect(st.w, st.h, st.r, 16);
const ring = [];
for (let j = 0; j < rr.length; j++) {
const zl = rr[j][0], yl = rr[j][1];
let x = st.x;
if (i === 0) {
// caret rake: outboard-lower lip is furthest forward
x = st.x + 0.24 * (zl / st.w) - 0.16 * (yl / st.h);
}
ring.push([x, st.yc + yl, side * (st.zc + zl)]);
}
secs.push(ring);
}
createPart('Intake' + nm, loftGeo(secs, {}), skin, { parent: root });
// inner duct, normals inverted so the bore reads as a hole
const inner = [];
for (let i = 0; i < intakeTable.length; i++) {
const st = intakeTable[i];
const k = 0.86;
const rr = roundRect(st.w * k, st.h * k, st.r * k, 14);
const ring = [];
for (let j = 0; j < rr.length; j++) {
const zl = rr[j][0], yl = rr[j][1];
let x = st.x;
if (i === 0) x = st.x + 0.24 * (zl / (st.w * k)) * k - 0.16 * (yl / (st.h * k)) * k + 0.012;
ring.push([x, st.yc + yl + (i > 2 ? (i - 2) * 0.05 : 0), side * (st.zc + zl)]);
}
inner.push(ring);
}
createPart('IntakeDuct' + nm, loftGeo(inner, { invert: true }), duct, { parent: root });
createPart('IntakeFace' + nm, cylinderXGeo(0.33, 0.33, 0.05, 14), black, { position: [-1.40, 1.72, side * 1.19], parent: root });
// boundary-layer diverter splitter plate between duct and fuselage side
createPart('Diverter' + nm, boxGeo(2.9, 0.62, 0.05), skinDark, { position: [1.10, 1.52, side * 0.80], parent: root });
// lower lip reinforcement
createPart('IntakeLip' + nm, capsuleZGeo(0.045, 0.80, 8), skinDark, { position: [2.56, 1.175, side * 1.25], rotation: [0, 0, 0], parent: root });
}
// ---------------------------------------------------- exhaust / nozzles
const nozProfile = [
[-6.10, 0.545], [-6.45, 0.520], [-6.80, 0.480], [-7.10, 0.440], [-7.28, 0.428], [-7.42, 0.440], [-7.50, 0.458],
[-7.50, 0.418], [-7.42, 0.402], [-7.28, 0.392], [-7.10, 0.404], [-6.80, 0.442], [-6.45, 0.482], [-6.10, 0.508],
];
const petalGeo = await extrudeProfile(
[[-0.24, -0.020], [0.24, -0.013], [0.24, 0.013], [-0.24, 0.020]],
{ depth: 0.140, axis: 'z', center: true }
);
const shingleGeo = await extrudeProfile(
[[-0.19, -0.022], [0.19, -0.014], [0.19, 0.014], [-0.19, 0.022]],
{ depth: 0.152, axis: 'z', center: true }
);
for (let side = -1; side <= 1; side += 2) {
const nm = side > 0 ? 'Right' : 'Left';
const zc = side * 0.50, yc = 1.780;
const noz = createPart('Nozzle' + nm, revolveClosed(nozProfile, 24, 'x'), nozzleMat, { parent: root });
noz.position.set(0, yc, zc);
// afterburner interior + flame holder
createPart('BurnerCan' + nm, cylinderXGeo(0.375, 0.375, 1.30, 16), burnt, { position: [-6.85, yc, zc], parent: root });
createPart('TurbineFace' + nm, cylinderXGeo(0.36, 0.36, 0.06, 16), black, { position: [-6.55, yc, zc], parent: root });
createPart('FlameCone' + nm, coneXGeo(0.16, 0.55, 12), burnt, { position: [-6.90, yc, zc], parent: root });
const count = 20;
let petal0 = null, shing0 = null;
for (let i = 0; i < count; i++) {
const a = (i / count) * 360;
const ar = a * D2R;
const py = yc + 0.418 * Math.cos(ar), pz = zc + 0.418 * Math.sin(ar);
if (i === 0) petal0 = createPart('NozzlePetal' + nm + i, petalGeo, nozzleMat, { position: [-7.30, py, pz], rotation: [a, 0, 0], parent: root });
else createInstance('NozzlePetal' + nm + i, petal0, { position: [-7.30, py, pz], rotation: [a, 0, 0], parent: root });
const b = ((i + 0.5) / count) * 360, br = b * D2R;
const sy = yc + 0.478 * Math.cos(br), sz = zc + 0.478 * Math.sin(br);
if (i === 0) shing0 = createPart('NozzleShingle' + nm + i, shingleGeo, nozzleMat, { position: [-7.00, sy, sz], rotation: [b, 0, 0], parent: root });
else createInstance('NozzleShingle' + nm + i, shing0, { position: [-7.00, sy, sz], rotation: [b, 0, 0], parent: root });
}
// actuator rods around the nozzle throat
for (let i = 0; i < 6; i++) {
const a = (i / 6) * Math.PI * 2 + 0.4;
beamBetween('NozzleActuator' + nm + i,
[-6.55, yc + 0.55 * Math.cos(a), zc + 0.55 * Math.sin(a)],
[-7.05, yc + 0.475 * Math.cos(a), zc + 0.475 * Math.sin(a)],
0.026, steel, { parent: root });
}
}
// ventral fairing between the two engine bays
createPart('EngineBayFairing', capsuleXGeo(0.22, 1.9, 10), skinDark, { position: [-6.10, 1.44, 0], parent: root });
// ---------------------------------------------------------- cockpit tub
const tub = boxGeo(2.55, 0.72, 1.14);
createPart('CockpitTub', tub, black, { position: [4.45, 2.18, 0], parent: root });
createPart('CockpitFloor', boxGeo(2.4, 0.06, 1.0), black, { position: [4.45, 1.86, 0], parent: root });
// instrument panel + coaming
createPart('InstrumentPanel', boxGeo(0.10, 0.46, 0.90), black, { position: [5.42, 2.24, 0], rotation: [0, 0, 18], parent: root });
createPart('PanelGlare', await roundedBoxGeo(0.34, 0.10, 1.00, 0.035, { segments: 3 }), black, { position: [5.30, 2.47, 0], parent: root });
createPart('MFD_Centre', decalBox(0.24, 0.24, 0.02), gameMaterial(0x123a2e, { emissive: 0x1d7a5a, emissiveIntensity: 0.5 }), { position: [5.37, 2.26, 0], rotation: [0, 0, 18], parent: root });
createPart('MFD_Left', decalBox(0.18, 0.18, 0.02), gameMaterial(0x123a2e, { emissive: 0x1d7a5a, emissiveIntensity: 0.5 }), { position: [5.38, 2.24, -0.30], rotation: [0, 0, 18], parent: root });
createPart('MFD_Right', decalBox(0.18, 0.18, 0.02), gameMaterial(0x123a2e, { emissive: 0x1d7a5a, emissiveIntensity: 0.5 }), { position: [5.38, 2.24, 0.30], rotation: [0, 0, 18], parent: root });
// HUD
createPart('HudBody', await roundedBoxGeo(0.26, 0.20, 0.34, 0.03, { segments: 3 }), black, { position: [5.30, 2.62, 0], parent: root });
createPart('HudCombiner', decalBox(0.30, 0.30, 0.012), glass, { position: [5.22, 2.79, 0], rotation: [0, 0, 15], parent: root });
createPart('HudCombiner2', decalBox(0.26, 0.26, 0.012), glass, { position: [5.36, 2.77, 0], rotation: [0, 0, 8], parent: root });
// ejection seat
createPart('SeatPan', await roundedBoxGeo(0.52, 0.10, 0.50, 0.03, { segments: 3 }), gameMaterial(0x2c3138, { roughness: 0.85 }), { position: [4.10, 2.10, 0], parent: root });
createPart('SeatBack', await roundedBoxGeo(0.16, 0.66, 0.50, 0.04, { segments: 3 }), gameMaterial(0x2c3138, { roughness: 0.85 }), { position: [3.86, 2.36, 0], rotation: [0, 0, -12], parent: root });
createPart('SeatHeadbox', await roundedBoxGeo(0.24, 0.30, 0.44, 0.05, { segments: 3 }), gameMaterial(0x33383f, { roughness: 0.85 }), { position: [3.79, 2.72, 0], parent: root });
createPart('SeatHandle', cylinderZGeo(0.022, 0.022, 0.32, 8), gameMaterial(0xd8b400, { roughness: 0.5 }), { position: [3.86, 2.88, 0], parent: root });
createPart('ControlStick', cylinderYGeo(0.022, 0.028, 0.30, 8), black, { position: [4.55, 2.10, 0], parent: root });
createPart('StickGrip', capsuleYGeo(0.038, 0.12, 8), gameMaterial(0x24282d, { roughness: 0.9 }), { position: [4.55, 2.28, 0], parent: root });
createPart('ThrottleQuadrant', boxGeo(0.34, 0.08, 0.10), black, { position: [4.75, 2.16, -0.44], parent: root });
// --------------------------------------------------------- bubble canopy
const canTable = [
[5.95, 0.30, 0.18, 2.500], [5.70, 0.42, 0.36, 2.500], [5.40, 0.52, 0.55, 2.500],
[5.00, 0.60, 0.70, 2.510], [4.50, 0.64, 0.78, 2.520], [4.00, 0.64, 0.78, 2.530],
[3.50, 0.60, 0.70, 2.540], [3.10, 0.52, 0.52, 2.550], [2.85, 0.44, 0.30, 2.560],
];
function canopyRing(x, w, h, sy) {
const ring = [];
const n = 12;
for (let i = 0; i <= n; i++) {
const a = (i / n) * Math.PI;
ring.push([x, sy + h * Math.pow(Math.sin(a), 0.78), w * Math.cos(a)]);
}
for (let i = n - 1; i >= 1; i--) {
const a = (i / n) * Math.PI;
ring.push([x, sy - 0.05, w * Math.cos(a) * 0.985]);
}
return ring;
}
const canopySections = canTable.map(function (r) { return canopyRing(r[0], r[1], r[2], r[3]); });
createPart('Canopy', loftGeo(canopySections, {}), glass, { parent: root });
// canopy framing: windscreen bow, rear bow, and the two sill rails
function archPath(x, w, h, sy, scale) {
const pts = [];
const n = 14;
for (let i = 0; i <= n; i++) {
const a = (i / n) * Math.PI;
pts.push([x, sy + h * scale * Math.pow(Math.sin(a), 0.78), w * scale * Math.cos(a)]);
}
return pts;
}
createPart('WindscreenBow', curveToMesh(archPath(5.40, 0.52, 0.55, 2.500, 1.015), 0.030, 20, 6), skinDark, { parent: root });
createPart('CanopyRearBow', curveToMesh(archPath(2.87, 0.44, 0.30, 2.560, 1.015), 0.030, 20, 6), skinDark, { parent: root });
for (let side = -1; side <= 1; side += 2) {
const rail = [];
for (let i = 0; i < canTable.length; i++) rail.push([canTable[i][0], canTable[i][3] - 0.03, side * canTable[i][1] * 1.01]);
createPart('CockpitSill' + (side > 0 ? 'Right' : 'Left'), curveToMesh(rail, 0.036, 26, 6), skinDark, { parent: root });
}
createPart('CanopyActuator', boxGeo(0.55, 0.09, 0.09), steel, { position: [3.05, 2.62, 0], parent: root });
// -------------------------------------------------------- landing gear
// shared wheel builder
function makeWheel(name, cx, cy, cz, R, W, parent) {
const rBead = R * 0.62;
const tyre = [
[-W / 2, rBead], [W / 2, rBead], [W / 2, rBead + (R - rBead) * 0.30], [W * 0.44, rBead + (R - rBead) * 0.72],
[W * 0.34, R * 0.995], [W * 0.16, R], [-W * 0.16, R], [-W * 0.34, R * 0.995],
[-W * 0.44, rBead + (R - rBead) * 0.72], [-W / 2, rBead + (R - rBead) * 0.30],
];
const t = createPart(name + 'Tyre', revolveClosed(tyre, 22, 'z'), rubber, { parent: parent });
t.position.set(cx, cy, cz);
const rimProf = [
[-W * 0.40, R * 0.28], [W * 0.40, R * 0.28], [W * 0.40, rBead * 1.02], [W * 0.18, rBead * 1.02],
[W * 0.10, R * 0.50], [-W * 0.10, R * 0.50], [-W * 0.18, rBead * 1.02], [-W * 0.40, rBead * 1.02],
];
const rim = createPart(name + 'Rim', revolveClosed(rimProf, 18, 'z'), gameMaterial(0x9aa0a6, { metalness: 0.85, roughness: 0.35 }), { parent: parent });
rim.position.set(cx, cy, cz);
createPart(name + 'Hub', cylinderZGeo(R * 0.24, R * 0.20, W * 0.95, 12), steel, { position: [cx, cy, cz], parent: parent });
for (let i = 0; i < 6; i++) {
const a = (i / 6) * Math.PI * 2;
createPart(name + 'Bolt' + i, cylinderZGeo(0.022, 0.022, W * 0.86, 6), steel,
{ position: [cx + R * 0.36 * Math.cos(a), cy + R * 0.36 * Math.sin(a), cz], parent: parent });
}
createPart(name + 'Brake', cylinderZGeo(R * 0.52, R * 0.52, W * 0.30, 14), gameMaterial(0x585d63, { metalness: 0.8, roughness: 0.55 }),
{ position: [cx, cy, cz - Math.sign(cz || 1) * W * 0.42], parent: parent });
}
// --- nose gear ---
const ng = createPivot('NoseGear', [0, 0, 0], root);
createPart('NoseBayRoof', boxGeo(1.5, 0.10, 0.70), black, { position: [4.60, 1.36, 0], parent: ng });
createPart('NoseTrunnion', await roundedBoxGeo(0.34, 0.24, 0.34, 0.04, { segments: 3 }), skinDark, { position: [4.78, 1.30, 0], parent: ng });
createPart('NoseOleoBarrel', cylinderYGeo(0.088, 0.095, 0.60, 12), gameMaterial(0x767c83, { metalness: 0.8, roughness: 0.4 }), { position: [4.72, 1.02, 0], rotation: [0, 0, 5], parent: ng });
createPart('NoseOleoPiston', cylinderYGeo(0.062, 0.062, 0.42, 12), steel, { position: [4.66, 0.70, 0], rotation: [0, 0, 5], parent: ng });
createPart('NoseForkYokeL', boxGeo(0.12, 0.30, 0.05), gameMaterial(0x767c83, { metalness: 0.8, roughness: 0.4 }), { position: [4.63, 0.45, -0.145], parent: ng });
createPart('NoseForkYokeR', boxGeo(0.12, 0.30, 0.05), gameMaterial(0x767c83, { metalness: 0.8, roughness: 0.4 }), { position: [4.63, 0.45, 0.145], parent: ng });
createPart('NoseAxle', cylinderZGeo(0.040, 0.040, 0.34, 10), steel, { position: [4.62, 0.32, 0], parent: ng });
makeWheel('NoseWheel', 4.62, 0.32, 0.0, 0.32, 0.20, ng);
beamBetween('NoseDragBrace', [4.86, 1.24, 0], [5.44, 1.42, 0], 0.038, steel, { parent: ng });
beamBetween('NoseTorqueUpper', [4.58, 1.00, 0.10], [4.55, 0.80, 0.06], 0.022, steel, { parent: ng });
beamBetween('NoseTorqueLower', [4.55, 0.80, 0.06], [4.55, 0.58, 0.10], 0.022, steel, { parent: ng });
createPart('TaxiLight', await roundedBoxGeo(0.10, 0.12, 0.18, 0.02, { segments: 3 }), lightW, { position: [4.86, 0.92, 0], parent: ng });
createPart('NoseDoorL', boxGeo(1.20, 0.035, 0.42), skin, { position: [4.60, 1.24, -0.44], rotation: [-62, 0, 0], parent: ng });
createPart('NoseDoorR', boxGeo(1.20, 0.035, 0.42), skin, { position: [4.60, 1.24, 0.44], rotation: [62, 0, 0], parent: ng });
// --- main gear ---
for (let side = -1; side <= 1; side += 2) {
const nm = side > 0 ? 'Right' : 'Left';
const mg = createPivot('MainGear' + nm, [0, 0, 0], root);
createPart('MainBayRoof' + nm, boxGeo(1.7, 0.10, 0.78), black, { position: [-1.55, 1.16, side * 1.20], parent: mg });
createPart('MainTrunnion' + nm, await roundedBoxGeo(0.40, 0.26, 0.30, 0.04, { segments: 3 }), skinDark, { position: [-1.40, 1.14, side * 1.10], parent: mg });
beamBetween('MainOleoBarrel' + nm, [-1.44, 1.14, side * 1.12], [-1.58, 0.66, side * 1.36], 0.090, gameMaterial(0x767c83, { metalness: 0.8, roughness: 0.4 }), { parent: mg });
beamBetween('MainOleoPiston' + nm, [-1.57, 0.72, side * 1.35], [-1.62, 0.46, side * 1.42], 0.062, steel, { parent: mg });
beamBetween('MainDragBrace' + nm, [-1.46, 1.06, side * 1.16], [-2.46, 1.20, side * 0.98], 0.045, steel, { parent: mg });
beamBetween('MainSideStay' + nm, [-1.52, 0.92, side * 1.26], [-1.50, 1.14, side * 0.72], 0.038, steel, { parent: mg });
beamBetween('MainTorqueUpper' + nm, [-1.44, 0.96, side * 1.28], [-1.42, 0.78, side * 1.33], 0.022, steel, { parent: mg });
beamBetween('MainTorqueLower' + nm, [-1.42, 0.78, side * 1.33], [-1.48, 0.56, side * 1.38], 0.022, steel, { parent: mg });
createPart('MainAxle' + nm, cylinderZGeo(0.048, 0.048, 0.42, 10), steel, { position: [-1.62, 0.46, side * 1.42], parent: mg });
makeWheel('MainWheel' + nm, -1.62, 0.46, side * 1.52, 0.46, 0.30, mg);
createPart('MainDoor' + nm, boxGeo(1.45, 0.04, 0.50), skin, { position: [-1.55, 1.04, side * 1.02], rotation: [side * 68, 0, 0], parent: mg });
createPart('MainDoorAft' + nm, boxGeo(0.62, 0.04, 0.36), skin, { position: [-2.40, 1.10, side * 1.24], rotation: [side * 48, 0, 0], parent: mg });
}
// -------------------------------------------------------------- pylons
const pylonGeo = await roundedBoxGeo(1.85, 0.36, 0.22, 0.055, { segments: 3 });
const pylonNose = await roundedBoxGeo(0.42, 0.24, 0.20, 0.075, { segments: 3 });
const pylonSpec = [
{ z: 2.30, x: 0.20, yTop: 1.815 },
{ z: 3.55, x: -0.20, yTop: 1.905 },
];
for (let side = -1; side <= 1; side += 2) {
for (let i = 0; i < pylonSpec.length; i++) {
const p = pylonSpec[i];
const nm = (side > 0 ? 'R' : 'L') + (i === 0 ? 'Inner' : 'Outer');
createPart('Pylon' + nm, pylonGeo, skinDark, { position: [p.x, p.yTop - 0.20, side * p.z], parent: root });
createPart('PylonNose' + nm, pylonNose, skinDark, { position: [p.x + 1.05, p.yTop - 0.14, side * p.z], parent: root });
createPart('PylonShoe' + nm, boxGeo(1.60, 0.05, 0.30), skinDark, { position: [p.x, p.yTop - 0.02, side * p.z], parent: root });
// sway braces and the ejector-rack lugs
for (let k = 0; k < 2; k++) {
createPart('SwayBrace' + nm + k, cylinderYGeo(0.026, 0.026, 0.16, 6), steel, { position: [p.x - 0.30 + k * 0.60, p.yTop - 0.44, side * (p.z + 0.10)], parent: root });
createPart('SwayBrace' + nm + 'b' + k, cylinderYGeo(0.026, 0.026, 0.16, 6), steel, { position: [p.x - 0.30 + k * 0.60, p.yTop - 0.44, side * (p.z - 0.10)], parent: root });
}
createPart('PylonLug' + nm, boxGeo(0.10, 0.10, 0.16), steel, { position: [p.x, p.yTop - 0.40, side * p.z], parent: root });
}
}
// ------------------------------------------------------- surface detail
// gun port and gas-purge vents on the left LERX shoulder
createPart('GunPort', cylinderXGeo(0.055, 0.055, 0.22, 10), black, { position: [5.05, 2.16, -0.86], rotation: [0, 0, 0], parent: root });
createPart('GunFairing', capsuleXGeo(0.10, 0.70, 10), skinDark, { position: [4.70, 2.14, -0.86], parent: root });
for (let i = 0; i < 3; i++) createPart('GunVent' + i, decalBox(0.16, 0.05, 0.012), black, { position: [4.35 - i * 0.22, 2.20, -0.84], rotation: [0, 0, 0], parent: root });
// in-flight refuelling receptacle door on the spine
createPart('RefuelDoor', decalBox(0.42, 0.24, 0.02), skinDark, { position: [2.30, 2.72, 0.18], rotation: [-14, 0, 0], parent: root });
// upper-surface airbrakes, cracked open either side of the spine
for (let side = -1; side <= 1; side += 2) {
createPart('Airbrake' + (side > 0 ? 'R' : 'L'), boxGeo(1.05, 0.05, 0.52), skinDark,
{ position: [-3.05, 2.60, side * 0.62], rotation: [0, 0, -9], parent: root });
}
// AoA vanes, angle-of-sideslip probe, and blade antennas
for (let side = -1; side <= 1; side += 2) {
createPart('AoaVane' + (side > 0 ? 'R' : 'L'), boxGeo(0.20, 0.05, 0.02), black, { position: [7.10, 2.02, side * 0.51], parent: root });
createPart('AoaBoss' + (side > 0 ? 'R' : 'L'), cylinderZGeo(0.045, 0.045, 0.06, 8), skinDark, { position: [7.02, 2.02, side * 0.50], parent: root });
}
createPart('BladeAntennaTop', boxGeo(0.34, 0.20, 0.03), skinDark, { position: [0.60, 2.85, 0], rotation: [0, 0, -8], parent: root });
createPart('BladeAntennaBelly', boxGeo(0.30, 0.18, 0.03), skinDark, { position: [3.00, 1.14, 0], rotation: [0, 0, 8], parent: root });
// formation-light strips and anti-collision beacons
for (let side = -1; side <= 1; side += 2) {
createPart('FormLightFwd' + (side > 0 ? 'R' : 'L'), decalBox(0.70, 0.07, 0.012), lightW, { position: [3.60, 2.02, side * 1.01], rotation: [0, 0, 0], parent: root });
createPart('FormLightAft' + (side > 0 ? 'R' : 'L'), decalBox(0.70, 0.07, 0.012), lightW, { position: [-2.60, 2.06, side * 1.24], rotation: [0, 0, 0], parent: root });
}
createPart('BeaconTop', capsuleYGeo(0.055, 0.06, 8), lightR, { position: [-1.20, 2.70, 0], parent: root });
createPart('BeaconBelly', capsuleYGeo(0.055, 0.06, 8), lightR, { position: [-0.60, 1.10, 0], parent: root });
createPart('TailLight', capsuleXGeo(0.05, 0.10, 8), lightW, { position: [-7.06, 2.30, 0], parent: root });
// access panels along the spine and wing roots
const panelSpec = [
[1.60, 2.66, 0.55, 0.60, 0.44], [0.30, 2.66, 0.60, 0.60, 0.44], [-1.10, 2.62, 0.60, 0.60, 0.44],
[-2.20, 2.55, 0.62, 0.50, 0.40], [-4.00, 2.48, 0.60, 0.60, 0.40],
];
for (let i = 0; i < panelSpec.length; i++) {
const p = panelSpec[i];
for (let side = -1; side <= 1; side += 2) {
createPart('Panel' + i + (side > 0 ? 'R' : 'L'), decalBox(p[3], p[4], 0.015), skinDark,
{ position: [p[0], p[1], side * p[2]], rotation: [90, 0, 0], 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.