Skip to content
Kiln0.10Get Kiln

Workboat hull with an offset crane and sliding service platform

Earlier examples from earlier Kiln versions.

Historical gallery render of Research vessel; 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. 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
24,306
Estimated draws
150
Materials
10
Textures
0
Animation clips
1
Bounds X × Y × Z
8.06 × 4.3 × 3.36 m
Build warnings
0

Measurements come from this build.

Download this build

Runtime: 384,928 bytes. Original: 384,696 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
7f63149bfd6980429b3ad43fab8138d32565e8bb113c8da4ae8f20408c241b97
Original GLB
c37c273aca91cbc37be90cc73d79f7e9f630ebd75de2b7bfffa0504cccc9c830
Source
1f109fd8c5d6de9fe07507e473a1c7ba674565f1544ac68dd6067a5664371455

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

GPT-6 Astra through Codex ·

Recorded Codex / gpt-6-astra run, followed by a separate supplied-source refinement

Source access
Initial authoring used installed Kiln skills and tool definitions, without engine implementation or finished examples.
Inherited context
Fresh initial conversation; separate fresh refinement supplied the initial research-vessel source.
Starting example
None for initial authoring. Research-vessel source supplied for final-package refinement.
Human input
Maintainer supplied task briefs and reviewed results. Final refinement changed serviceOffset from 0.25 to 0.40; no hand-edited geometry.
Authoring review
Whole, attachment and locked animation PNGs were delivered by the GPU renderer and reviewed. Model inferred a possible 1 cm mid-motion overlap from source; engineering clearance is not verified.

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

