LoftedVessels
A bottle, a horn, and a boat hull, each one surface through several flat shapes placed along a path (Mesh.loft): a polygon foot blending into a round neck, a star rounding into a circle as it turns, and open ribs lofted into a hull with no caps.
- Run
swift run --package-path Examples Example-3D-Geometry-LoftedVessels- Source
- Examples/3D/Geometry/LoftedVessels/Sketch.swift
import Ollin
/// **Lofted vessels**: a bottle, a horn, and a boat hull, each one surface through
/// several flat shapes placed along a path.
///
/// A sweep carries one shape along a path. A loft (`Mesh.loft`) carries several, one
/// after another, and blends the surface from each to the next. The bottle runs up
/// a straight spine from a polygon foot through a round belly to a narrow neck and a
/// lip; the horn follows a bending spine from a star to a circle, turning on the
/// way; the hull is a sheet through open U-shaped ribs from bow to stern, so it has
/// no caps and an inside you can see into.
///
/// Every section is laid exactly where it was asked for, corners and all, and the
/// outlines are matched round their lengths with each seam turned to the nearest
/// start, so the skin does not twist on its own. With **smoothed** on, the surface
/// runs as a smooth curve through the sections; off, it runs straight from rib to
/// rib, and the stations show as soft ridges.
///
/// Try it: give the foot three **sides** for a triangular bottle, or eight for one
/// that is nearly round all the way up; widen the **belly** and narrow the **neck**.
@main
final class LoftedVessels_Example: Sketch {
@Param(3 ... 8, icon: "square") var sides = 4
@Param(0.5 ... 1.1, icon: "circle") var belly = 0.9
@Param(0.12 ... 0.4, icon: "circle.dotted") var neck = 0.22
@Param(icon: "scribble.variable") var smoothed = true
private var bottle = Mesh(positions: [], indices: [])
private var horn = Mesh(positions: [], indices: [])
private var hull = Mesh(positions: [], indices: [])
private var built: (sides: Int, belly: Double, neck: Double, smoothed: Bool)?
override func draw() {
if built.map({ $0 != (sides, belly, neck, smoothed) }) ?? true { rebuild() }
background(Color(hex: 0x0B0C12))
cameraShowcase(.orbitAndRise(period: 36, rise: 0.25, in: 18),
radius: 9.6, elevation: 0.3, fieldOfView: .pi / 3.8)
directionalLight(.white, direction: Vector3(-0.4, -0.75, -0.5), intensity: 1.0)
headlight(intensity: 0.35)
ambientLight(Color(white: 0.24))
specular(0.4); specularSharpness(44)
// The three stand on one floor, at the corners of a triangle, so the
// orbit never hides two behind the third for long.
withState {
translate(-2.5, -1.4, 1.1)
fill(Color(hex: 0x6FC3DF))
drawMesh(bottle)
}
withState {
translate(2.3, -1.4, 0.9)
fill(Color(hex: 0xF2B544))
drawMesh(horn)
}
withState {
translate(0, -0.55, -1.9)
rotateY(0.35)
fill(Color(hex: 0xE4572E))
drawMesh(hull)
}
}
private func rebuild() {
// The bottle: a polygon foot, a round belly, a narrow neck, and a lip,
// up a straight spine. Sections are listed with where each sits.
let spine = Curve3D((0...48).map { Vector3(0, Double($0) / 48 * 3.0, 0) })
let foot = Profile.polygon(sides: sides, radius: 0.72)
let round = { (r: Double) in Profile.ellipse(radiusX: r, radiusY: r, segments: 40) }
bottle = .loft([foot, round(belly), round(belly * 0.92), round(neck), round(neck * 1.25), round(neck * 1.25)],
along: spine, at: [0, 0.3, 0.52, 0.76, 0.9, 1], smoothed: smoothed)
// The horn: a star that rounds into a circle along a bending spine,
// turning half a turn on the way and shrinking toward its tip.
let bend = Curve3D(curveThrough: [Vector3(0, 0, 0), Vector3(0.4, 1.2, 0.3), Vector3(-0.3, 2.2, -0.2),
Vector3(0.6, 3.0, 0.2)], segments: 16)
horn = .loft([Profile.star(points: 6, outerRadius: 0.75, innerRadius: 0.42),
Profile.ellipse(radiusX: 0.55, radiusY: 0.55, segments: 36),
Profile.ellipse(radiusX: 0.3, radiusY: 0.3, segments: 36)],
along: bend, smoothed: smoothed, scale: { 1 - 0.55 * $0 }, twist: { $0 * .pi })
// The hull: open U-shaped ribs from the bow to the stern, lofted as a
// sheet with no caps. Each rib is a few points in the frame's plane,
// x across the boat and y up, drawn from the port gunwale down round
// the keel and up to starboard.
func rib(halfBeam: Double, depth: Double, flare: Double) -> Shape {
Shape(contours: [Contour([Vector2(-halfBeam - flare, 0.7), Vector2(-halfBeam, 0.25),
Vector2(-halfBeam * 0.55, -depth * 0.8), Vector2(0, -depth),
Vector2(halfBeam * 0.55, -depth * 0.8), Vector2(halfBeam, 0.25),
Vector2(halfBeam + flare, 0.7)], closed: false)])
}
let keel = Curve3D((0...40).map { Vector3(-1.6 + Double($0) / 40 * 3.2, 0, 0) })
hull = .loft([rib(halfBeam: 0.02, depth: 0.2, flare: 0.02), rib(halfBeam: 0.4, depth: 0.65, flare: 0.2),
rib(halfBeam: 0.66, depth: 0.85, flare: 0.18), rib(halfBeam: 0.62, depth: 0.76, flare: 0.12),
rib(halfBeam: 0.44, depth: 0.56, flare: 0.08)],
along: keel, at: [0, 0.2, 0.5, 0.78, 1], smoothed: smoothed)
built = (sides, belly, neck, smoothed)
}
}