1.1K tris2.27 × 10.1 × 6.36 m
Use it in code
Remix in the editorsailboat.js · 218 lines
Self-contained ES module — only depends on three. Call the factory and add the group to your scene.
/**
* Sailboat — Codels 3D model library
* License: CC0-1.0 · Units: meters · Y-up · Front: +Z · Origin: center of the base (y = 0)
* Triangles: 1,084 · Size (W × H × D): 2.27 × 10.10 × 6.36 m
*
* Usage:
* import { createSailboat } from './sailboat.js';
*
* const model = createSailboat();
* model.position.set(0, 0, 0);
* scene.add(model);
*/
import * as THREE from 'three';
const deg = THREE.MathUtils.degToRad;
// Extrude a 2D outline along Z (centered). bevel = [thickness, size, segments].
function extrude(outline, depth, holes = [], bevel = null) {
const shape = new THREE.Shape(outline.map(([x, y]) => new THREE.Vector2(x, y)));
for (const hole of holes) shape.holes.push(new THREE.Path(hole.map(([x, y]) => new THREE.Vector2(x, y))));
const options = bevel
? { depth, bevelEnabled: true, bevelThickness: bevel[0], bevelSize: bevel[1], bevelSegments: bevel[2] }
: { depth, bevelEnabled: false };
return new THREE.ExtrudeGeometry(shape, options).translate(0, 0, -depth / 2);
}
// Tube along a smooth curve through the given points.
function tube(path, radius, tubularSegments, radialSegments, closed = false) {
const curve = new THREE.CatmullRomCurve3(path.map(([x, y, z]) => new THREE.Vector3(x, y, z)), closed);
return new THREE.TubeGeometry(curve, tubularSegments, radius, radialSegments, closed);
}
export function createSailboat() {
const root = new THREE.Group();
root.name = 'Sailboat';
// Materials
const hullMat = new THREE.MeshStandardMaterial({ name: 'hull', color: 0x24527d, roughness: 0.45, metalness: 0.1, flatShading: true });
const whiteMat = new THREE.MeshStandardMaterial({ name: 'white', color: 0xf1eee6, roughness: 0.55, flatShading: true });
const woodMat = new THREE.MeshStandardMaterial({ name: 'wood', color: 0xb98a5c, roughness: 0.85, flatShading: true });
const sailMat = new THREE.MeshStandardMaterial({ name: 'sail', color: 0xf5f1e6, roughness: 0.8, flatShading: true });
const accentMat = new THREE.MeshStandardMaterial({ name: 'accent', color: 0xd6493c, roughness: 0.5, flatShading: true });
const darkMat = new THREE.MeshStandardMaterial({ name: 'dark', color: 0x3b3f46, roughness: 0.5, metalness: 0.4, flatShading: true });
const glassMat = new THREE.MeshStandardMaterial({ name: 'glass', color: 0x2a3746, roughness: 0.2, metalness: 0.3, flatShading: true });
// Shared geometries
const boxGeo = new THREE.BoxGeometry(0.02, 0.14, 0.32);
const boxGeo2 = new THREE.BoxGeometry(0.06, 0.16, 1.46);
// Parts
const hull = new THREE.Group();
hull.name = 'hull';
root.add(hull);
const bowHull = new THREE.Mesh(new THREE.SphereGeometry(1, 12, 5, 0, Math.PI, Math.PI / 2, Math.PI / 2), hullMat);
bowHull.name = 'bowHull';
bowHull.position.set(0, 1.6, 0);
bowHull.scale.set(1.12, 0.55, 3.55);
hull.add(bowHull);
const sternHull = new THREE.Mesh(new THREE.SphereGeometry(1, 12, 5, Math.PI, Math.PI, Math.PI / 2, Math.PI / 2), hullMat);
sternHull.name = 'sternHull';
sternHull.position.set(0, 1.6, 0);
sternHull.scale.set(1.12, 0.55, 2.75);
hull.add(sternHull);
const topsides = new THREE.Mesh(extrude([[-1.12, 0], [-1.0818, 0.9188], [-0.9699, 1.775], [-0.792, 2.5102], [-0.56, 3.0744], [-0.2899, 3.429], [0, 3.55], [0.2899, 3.429], [0.56, 3.0744], [0.792, 2.5102], [0.9699, 1.775], [1.0818, 0.9188], [1.12, 0], [1.0818, -0.7118], [0.9699, -1.375], [0.792, -1.9445], [0.56, -2.3816], [0.2899, -2.6563], [0, -2.75], [-0.2899, -2.6563], [-0.56, -2.3816], [-0.792, -1.9445], [-0.9699, -1.375], [-1.0818, -0.7118]], 0.42), hullMat);
topsides.name = 'topsides';
topsides.position.set(0, 1.81, 0);
topsides.rotation.set(deg(90), 0, 0);
hull.add(topsides);
const waterlineStripe = new THREE.Mesh(extrude([[-1.12, 0], [-1.0818, 0.9188], [-0.9699, 1.775], [-0.792, 2.5102], [-0.56, 3.0744], [-0.2899, 3.429], [0, 3.55], [0.2899, 3.429], [0.56, 3.0744], [0.792, 2.5102], [0.9699, 1.775], [1.0818, 0.9188], [1.12, 0], [1.0818, -0.7118], [0.9699, -1.375], [0.792, -1.9445], [0.56, -2.3816], [0.2899, -2.6563], [0, -2.75], [-0.2899, -2.6563], [-0.56, -2.3816], [-0.792, -1.9445], [-0.9699, -1.375], [-1.0818, -0.7118]], 0.1), accentMat);
waterlineStripe.name = 'waterlineStripe';
waterlineStripe.position.set(0, 1.67, 0);
waterlineStripe.rotation.set(deg(90), 0, 0);
waterlineStripe.scale.set(1.012, 1.008, 1);
hull.add(waterlineStripe);
const deck = new THREE.Mesh(extrude([[-1.12, 0], [-1.0818, 0.9188], [-0.9699, 1.775], [-0.792, 2.5102], [-0.56, 3.0744], [-0.2899, 3.429], [0, 3.55], [0.2899, 3.429], [0.56, 3.0744], [0.792, 2.5102], [0.9699, 1.775], [1.0818, 0.9188], [1.12, 0], [1.0818, -0.7118], [0.9699, -1.375], [0.792, -1.9445], [0.56, -2.3816], [0.2899, -2.6563], [0, -2.75], [-0.2899, -2.6563], [-0.56, -2.3816], [-0.792, -1.9445], [-0.9699, -1.375], [-1.0818, -0.7118]], 0.06), woodMat);
deck.name = 'deck';
deck.position.set(0, 2.03, 0);
deck.rotation.set(deg(90), 0, 0);
deck.scale.set(1.015, 1.01, 1);
hull.add(deck);
const keel = new THREE.Mesh(extrude([[0.45, 1.13], [-0.3, 1.13], [-0.42, 0.14], [0.12, 0.14]], 0.1), darkMat);
keel.name = 'keel';
keel.rotation.set(0, deg(-90), 0);
hull.add(keel);
const keelBulb = new THREE.Mesh(new THREE.CapsuleGeometry(0.13, 0.7, 3, 8), darkMat);
keelBulb.name = 'keelBulb';
keelBulb.position.set(0, 0.13, -0.12);
keelBulb.rotation.set(deg(90), 0, 0);
hull.add(keelBulb);
const rudder = new THREE.Mesh(extrude([[-2.3, 1.5], [-2.62, 1.5], [-2.72, 0.75], [-2.45, 0.8]], 0.06), woodMat);
rudder.name = 'rudder';
rudder.rotation.set(0, deg(-90), 0);
hull.add(rudder);
const deckGear = new THREE.Group();
deckGear.name = 'deckGear';
root.add(deckGear);
const cabin = new THREE.Mesh(extrude([[1.65, 2.04], [1.32, 2.52], [-0.45, 2.56], [-0.52, 2.04]], 1.24, [], [0.04, 0.04, 2]), whiteMat);
cabin.name = 'cabin';
cabin.rotation.set(0, deg(-90), 0);
deckGear.add(cabin);
const box12 = new THREE.Mesh(boxGeo, glassMat);
box12.position.set(0.665, 2.33, 0.95);
deckGear.add(box12);
const box13 = new THREE.Mesh(boxGeo, glassMat);
box13.position.set(0.665, 2.33, 0.47);
deckGear.add(box13);
const box14 = new THREE.Mesh(boxGeo, glassMat);
box14.position.set(0.665, 2.33, -0.01);
deckGear.add(box14);
const box15 = new THREE.Mesh(boxGeo, glassMat);
box15.position.set(-0.665, 2.33, 0.95);
deckGear.add(box15);
const box16 = new THREE.Mesh(boxGeo, glassMat);
box16.position.set(-0.665, 2.33, 0.47);
deckGear.add(box16);
const box17 = new THREE.Mesh(boxGeo, glassMat);
box17.position.set(-0.665, 2.33, -0.01);
deckGear.add(box17);
const frontWindow = new THREE.Mesh(new THREE.BoxGeometry(0.9, 0.02, 0.28), glassMat);
frontWindow.name = 'frontWindow';
frontWindow.position.set(0, 2.31, 1.535);
frontWindow.rotation.set(deg(55.5), 0, 0);
deckGear.add(frontWindow);
const cockpitFloor = new THREE.Mesh(new THREE.BoxGeometry(1.3, 0.02, 1.4), darkMat);
cockpitFloor.name = 'cockpitFloor';
cockpitFloor.position.set(0, 2.062, -1.3);
deckGear.add(cockpitFloor);
const box20 = new THREE.Mesh(boxGeo2, whiteMat);
box20.position.set(0.68, 2.14, -1.3);
deckGear.add(box20);
const box21 = new THREE.Mesh(boxGeo2, whiteMat);
box21.position.set(-0.68, 2.14, -1.3);
deckGear.add(box21);
const box22 = new THREE.Mesh(new THREE.BoxGeometry(1.42, 0.16, 0.06), whiteMat);
box22.position.set(0, 2.14, -2.03);
deckGear.add(box22);
const rudderStock = new THREE.Mesh(new THREE.CylinderGeometry(0.03, 0.03, 0.3, 6), darkMat);
rudderStock.name = 'rudderStock';
rudderStock.position.set(0, 2.21, -2.5);
deckGear.add(rudderStock);
const tiller = new THREE.Mesh(new THREE.BoxGeometry(0.05, 0.05, 1), woodMat);
tiller.name = 'tiller';
tiller.position.set(0, 2.39, -2.05);
tiller.rotation.set(deg(-6), 0, 0);
deckGear.add(tiller);
const pulpit = new THREE.Mesh(tube([[-0.42, 2.06, 3], [-0.3, 2.51, 3.25], [0, 2.56, 3.45], [0.3, 2.51, 3.25], [0.42, 2.06, 3]], 0.02, 10, 4), darkMat);
pulpit.name = 'pulpit';
deckGear.add(pulpit);
const rig = new THREE.Group();
rig.name = 'rig';
root.add(rig);
const mast = new THREE.Mesh(new THREE.CylinderGeometry(0.055, 0.075, 8, 8), darkMat);
mast.name = 'mast';
mast.position.set(0, 6.02, 0.85);
rig.add(mast);
const masthead = new THREE.Mesh(new THREE.SphereGeometry(0.06, 6, 4), accentMat);
masthead.name = 'masthead';
masthead.position.set(0, 10.04, 0.85);
rig.add(masthead);
const forestay = new THREE.Mesh(tube([[0, 2.07, 3.43], [0, 9.92, 0.85]], 0.012, 1, 4), darkMat);
forestay.name = 'forestay';
rig.add(forestay);
const backstay = new THREE.Mesh(tube([[0, 9.92, 0.85], [0, 2.12, -2.63]], 0.012, 1, 4), darkMat);
backstay.name = 'backstay';
rig.add(backstay);
const shroudL = new THREE.Mesh(tube([[0, 8.82, 0.85], [1.07, 2.06, 0.6]], 0.012, 1, 4), darkMat);
shroudL.name = 'shroudL';
rig.add(shroudL);
const shroudR = new THREE.Mesh(tube([[0, 8.82, 0.85], [-1.07, 2.06, 0.6]], 0.012, 1, 4), darkMat);
shroudR.name = 'shroudR';
rig.add(shroudR);
const spreader = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.04, 0.05), darkMat);
spreader.name = 'spreader';
spreader.position.set(0, 6.62, 0.85);
rig.add(spreader);
const mainsail = new THREE.Group();
mainsail.name = 'mainsail';
mainsail.position.set(0, 0, 0.85);
mainsail.rotation.set(0, deg(-22), 0);
rig.add(mainsail);
const boom = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.05, 2.9, 6), darkMat);
boom.name = 'boom';
boom.position.set(0, 2.97, -1.45);
boom.rotation.set(deg(90), 0, 0);
mainsail.add(boom);
const mainSail = new THREE.Mesh(extrude([[-0.08, 3.03], [-2.85, 3.03], [-0.08, 9.77]], 0.03), sailMat);
mainSail.name = 'mainSail';
mainSail.rotation.set(0, deg(-90), 0);
mainsail.add(mainSail);
const sailStripe = new THREE.Mesh(extrude([[-0.08, 4.57], [-2.2471, 4.57], [-2.1649, 4.77], [-0.08, 4.77]], 0.05), accentMat);
sailStripe.name = 'sailStripe';
sailStripe.rotation.set(0, deg(-90), 0);
mainsail.add(sailStripe);
const jib = new THREE.Group();
jib.name = 'jib';
jib.position.set(0, 2.07, 3.43);
jib.rotation.set(deg(-18.1938), 0, 0);
rig.add(jib);
const jibSwing = new THREE.Group();
jibSwing.name = 'jibSwing';
jibSwing.rotation.set(0, deg(-14), 0);
jib.add(jibSwing);
const jibSail = new THREE.Mesh(extrude([[0, 0.25], [0, 6.4452], [-2.55, 1.6]], 0.03), sailMat);
jibSail.name = 'jibSail';
jibSail.rotation.set(0, deg(-90), 0);
jibSwing.add(jibSail);
root.traverse((o) => {
if (o.isMesh) {
o.castShadow = true;
o.receiveShadow = true;
}
});
return root;
}
export default createSailboat;
Ratings & comments
0 comments
No comments yet. Share how you used this model or what could be better.
Related models
Same category or similar tags









