How I built a 3D bottle carousel for Base Fragrances (and the glass effect that didn’t work)

The Base Fragrances homepage has six perfume bottles in a row. Drag the one in the middle and it spins, click a neighbour and it takes over. It’s real WebGL, not a video or a photo trick, and building it taught me a few things worth writing down.

The brief

A luxury fragrance brand needed a storefront that didn’t feel like a template, and the obvious centrepiece was the bottle itself. The catch: no 3D scan of the real bottle exists yet. So instead of waiting for one, I built the bottle from basic shapes in Three.js.

Building a bottle from primitives

The real bottle has faceted glass, so the body is an eight-sided prism. One detail matters more than it sounds: a prism with an even number of sides points an edge at the camera by default. Offsetting it by half a segment points a flat face at the camera instead, and that flat face is where the logo and name plate sit.

const FACETS = 8;
const FACE_OFFSET = Math.PI / FACETS;   // centres a flat face toward the camera

new THREE.CylinderGeometry(r, r, height, FACETS, 1, false, FACE_OFFSET);

Inside the glass is a slightly smaller prism for the liquid, coloured per fragrance from the product data. Some accent colours (a near-black variant, for example) are too dark to read against the page, so the liquid’s lightness is lifted when needed. Without that, the bottle looked like an empty silhouette. On top sit a short shoulder, a metal ring and a wide dark cap.

A name plate that doesn’t look like a sticker

The brass name plate is drawn on a 2D canvas and used as a texture: a brushed-metal gradient, a light and dark bevel, and a soft shadow behind the lettering. Two things made it look sharp instead of printed:

The logo also uses a lit material with an alpha cut-out rather than an unlit transparent one. That small change lets it catch the studio lighting like engraved metal instead of reading as a decal.

The glass effect that didn’t work

My first plan for the glass was Three.js’s physically based transmission, which refracts what’s behind it. It was the “correct” answer, and it failed in practice. Either it washed out each perfume’s liquid colour, which defeats the point of personalised bottles, or I turned it down until the colour came back and it stopped looking like glass at all.

So the body is simple alpha-blended glass, and the reflective environment goes where there’s no colour identity to lose: the metal cap and ring. That environment is a tiny custom “studio” — a few bright panels, like the softboxes in product photography — rendered once into a texture and shared by every bottle.

The lesson: the goal isn’t physical accuracy, it’s that the product looks right next to the real one. When the correct technique fights the brand’s colours, choose the one that serves the product.

Making six bottles cheap enough to run

Each of the six slots has its own renderer ready to go, but only the centre bottle is live. The neighbours are static, scaled down and dimmed. Neighbours sit at roughly 66% and 48% scale with falling opacity, which also sells the depth.

Switching bottles doesn’t tear scenes down and rebuild them. It stops the old bottle’s drag handling, starts the new one’s, and there is only ever one animation loop running. That avoids a whole class of clean-up bugs.

The carousel is also data-driven: it reads the product list from the store’s API, so adding a perfume adds a bottle, with its colour and scent notes.

Not blocking the page

The Three.js module is around 1.2 MB, and setting up six scenes is real CPU work. Originally it ran at page load and held up the rest of the page for several seconds. It now starts when the section is near the viewport, plus a short delayed start after load so there’s no empty canvas flash when someone scrolls straight there. The library is also served from the site itself rather than a CDN, so there’s one less thing to fail.

Idle motion that doesn’t hide the label

A full idle spin shows the name plate to the camera only about once a minute, which is a poor deal for a product whose name is on the label. Instead the bottle sways gently either side of the label-facing angle. Dragging still spins it freely, and the sway resumes a couple of seconds after you let go.

Fallbacks

If WebGL isn’t available, the canvas is hidden and a static image takes its place. If the visitor has asked their system for reduced motion, the auto-rotation is switched off.

Takeaways

Have something like this to build?

Tell me what you’re working on and I’ll reply with a straight answer on scope, price and timeline.