research-vessel.kiln.js
const meta = { name: 'RV Tern — asymmetric coastal research vessel', category: 'vehicle', role: 'vehicle', units: 'metres' };
const serviceOffset = 0.40;
const deckHeight = 1.20;
const carriageX = -2.85;
function build() {
  const root = createRoot('ResearchVessel');
  const navy = gameMaterial(0x16475a, { roughness: 0.38, metalness: 0.38, flatShading: false });
  const deckMat = gameMaterial(0x87978e, { roughness: 0.93 });
  const white = gameMaterial(0xe5e6d8, { roughness: 0.45, metalness: 0.18 });
  const steel = gameMaterial(0x82969d, { roughness: 0.32, metalness: 0.8 });
  const dark = gameMaterial(0x172b35, { roughness: 0.55, metalness: 0.3 });
  const yellow = gameMaterial(0xf2ad32, { roughness: 0.4, metalness: 0.32 });
  const glass = gameMaterial(0x164052, { roughness: 0.16, metalness: 0.5 });
  const red = gameMaterial(0xc85a42, { roughness: 0.65 });
  const rubber = gameMaterial(0x20272b, { roughness: 0.92 });
  function group(name, parent=root, position=[0,0,0]) {
    const g = new THREE.Group(); g.name=name; g.position.set(...position); parent.add(g); return g;
  }
  function box(name, size, pos, mat, parent=root) { return createPart(name,boxGeo(...size),mat,{position:pos,parent}); }
  function rod(name,a,b,r,mat,parent=root) {
    const av=new THREE.Vector3(...a), bv=new THREE.Vector3(...b), d=bv.clone().sub(av);
    const m=createPart(name,cylinderGeo(r,r,d.length(),10),mat,{position:av.clone().add(bv).multiplyScalar(0.5).toArray(),parent});
    m.quaternion.setFromUnitVectors(new THREE.Vector3(0,1,0),d.normalize()); return m;
  }
  function pipe(name,points,r,mat,parent=root) { return createPart(name,pipeAlongPath(points,r,{tubularSegments:96,radialSegments:8}),mat,{parent}); }
  // Authored longitudinal sections: broad transom, gently curved working body, fine raised entrance.
  function section(x) {
    const t=(x+4)/8;
    const w=1.43+0.13*Math.sin(Math.PI*t)-1.40*Math.pow(t,4);
    const rise=Math.pow(Math.max(0,(x-0.3)/3.7),1.65);
    return {x,w,y:deckHeight+0.48*rise,k:0.14+0.48*Math.pow(t,5)};
  }
  const stations=Array.from({length:65},(_,i)=>section(-4+i/8));
  // Closed seven-sided transverse rings, continuous longitudinally; ring order faces +X.
  function hullGeometry() {
    const positions=[],indices=[], n=7;
    stations.forEach(s=>{
      const ch=s.k+0.34;
      [[s.y,s.w],[ch,s.w*0.86],[s.k+0.055,s.w*0.34],[s.k,0],[s.k+0.055,-s.w*0.34],[ch,-s.w*0.86],[s.y,-s.w]]
        .forEach(p=>positions.push(s.x,p[0],p[1]));
    });
    for(let i=0;i<stations.length-1;i++)for(let j=0;j<n;j++){
      const a=i*n+j,b=i*n+(j+1)%n,c=(i+1)*n+(j+1)%n,d=(i+1)*n+j;
      indices.push(a,c,d,a,b,c);
    }
    for(const end of [0,stations.length-1]){
      const s=stations[end], center=positions.length/3; positions.push(s.x,(s.y+s.k)/2,0);
      for(let j=0;j<n;j++){const a=end*n+j,b=end*n+(j+1)%n; if(end===0)indices.push(center,b,a);else indices.push(center,a,b);}
    }
    return meshGeo({positions,indices});
  }
  const hull=group('ContinuousChineHull');
  createPart('CurvedShell',hullGeometry(),navy,{parent:hull});
  // Deck is an explicit thin closed shell following the same sheer.
  const dp=[],di=[];
  stations.forEach(s=>dp.push(s.x,s.y+0.025,s.w-0.025,s.x,s.y+0.025,-s.w+0.025,s.x,s.y-0.015,-s.w+0.025,s.x,s.y-0.015,s.w-0.025));
  for(let i=0;i<64;i++)for(let j=0;j<4;j++){const a=4*i+j,b=4*i+(j+1)%4,c=4*(i+1)+(j+1)%4,d=4*(i+1)+j;di.push(a,d,c,a,c,b);}
  di.push(0,1,2,0,2,3,256,258,257,256,259,258);
  createPart('SheerFollowingDeck',meshGeo({positions:dp,indices:di}),deckMat,{parent:hull});
  for(const side of [-1,1]){
    pipe('ChineRubbingStrip_'+side,stations.map(s=>[s.x,s.k+0.34,side*s.w*0.86]),0.027,white,hull);
    pipe('Gunwale_'+side,stations.map(s=>[s.x,s.y+0.07,side*(s.w-0.015)]),0.052,dark,hull);
    pipe('BootStripe_'+side,stations.map(s=>[s.x,s.k+0.17,side*s.w*0.60]),0.034,red,hull);
  }
  box('TransomRubRail',[0.08,0.1,2.82],[-3.99,1.17,0],dark,hull);
  const rails=group('SafetyRailings');
  for(const side of [-1,1]){
    const pts=[];
    for(let i=0;i<=32;i++){const s=section(-3.85+i*7.72/32);pts.push([s.x,s.y+0.85,side*(s.w-0.12)]);}
    pipe('ContinuousTopRail_'+side,pts,0.026,white,rails);
    pipe('ContinuousMidRail_'+side,pts.map(p=>[p[0],p[1]-0.39,p[2]]),0.018,steel,rails);
    for(let i=0;i<=10;i++){const s=section(-3.85+i*7.65/10),z=side*(s.w-0.12);
      rod('Stanchion_'+side+'_'+i,[s.x,s.y+0.045,z],[s.x,s.y+0.85,z],0.025,white,rails);
      box('RailFoot_'+side+'_'+i,[0.12,0.035,0.12],[s.x,s.y+0.045,z],steel,rails);
    }
  }
  for(const y of [1.65,2.05]) rod('SternRail_'+y,[-3.86,y,-1.30],[-3.86,y,1.30],0.025,white,rails);
  for(const z of [-0.65,0,0.65])rod('SternPost_'+z,[-3.86,1.23,z],[-3.86,2.05,z],0.025,white,rails);
  const cabin=group('PortWheelhouse',root,[0.45,1.25,-0.62]);
  box('CabinBody',[2.28,1.62,1.18],[0,0.82,0],white,cabin);
  box('CabinFoundation',[2.4,0.12,1.30],[0,0.02,0],dark,cabin);
  box('RoofOverhang',[2.53,0.14,1.43],[0,1.69,0],white,cabin);
  box('RoofAccent',[2.49,0.055,1.39],[0,1.78,0],navy,cabin);
  for(const z of [-0.605,0.605]){
    for(const x of [-0.72,0,0.72])box('SideWindow_'+z+'_'+x,[0.57,0.59,0.018],[x,1.14,z],glass,cabin);
    rod('CabinGrabRail_'+z,[-0.85,0.55,z*1.08],[0.9,0.55,z*1.08],0.019,steel,cabin);
  }
  for(const z of [-0.3,0.3]){
    box('ForwardWindshield_'+z,[0.022,0.67,0.5],[1.151,1.16,z],glass,cabin);
    rod('WindshieldWiper_'+z,[1.172,0.88,z-0.18],[1.172,1.32,z+0.13],0.012,dark,cabin);
  }
  box('AftDoor',[0.025,1.25,0.62],[-1.153,0.66,0.03],navy,cabin);
  box('DoorGlazing',[0.025,0.48,0.45],[-1.17,1.0,0.03],glass,cabin);
  rod('DoorHandle',[-1.19,0.54,0.20],[-1.19,0.74,0.20],0.018,steel,cabin);
  const mast=group('SensorMast',cabin,[0.2,1.8,0]);
  rod('Mast',[0,0,0],[0,0.92,0],0.045,white,mast);
  box('RadarBar',[0.85,0.10,0.17],[0,0.68,0],white,mast);
  createPart('Radome',sphereGeo(0.19,16,12),white,{position:[0,1.0,0],parent:mast});
  rod('Aerial',[-0.48,0,0],[-0.48,1.22,0],0.012,steel,mast);
  createPart('PortLamp',sphereGeo(0.055,12,8),red,{position:[0.75,1.8,-0.63],parent:cabin});
  createPart('StarboardLamp',sphereGeo(0.055,12,8),gameMaterial(0x37bc88),{position:[0.75,1.8,0.63],parent:cabin});
  const work=group('OpenStarboardWorkDeck');
  // Flush perimeter markings leave the main walking and sampling area clear.
  for(const z of [0.29,1.20])box('WorkZoneLine_'+z,[3.05,0.014,0.025],[-0.28,1.24,z],yellow,work);
  box('FlushSampleHatch',[0.95,0.035,0.67],[-0.68,1.245,0.75],dark,work);
  box('SampleHatchLid',[0.84,0.018,0.56],[-0.68,1.272,0.75],steel,work);
  for(const x of [-0.96,-0.41])rod('HatchHandle_'+x,[x,1.30,0.66],[x,1.30,0.84],0.014,dark,work);
  const track=group('SternDeckTrack');
  for(const z of [-0.28,0.28]){
    box('TrackBed_'+z,[1.8,0.065,0.20],[-2.50,1.265,z],dark,track);
    box('TrackRail_'+z,[1.8,0.065,0.075],[-2.50,1.33,z],steel,track);
    for(const x of [-3.34,-1.66])box('EndStop_'+z+'_'+x,[0.10,0.16,0.16],[x,1.34,z],yellow,track);
  }
  const motion=createPivot('ServiceTranslation',[carriageX+serviceOffset,1.35,0],root);
  const service=group('ServiceAssembly',motion);
  box('CarriageFootplate',[0.70,0.11,0.90],[0,0.085,0],yellow,service);
  for(const x of [-0.23,0.23])for(const z of [-0.28,0.28]){
    box('RailShoe_'+x+'_'+z,[0.18,0.07,0.17],[x,-0.005,z],dark,service);
    createPart('FootplateBolt_'+x+'_'+z,cylinderGeo(0.033,0.033,0.025,6),steel,{position:[x,0.155,z],parent:service});
  }
  createPart('SlewBearing',cylinderGeo(0.27,0.29,0.16,24),dark,{position:[0,0.22,0],parent:service});
  createPart('CranePedestal',cylinderGeo(0.15,0.21,0.73,20),yellow,{position:[0,0.65,0],parent:service});
  box('CraneShoulder',[0.34,0.31,0.34],[0,1.08,0],yellow,service);
  rod('MainBoom',[0,1.11,0],[0.1,2.07,0.63],0.105,yellow,service);
  rod('OuterBoom',[0.1,2.07,0.63],[0.16,1.99,1.19],0.082,yellow,service);
  rod('HydraulicBody',[0,0.66,0.11],[0.065,1.42,0.56],0.063,dark,service);
  rod('HydraulicRod',[0.065,1.42,0.56],[0.10,1.84,0.73],0.032,steel,service);
  createPart('BoomHinge',cylinderXGeo(0.13,0.13,0.41,16),steel,{position:[0.1,2.07,0.63],parent:service});
  createPart('WinchDrum',cylinderXGeo(0.17,0.17,0.34,20),dark,{position:[-0.15,1.10,-0.22],parent:service});
  pipe('CableGuide',[[-0.28,0.17,-0.31],[-0.28,0.42,-0.31],[-0.18,0.73,-0.25],[-0.15,1.10,-0.22]],0.031,steel,service);
  pipe('HoistCable',[[-0.15,1.13,-0.22],[0.07,2.19,0.62],[0.17,2.08,1.20],[0.17,0.78,1.20]],0.014,dark,service);
  createPart('HeadSheave',cylinderXGeo(0.10,0.10,0.10,16),dark,{position:[0.17,2.04,1.20],parent:service});
  createPart('SampleWeight',cylinderGeo(0.11,0.07,0.28,12),steel,{position:[0.17,0.65,1.20],parent:service});
  box('CraneControlBox',[0.19,0.30,0.21],[0.24,0.63,-0.16],navy,service);
  for(const z of [-1,1])for(const x of [-2.0,0.0,1.8]){
    const s=section(x);
    createPart('SideFender_'+x+'_'+z,cylinderGeo(0.105,0.105,0.52,12),rubber,{position:[x,s.y-0.14,z*(s.w+0.06)],parent:root});
    rod('FenderLanyard_'+x+'_'+z,[x,s.y+0.25,z*(s.w-0.08)],[x,s.y+0.1,z*(s.w+0.06)],0.011,dark);
  }
  for(const x of [-3.55,2.52])for(const z of [-0.83,0.83]){
    const s=section(x); rod('Bollard_'+x+'_'+z,[x,s.y,z],[x,s.y+0.20,z],0.055,steel);
    rod('Cleat_'+x+'_'+z,[x-0.13,s.y+0.18,z],[x+0.13,s.y+0.18,z],0.029,steel);
  }
  box('Skeg',[1.35,0.25,0.09],[-2.75,0.12,0],navy);
  rod('PropellerShaft',[-3.45,0.18,0],[-2.1,0.23,0],0.045,steel);
  for(let i=0;i<3;i++){
    const blade=box('PropellerBlade_'+i,[0.06,0.42,0.11],[-3.42,0.18,0],yellow);
    blade.rotation.x=i*Math.PI*2/3;
  }
  box('Rudder',[0.31,0.42,0.055],[-3.78,0.21,0],navy);
  return root;
}
function animate(root) {
  return [createClip('ServiceMotion',1,[positionTrack('Joint_ServiceTranslation',[
    {time:0,position:[carriageX+serviceOffset,1.35,0]},
    {time:0.5,position:[carriageX+serviceOffset+0.4,1.35,0]},
    {time:1,position:[carriageX+serviceOffset,1.35,0]}
  ])])];
}

Brief and retained revisions

Brief summary

Build an 8 m research vessel with a broad working stern, narrow raised bow, port-offset wheelhouse and open starboard deck. Shape a continuous curved hull with a visible chine, add a stern sampling crane and deck railings, and animate the crane carriage with its attached footplate and cable guide.

  1. Initial vessel

    First retained vessel source, with serviceOffset at 0.10.

    Download initial vessel source

    SHA-256: 6fb756d6f62075d27ca0bde6ad85bb1247a551042abf4dea9c504cb5fe70aa3e

  2. Reviewed authoring revision

    Moved serviceOffset to 0.25 and corrected two deck-shell winding expressions during the authoring run.

    Download reviewed authoring revision source

    SHA-256: a0e702f76ca06c736997982394c7a388b23053a552c1a3e3975da49515a7d430

  3. Current gallery revision · Shown here

    A separate supplied-source refinement changed only serviceOffset from 0.25 to 0.40. All unrelated source bytes were preserved; a possible small mid-motion overlap remains a review limitation.

    Download current gallery revision source

    SHA-256: 1f109fd8c5d6de9fe07507e473a1c7ba674565f1544ac68dd6067a5664371455